Initial commit: Nextcloud Node-RED Docker image and custom nodes
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
|||||||
|
# Node-RED runtime (local dev)
|
||||||
|
.flow.json
|
||||||
|
.flow_cred.json
|
||||||
|
.node-red/
|
||||||
|
|
||||||
|
# Dependencies (if installed locally for testing)
|
||||||
|
node_modules/
|
||||||
|
nodes/nextcloud-ocs/node_modules/
|
||||||
|
|
||||||
|
# OS / editor
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
*.pem
|
||||||
|
credentials.json
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# Docker local overrides
|
||||||
|
docker-compose.override.yml
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to **node-red-contrib-nextcloud-ocs** and the Docker image are documented here.
|
||||||
|
|
||||||
|
Format based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
||||||
|
|
||||||
|
## [0.8.0] - Unreleased
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Nodes: `core`, `oauth2`, `provisioning`, `filesharing`, `userstatus`, `settings`
|
||||||
|
- Nodes: `webhooks`, `dashboard`, `dav` (from 0.6.x line)
|
||||||
|
- Docker `ENTRYPOINT` runs `entrypoint.sh` (fixes nodes not copied to `/data`)
|
||||||
|
- Entrypoint logging and install verification
|
||||||
|
- Documentation: README, ARCHITECTURE, DEPLOYMENT, NODES, TROUBLESHOOTING
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Node-RED `userDir` forced to `/data` so palette loads from volume `node_modules`
|
||||||
|
- Removed brittle copy of default `settings.js` (path differs in base image)
|
||||||
|
- Stale `node-red-data` volume keeping old package versions (documented)
|
||||||
|
|
||||||
|
## [0.6.0]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `webhooks`, `dashboard`, `dav` nodes
|
||||||
|
- 10-node package (config + 9 functional nodes)
|
||||||
|
|
||||||
|
## [0.5.0]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Initial set: `nextcloud-config`, `ocs-api`, `collectives`, `file-operations`, `mail`, `tables`, `talk`
|
||||||
|
|
||||||
|
[0.8.0]: https://github.com/your-org/nextcloud-node-red/compare/v0.6.0...HEAD
|
||||||
|
[0.6.0]: https://github.com/your-org/nextcloud-node-red/compare/v0.5.0...v0.6.0
|
||||||
|
[0.5.0]: https://github.com/your-org/nextcloud-node-red/releases/tag/v0.5.0
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
FROM nodered/node-red:latest
|
||||||
|
|
||||||
|
USER root
|
||||||
|
|
||||||
|
COPY nodes/nextcloud-ocs /opt/nextcloud-nodes/node-red-contrib-nextcloud-ocs
|
||||||
|
COPY entrypoint.sh /entrypoint.sh
|
||||||
|
RUN chmod +x /entrypoint.sh
|
||||||
|
|
||||||
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
See https://www.gnu.org/licenses/agpl-3.0.txt for the full license text.
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
# Nextcloud Node-RED Integration
|
||||||
|
|
||||||
|
Custom [Node-RED](https://nodered.org/) Docker image with **16 palette nodes** that call Nextcloud APIs (OCS, WebDAV, and app-specific endpoints), plus a companion [Nextcloud app](../nodered-embed/) that embeds the editor in the Nextcloud UI.
|
||||||
|
|
||||||
|
## Repositories
|
||||||
|
|
||||||
|
| Directory | Purpose |
|
||||||
|
|-----------|---------|
|
||||||
|
| **`nextcloud-node-red/`** (this repo) | Docker image + `node-red-contrib-nextcloud-ocs` custom nodes |
|
||||||
|
| **`nodered-embed/`** | Nextcloud PHP app — iframe embed, CSP, admin settings |
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Nextcloud (https://your-cloud.example) │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ nodered_embed app → iframe → Node-RED :1880 │ │
|
||||||
|
│ │ CSPListener allows frame-src / frame-ancestors │ │
|
||||||
|
│ └───────────────────────────────────────────────────────────┘ │
|
||||||
|
└───────────────────────────────┬─────────────────────────────────┘
|
||||||
|
│ Basic Auth (app password)
|
||||||
|
▼
|
||||||
|
┌─────────────────────────────────────────────────────────────────┐
|
||||||
|
│ Docker: nextcloud-node-red │
|
||||||
|
│ userDir: /data → /data/node_modules/node-red-contrib-... │
|
||||||
|
│ entrypoint copies nodes from /opt at container start │
|
||||||
|
└─────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
See [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) for implementation details.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Linux host (or VM) with Docker
|
||||||
|
- Nextcloud 25+ (tested with modern OCS v2 APIs)
|
||||||
|
- **`network_mode: host`** on Linux so the container can reach Nextcloud on the same machine (avoids `127.0.0.1` / `/etc/hosts` issues inside the container)
|
||||||
|
- Nextcloud **app password** per user (Settings → Security → Devices & sessions)
|
||||||
|
|
||||||
|
## Quick start
|
||||||
|
|
||||||
|
### Docker Compose (recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone <your-repo-url> nextcloud-node-red
|
||||||
|
cd nextcloud-node-red
|
||||||
|
docker compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Open **http://localhost:1880** (or the host IP if remote).
|
||||||
|
|
||||||
|
### Manual Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t nextcloud-node-red:latest .
|
||||||
|
docker run -d \
|
||||||
|
--name nextcloud-node-red \
|
||||||
|
--network host \
|
||||||
|
-v node-red-data:/data \
|
||||||
|
--restart unless-stopped \
|
||||||
|
nextcloud-node-red:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Important:** Use `--network host` on Linux. Without it, `https://localhost` or your server hostname may resolve incorrectly inside the container.
|
||||||
|
|
||||||
|
### Nextcloud embed app
|
||||||
|
|
||||||
|
Copy `nodered-embed/` to your Nextcloud apps directory and enable it:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp -r nodered-embed /var/www/nextcloud/apps/
|
||||||
|
sudo -u www-data php /var/www/nextcloud/occ app:enable nodered_embed
|
||||||
|
```
|
||||||
|
|
||||||
|
In **Administration → Node-RED Embed**, set the Node-RED URL (e.g. `http://192.168.1.26:1880`). See [nodered-embed README](../nodered-embed/README.md).
|
||||||
|
|
||||||
|
## First-time setup in Node-RED
|
||||||
|
|
||||||
|
1. Drag a **nextcloud config** node onto the canvas (any Nextcloud node will prompt you).
|
||||||
|
2. Set **Nextcloud URL** to your instance, e.g. `https://cloud.example.com` (no trailing slash).
|
||||||
|
3. Set **Username** and **App password** (not your login password).
|
||||||
|
4. Deploy and test with **ocs-api** → operation `GET` on `/ocs/v2.php/cloud/user`.
|
||||||
|
|
||||||
|
## Custom nodes (overview)
|
||||||
|
|
||||||
|
| Node | Palette category | Description |
|
||||||
|
|------|------------------|-------------|
|
||||||
|
| `nextcloud-config` | config | Shared credentials (URL, user, app password) |
|
||||||
|
| `ocs-api` | nextcloud | Generic OCS HTTP (any path/method) |
|
||||||
|
| `collectives` | nextcloud | Collectives app (~80 operations) |
|
||||||
|
| `file-operations` | nextcloud | WebDAV files + OCS sharing |
|
||||||
|
| `mail` | nextcloud | Mail app |
|
||||||
|
| `tables` | nextcloud | Tables app |
|
||||||
|
| `talk` | nextcloud | Talk (Spreed) |
|
||||||
|
| `webhooks` | nextcloud | Webhook Listeners |
|
||||||
|
| `dashboard` | nextcloud | Dashboard widgets |
|
||||||
|
| `dav` | nextcloud | DAV (direct links, out-of-office) |
|
||||||
|
| `core` | nextcloud | Core OCS (capabilities, search, tasks, …) |
|
||||||
|
| `oauth2` | nextcloud | OAuth2 clients |
|
||||||
|
| `provisioning` | nextcloud | User/group provisioning API |
|
||||||
|
| `filesharing` | nextcloud | File sharing OCS |
|
||||||
|
| `userstatus` | nextcloud | User status |
|
||||||
|
| `settings` | nextcloud | User/admin settings |
|
||||||
|
|
||||||
|
Full operation lists: [docs/NODES.md](docs/NODES.md).
|
||||||
|
|
||||||
|
### Runtime overrides
|
||||||
|
|
||||||
|
Most nodes accept `msg` overrides:
|
||||||
|
|
||||||
|
| Property | Effect |
|
||||||
|
|----------|--------|
|
||||||
|
| `msg.operation` | Operation id (e.g. `collective:list`) |
|
||||||
|
| `msg.endpoint` | **ocs-api only** — API path |
|
||||||
|
| `msg.method` | **ocs-api only** — HTTP method |
|
||||||
|
| `msg.body` | JSON request body |
|
||||||
|
| `msg.headers` | Extra HTTP headers |
|
||||||
|
| Path params | `msg.collectiveId`, `msg.pageId`, `msg.userId`, etc. |
|
||||||
|
|
||||||
|
## Updating nodes after code changes
|
||||||
|
|
||||||
|
The image bakes nodes under `/opt/nextcloud-nodes/`. At **every container start**, `entrypoint.sh` copies them into the **`/data` volume** (Node-RED `userDir`). To pick up new node files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --no-cache -t nextcloud-node-red:latest .
|
||||||
|
docker rm -f nextcloud-node-red
|
||||||
|
docker run -d --name nextcloud-node-red --network host \
|
||||||
|
-v node-red-data:/data --restart unless-stopped nextcloud-node-red:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
If the palette still shows an old version, remove the volume ( **deletes flows** ):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker rm -f nextcloud-node-red
|
||||||
|
docker volume rm node-red-data
|
||||||
|
docker run -d ... # same run command as above
|
||||||
|
```
|
||||||
|
|
||||||
|
See [docs/TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md).
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
nextcloud-node-red/
|
||||||
|
├── Dockerfile # Base: nodered/node-red, COPY nodes + entrypoint
|
||||||
|
├── entrypoint.sh # Install nodes into /data, start node-red
|
||||||
|
├── docker-compose.yml # host network + named volume
|
||||||
|
├── nodes/nextcloud-ocs/ # node-red-contrib-nextcloud-ocs package
|
||||||
|
│ ├── package.json # node-red.nodes registry (16 nodes)
|
||||||
|
│ ├── nextcloud-config.js / .html
|
||||||
|
│ ├── ocs-api.js / .html
|
||||||
|
│ └── … # one .js + .html pair per node
|
||||||
|
└── docs/
|
||||||
|
├── ARCHITECTURE.md
|
||||||
|
├── DEPLOYMENT.md
|
||||||
|
├── NODES.md
|
||||||
|
└── TROUBLESHOOTING.md
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security notes
|
||||||
|
|
||||||
|
- Credentials are stored in Node-RED’s encrypted credentials store; set `credentialSecret` in `/data/settings.js` for production.
|
||||||
|
- Nodes use **Basic Auth** with app passwords; scope passwords per automation.
|
||||||
|
- `rejectUnauthorized: false` is used for HTTPS to allow self-signed certs — tighten for production if you use proper TLS.
|
||||||
|
- The Nextcloud embed iframe uses a restrictive `sandbox` attribute; adjust only if you need additional browser capabilities.
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
- [Architecture](docs/ARCHITECTURE.md)
|
||||||
|
- [Deployment guide](docs/DEPLOYMENT.md)
|
||||||
|
- [Node reference](docs/NODES.md)
|
||||||
|
- [Troubleshooting](docs/TROUBLESHOOTING.md)
|
||||||
|
- [Nextcloud embed app](../nodered-embed/README.md)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
AGPL-3.0 — see [LICENSE](LICENSE).
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
services:
|
||||||
|
node-red:
|
||||||
|
build: .
|
||||||
|
image: nextcloud-node-red:latest
|
||||||
|
container_name: nextcloud-node-red
|
||||||
|
restart: unless-stopped
|
||||||
|
network_mode: host
|
||||||
|
volumes:
|
||||||
|
- node-red-data:/data
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
node-red-data:
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
# Architecture
|
||||||
|
|
||||||
|
## Two components
|
||||||
|
|
||||||
|
### 1. `nextcloud-node-red` (Docker + nodes)
|
||||||
|
|
||||||
|
A thin wrapper around the official [`nodered/node-red`](https://hub.docker.com/r/nodered/node-red) image:
|
||||||
|
|
||||||
|
1. **Build time:** `nodes/nextcloud-ocs/` is copied to `/opt/nextcloud-nodes/node-red-contrib-nextcloud-ocs`.
|
||||||
|
2. **Run time:** `entrypoint.sh` runs as container `ENTRYPOINT`, copies that tree into `/data/node_modules/node-red-contrib-nextcloud-ocs`, then starts `node-red --userDir /data`.
|
||||||
|
|
||||||
|
Node-RED discovers contrib nodes from `{userDir}/node_modules/*/package.json` → `node-red.nodes` map.
|
||||||
|
|
||||||
|
### 2. `nodered-embed` (Nextcloud app)
|
||||||
|
|
||||||
|
PHP app registered as `nodered_embed`:
|
||||||
|
|
||||||
|
- **Navigation** entry opens a full-page iframe to the configured Node-RED URL.
|
||||||
|
- **CSPListener** relaxes Content-Security-Policy so Nextcloud may embed Node-RED (`frame-src`) and Node-RED may embed Nextcloud pages if needed (`frame-ancestors`).
|
||||||
|
- **Admin settings** store `nodered_url`, optional Docker container name, and a Docker-management flag (UI only for now).
|
||||||
|
|
||||||
|
## Node implementation pattern
|
||||||
|
|
||||||
|
Each functional node follows the same structure:
|
||||||
|
|
||||||
|
```
|
||||||
|
collectives.js → OPERATIONS hash (operationId → method, path, body fields)
|
||||||
|
collectives.html → Editor UI, dropdown of operations, config node picker
|
||||||
|
nextcloud-config.js → Credential node (baseUrl, username, password)
|
||||||
|
```
|
||||||
|
|
||||||
|
On input:
|
||||||
|
|
||||||
|
1. Resolve `msg.operation` or the configured default operation.
|
||||||
|
2. Substitute path placeholders (`{id}`, `{collectiveId}`, …) from config + `msg.*`.
|
||||||
|
3. Build query string and JSON body from configured fields + `msg` overrides.
|
||||||
|
4. HTTP request with `OCS-APIRequest: true`, `Accept: application/json`, Basic Auth.
|
||||||
|
5. Parse JSON into `msg.payload`, set `msg.statusCode`, `node.send(msg)`.
|
||||||
|
|
||||||
|
**ocs-api** is the escape hatch: you supply any OCS path and method without adding a new operation to the hash.
|
||||||
|
|
||||||
|
## Data persistence
|
||||||
|
|
||||||
|
| Path | Contents |
|
||||||
|
|------|----------|
|
||||||
|
| `/data/flows.json` | Flows (Docker volume `node-red-data`) |
|
||||||
|
| `/data/flows_cred.json` | Encrypted credentials |
|
||||||
|
| `/data/node_modules/node-red-contrib-nextcloud-ocs/` | Copy of custom nodes (refreshed on each start) |
|
||||||
|
|
||||||
|
The named volume **masks** anything copied into `/data` at image build time. That is why the entrypoint always re-copies from `/opt`.
|
||||||
|
|
||||||
|
## Network
|
||||||
|
|
||||||
|
```
|
||||||
|
Node-RED container (--network host)
|
||||||
|
→ https://cloud.example.com (same host or LAN)
|
||||||
|
```
|
||||||
|
|
||||||
|
On Ubuntu, a bridge network often breaks `localhost` / hostname resolution (`127.0.1.1` in `/etc/hosts`). Host networking avoids that for co-located Nextcloud + Node-RED.
|
||||||
|
|
||||||
|
## Authentication flow
|
||||||
|
|
||||||
|
```
|
||||||
|
Editor: nextcloud-config node
|
||||||
|
→ stored in flows_cred.json (encrypted)
|
||||||
|
|
||||||
|
Runtime: each API node
|
||||||
|
→ RED.nodes.getNode(config.nextcloud)
|
||||||
|
→ credentials.username + credentials.password
|
||||||
|
→ Authorization: Basic base64(user:app-password)
|
||||||
|
```
|
||||||
|
|
||||||
|
Use **app passwords**, not the account login password.
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
Package version in `nodes/nextcloud-ocs/package.json` (e.g. `0.8.0`) should be bumped when adding nodes or breaking operation ids. After deploy, verify inside the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-node-red cat /data/node_modules/node-red-contrib-nextcloud-ocs/package.json
|
||||||
|
```
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
# Deployment guide
|
||||||
|
|
||||||
|
## Target layout (example)
|
||||||
|
|
||||||
|
| Service | Host | Port |
|
||||||
|
|---------|------|------|
|
||||||
|
| Nextcloud | `https://cloud.example.com` | 443 |
|
||||||
|
| Node-RED | same VM, `192.168.1.26` | 1880 |
|
||||||
|
|
||||||
|
Node-RED must be reachable from users’ browsers (for the iframe) and from itself (for API calls to Nextcloud).
|
||||||
|
|
||||||
|
## 1. Deploy Node-RED (Docker)
|
||||||
|
|
||||||
|
On the Ubuntu VM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /home/ncadmin/nextcloud-node-red # or your clone path
|
||||||
|
git pull # get latest nodes + entrypoint
|
||||||
|
|
||||||
|
docker compose build --no-cache
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Or without Compose:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --no-cache -t nextcloud-node-red:latest .
|
||||||
|
docker rm -f nextcloud-node-red 2>/dev/null || true
|
||||||
|
docker run -d \
|
||||||
|
--name nextcloud-node-red \
|
||||||
|
--network host \
|
||||||
|
-v node-red-data:/data \
|
||||||
|
--restart unless-stopped \
|
||||||
|
nextcloud-node-red:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Verify:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs -n 30 nextcloud-node-red
|
||||||
|
# Expect: [entrypoint] Installing nextcloud nodes...
|
||||||
|
# User directory : /data
|
||||||
|
|
||||||
|
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:1880/
|
||||||
|
# Expect: 200
|
||||||
|
```
|
||||||
|
|
||||||
|
### Nextcloud URL in config nodes
|
||||||
|
|
||||||
|
Inside Node-RED, set **Nextcloud URL** to something the **container** can resolve:
|
||||||
|
|
||||||
|
| Scenario | URL |
|
||||||
|
|----------|-----|
|
||||||
|
| Nextcloud on same VM, host networking | `https://127.0.0.1` or `https://cloud.example.com` |
|
||||||
|
| Nextcloud on another host | `https://192.168.1.x` or public hostname |
|
||||||
|
|
||||||
|
Test from inside the container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-node-red wget -qO- --no-check-certificate \
|
||||||
|
https://cloud.example.com/status.php
|
||||||
|
```
|
||||||
|
|
||||||
|
## 2. Deploy Nextcloud app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo cp -r nodered-embed /var/www/nextcloud/apps/
|
||||||
|
sudo chown -R www-data:www-data /var/www/nextcloud/apps/nodered-embed
|
||||||
|
sudo -u www-data php /var/www/nextcloud/occ app:enable nodered_embed
|
||||||
|
```
|
||||||
|
|
||||||
|
Configure in **Settings → Administration → Node-RED Embed**:
|
||||||
|
|
||||||
|
- **Node-RED URL:** `http://192.168.1.26:1880` (use the address clients use; not `127.0.0.1` unless only local admins use it)
|
||||||
|
|
||||||
|
## 3. Reverse proxy (optional)
|
||||||
|
|
||||||
|
If Node-RED is behind nginx/Apache with TLS:
|
||||||
|
|
||||||
|
- Ensure the proxy does **not** send `X-Frame-Options: DENY` (blocks iframe embed).
|
||||||
|
- Point Nextcloud admin URL to the public HTTPS URL, e.g. `https://nodered.example.com`.
|
||||||
|
|
||||||
|
If Nextcloud is behind a proxy, CSP host extraction uses the hostname from `nodered_url` — use the same hostname users load in the iframe.
|
||||||
|
|
||||||
|
## 4. Firewall
|
||||||
|
|
||||||
|
Allow **1880/tcp** (or your mapped port) from Nextcloud users’ networks if they open the embed from LAN/VPN.
|
||||||
|
|
||||||
|
## 5. Updating custom nodes
|
||||||
|
|
||||||
|
After changing files under `nodes/nextcloud-ocs/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker builder prune -af # optional, avoids stale COPY cache
|
||||||
|
docker build --no-cache -t nextcloud-node-red:latest .
|
||||||
|
docker restart nextcloud-node-red
|
||||||
|
```
|
||||||
|
|
||||||
|
Confirm version:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-node-red cat \
|
||||||
|
/data/node_modules/node-red-contrib-nextcloud-ocs/package.json | grep version
|
||||||
|
```
|
||||||
|
|
||||||
|
### When to delete the volume
|
||||||
|
|
||||||
|
Delete `node-red-data` only if:
|
||||||
|
|
||||||
|
- Palette shows wrong/old nodes after rebuild, or
|
||||||
|
- Entrypoint copy failed with permission errors from old root-owned files
|
||||||
|
|
||||||
|
**Warning:** removes all flows and credentials.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker rm -f nextcloud-node-red
|
||||||
|
docker volume rm node-red-data
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Backup
|
||||||
|
|
||||||
|
Back up the Docker volume:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v node-red-data:/data -v $(pwd):/backup alpine \
|
||||||
|
tar czf /backup/node-red-data-backup.tar.gz -C /data .
|
||||||
|
```
|
||||||
|
|
||||||
|
Restore by extracting into a new volume before first start.
|
||||||
|
|
||||||
|
## 7. Production checklist
|
||||||
|
|
||||||
|
- [ ] Set `credentialSecret` in Node-RED settings (`/data/settings.js`)
|
||||||
|
- [ ] Use app passwords with minimal needed scopes
|
||||||
|
- [ ] TLS on Nextcloud; consider TLS on Node-RED if exposed beyond LAN
|
||||||
|
- [ ] Restrict who can access Node-RED (firewall / VPN / admin-only NC group)
|
||||||
|
- [ ] Enable Nextcloud app only for trusted admins if flows can access sensitive data
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
# Node reference
|
||||||
|
|
||||||
|
Package: **`node-red-contrib-nextcloud-ocs`** (see `nodes/nextcloud-ocs/package.json`).
|
||||||
|
|
||||||
|
All API nodes require a **nextcloud-config** configuration node unless noted.
|
||||||
|
|
||||||
|
## nextcloud-config
|
||||||
|
|
||||||
|
Configuration node — not placed on flows alone.
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| Name | Label in the editor |
|
||||||
|
| Nextcloud URL | Base URL, e.g. `https://cloud.example.com` |
|
||||||
|
| Username | Nextcloud account name |
|
||||||
|
| Password | **App password** (stored encrypted) |
|
||||||
|
|
||||||
|
## ocs-api
|
||||||
|
|
||||||
|
Generic OCS/HTTP caller.
|
||||||
|
|
||||||
|
| Config | Default |
|
||||||
|
|--------|---------|
|
||||||
|
| Endpoint | `/ocs/v2.php/cloud/user` |
|
||||||
|
| Method | `GET` |
|
||||||
|
|
||||||
|
**Runtime:** `msg.endpoint`, `msg.method`, `msg.body`, `msg.headers`.
|
||||||
|
|
||||||
|
## collectives
|
||||||
|
|
||||||
|
Nextcloud **Collectives** app. Operations use ids like `collective:list`, `page:create`, `share:updatePageShare`, etc.
|
||||||
|
|
||||||
|
Categories include: collectives, pages, trash, shares, templates, tags, user settings, sessions, public (token) APIs, search.
|
||||||
|
|
||||||
|
~80 operations — see `collectives.js` `OPERATIONS` for the full list.
|
||||||
|
|
||||||
|
## file-operations
|
||||||
|
|
||||||
|
| Operation | Protocol |
|
||||||
|
|-----------|----------|
|
||||||
|
| `list`, `get`, `upload`, `mkdir`, `move`, `copy`, `delete` | WebDAV `/remote.php/dav/files/{user}/…` |
|
||||||
|
| `listShares`, `createShare`, `deleteShare` | OCS sharing API |
|
||||||
|
| `fileInfo`, `favorites` | OCS files API |
|
||||||
|
|
||||||
|
Path placeholders: `{user}`, `{folder}`, `{path}`, `{id}`.
|
||||||
|
|
||||||
|
## mail
|
||||||
|
|
||||||
|
Mail app OCS endpoints (send, list messages, accounts, etc.). See `mail.js`.
|
||||||
|
|
||||||
|
## tables
|
||||||
|
|
||||||
|
Tables app — tables, views, rows, columns, shares. See `tables.js`.
|
||||||
|
|
||||||
|
## talk
|
||||||
|
|
||||||
|
Talk (Spreed) — rooms, messages, reactions, polls, bots, breakout rooms, etc. Largest node (~80+ operations). See `talk.js`.
|
||||||
|
|
||||||
|
## webhooks
|
||||||
|
|
||||||
|
Webhook Listeners app:
|
||||||
|
|
||||||
|
| Operation | Method |
|
||||||
|
|-----------|--------|
|
||||||
|
| `webhook:list` | GET |
|
||||||
|
| `webhook:get` | GET |
|
||||||
|
| `webhook:create` | POST |
|
||||||
|
| `webhook:update` | POST |
|
||||||
|
| `webhook:delete` | DELETE |
|
||||||
|
| `webhook:deleteByApp` | DELETE |
|
||||||
|
|
||||||
|
## dashboard
|
||||||
|
|
||||||
|
Dashboard widgets API. See `dashboard.js`.
|
||||||
|
|
||||||
|
## dav
|
||||||
|
|
||||||
|
DAV-related OCS (direct links, out-of-office). See `dav.js`.
|
||||||
|
|
||||||
|
## core
|
||||||
|
|
||||||
|
Nextcloud **core** OCS and related APIs (~50 operations), including:
|
||||||
|
|
||||||
|
- Status, capabilities, app passwords
|
||||||
|
- Navigation, profile, hover card
|
||||||
|
- Collaboration resources, references, previews, avatars
|
||||||
|
- CSRF, login flow v2, wipe, OCM discovery
|
||||||
|
- Unified search, task processing, text processing
|
||||||
|
- Translation, text-to-image, teams
|
||||||
|
|
||||||
|
Operation ids use prefixes: `status:`, `capabilities:`, `appPassword:`, `search:`, `task:`, etc. See `core.js`.
|
||||||
|
|
||||||
|
## oauth2
|
||||||
|
|
||||||
|
OAuth2 client management OCS. See `oauth2.js`.
|
||||||
|
|
||||||
|
## provisioning
|
||||||
|
|
||||||
|
User and group provisioning API. See `provisioning.js`.
|
||||||
|
|
||||||
|
## filesharing
|
||||||
|
|
||||||
|
Dedicated file sharing OCS (beyond WebDAV in file-operations). See `filesharing.js`.
|
||||||
|
|
||||||
|
## userstatus
|
||||||
|
|
||||||
|
User status (emoji/message). See `userstatus.js`.
|
||||||
|
|
||||||
|
## settings
|
||||||
|
|
||||||
|
User and admin settings OCS. See `settings.js`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Common message properties
|
||||||
|
|
||||||
|
| Property | Used by |
|
||||||
|
|----------|---------|
|
||||||
|
| `msg.operation` | All operation-based nodes |
|
||||||
|
| `msg.payload` | Response body (output) |
|
||||||
|
| `msg.statusCode` | HTTP status (output) |
|
||||||
|
| `msg.error` | Error string on failure |
|
||||||
|
|
||||||
|
Path parameters are typically available as `msg.<name>` matching the placeholder (e.g. `msg.collectiveId`, `msg.pageId`, `msg.webhookId`).
|
||||||
|
|
||||||
|
Body fields configured in the editor can often be overridden via `msg.body` object or node-specific `msg.body*` fields — check each node’s `buildBody` / handler in the `.js` file.
|
||||||
|
|
||||||
|
## Adding a new operation
|
||||||
|
|
||||||
|
1. Add entry to `OPERATIONS` in the relevant `.js` file.
|
||||||
|
2. Add dropdown option and fields in the matching `.html` file.
|
||||||
|
3. Bump `version` in `package.json`.
|
||||||
|
4. Rebuild Docker image and restart container.
|
||||||
|
|
||||||
|
For one-off APIs, use **ocs-api** instead of extending a dedicated node.
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
## Nodes missing from palette
|
||||||
|
|
||||||
|
### Symptom
|
||||||
|
|
||||||
|
Node-RED runs but Nextcloud nodes do not appear under **Manage palette → Nodes**.
|
||||||
|
|
||||||
|
### Checks
|
||||||
|
|
||||||
|
1. **Entrypoint ran**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker logs nextcloud-node-red 2>&1 | head -20
|
||||||
|
```
|
||||||
|
|
||||||
|
Expect:
|
||||||
|
|
||||||
|
```
|
||||||
|
[entrypoint] Installing nextcloud nodes into /data...
|
||||||
|
[entrypoint] Source: /opt/nextcloud-nodes/node-red-contrib-nextcloud-ocs
|
||||||
|
[entrypoint] Target: /data/node_modules/node-red-contrib-nextcloud-ocs
|
||||||
|
```
|
||||||
|
|
||||||
|
If you only see the Node-RED banner with no `[entrypoint]` lines, the image may still use `CMD` instead of `ENTRYPOINT ["/entrypoint.sh"]` — rebuild from current `Dockerfile`.
|
||||||
|
|
||||||
|
2. **Module installed in userDir**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-node-red ls /data/node_modules/node-red-contrib-nextcloud-ocs
|
||||||
|
docker exec nextcloud-node-red cat /data/node_modules/node-red-contrib-nextcloud-ocs/package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **User directory**
|
||||||
|
|
||||||
|
Logs must show `User directory : /data`. If it shows `/usr/src/node-red/.node-red/`, entrypoint did not start Node-RED with `--userDir /data`.
|
||||||
|
|
||||||
|
4. **Stale volume (old package version)**
|
||||||
|
|
||||||
|
Compare `/opt` vs `/data`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-node-red sh -c \
|
||||||
|
'grep version /opt/nextcloud-nodes/node-red-contrib-nextcloud-ocs/package.json; \
|
||||||
|
grep version /data/node_modules/node-red-contrib-nextcloud-ocs/package.json'
|
||||||
|
```
|
||||||
|
|
||||||
|
If `/opt` is newer, restart should copy on boot. If `/data` stays old, remove volume (loses flows):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker rm -f nextcloud-node-red
|
||||||
|
docker volume rm node-red-data
|
||||||
|
docker run -d --name nextcloud-node-red --network host \
|
||||||
|
-v node-red-data:/data --restart unless-stopped nextcloud-node-red:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
5. **Browser cache**
|
||||||
|
|
||||||
|
Hard refresh (Ctrl+F5) or open Node-RED in a private window.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Container restart loop
|
||||||
|
|
||||||
|
### Symptom
|
||||||
|
|
||||||
|
`docker ps` shows `Restarting`; logs repeat entrypoint lines.
|
||||||
|
|
||||||
|
### Cause
|
||||||
|
|
||||||
|
Old `entrypoint.sh` tried to copy missing `settings.js`:
|
||||||
|
|
||||||
|
```
|
||||||
|
cp: can't stat '/usr/src/node-red/.node-red/settings.js': No such file or directory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fix
|
||||||
|
|
||||||
|
Use current `entrypoint.sh` (starts `node-red --userDir /data` only). Rebuild:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build --no-cache -t nextcloud-node-red:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `No Nextcloud configuration node selected`
|
||||||
|
|
||||||
|
Flow nodes reference a deleted or unconfigured **nextcloud-config** node. Open each red Nextcloud node and re-select the config node, then Deploy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API errors / connection refused
|
||||||
|
|
||||||
|
| Issue | Fix |
|
||||||
|
|-------|-----|
|
||||||
|
| `ECONNREFUSED` to localhost | Use `--network host` or use LAN IP / hostname, not `127.0.0.1` from bridge network |
|
||||||
|
| SSL errors | Self-signed: nodes set `rejectUnauthorized: false`; or install proper certs |
|
||||||
|
| 401 Unauthorized | Use **app password**, correct username |
|
||||||
|
| 404 on OCS path | App not installed/enabled on Nextcloud (Collectives, Talk, etc.) |
|
||||||
|
|
||||||
|
Test from container:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-node-red wget -qO- --header="OCS-APIRequest: true" \
|
||||||
|
--user='USER:APP_PASSWORD' \
|
||||||
|
https://cloud.example.com/ocs/v2.php/cloud/user
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Docker build shows old files
|
||||||
|
|
||||||
|
Build context must include updated `nodes/nextcloud-ocs/`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep version nodes/nextcloud-ocs/package.json # on host before build
|
||||||
|
docker build --no-cache -t nextcloud-node-red:latest .
|
||||||
|
```
|
||||||
|
|
||||||
|
`docker builder prune -af` clears COPY layer cache.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Nextcloud iframe blank
|
||||||
|
|
||||||
|
1. Browser DevTools → Console: CSP violations.
|
||||||
|
2. Confirm **nodered_embed** is enabled and admin URL matches iframe target host.
|
||||||
|
3. `CSPListener` adds `frame-src` for the hostname from `nodered_url`.
|
||||||
|
4. Reverse proxy must not set `X-Frame-Options: DENY` on Node-RED.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -I https://cloud.example.com | grep -i frame
|
||||||
|
curl -I http://192.168.1.26:1880 | grep -i frame
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Permission denied on entrypoint copy
|
||||||
|
|
||||||
|
Old files in volume owned by root. Fix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker rm -f nextcloud-node-red
|
||||||
|
docker volume rm node-red-data
|
||||||
|
# recreate container
|
||||||
|
```
|
||||||
|
|
||||||
|
Or fix ownership once:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec -u root nextcloud-node-red chown -R node-red:node-red /data
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Verify node discovery manually
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker exec nextcloud-node-red node -e "
|
||||||
|
const p=require('/data/node_modules/node-red-contrib-nextcloud-ocs/package.json');
|
||||||
|
console.log(p.version, Object.keys(p['node-red'].nodes).length, 'nodes');
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: version from `package.json` and **16** node types.
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Clean and install Nextcloud OCS nodes into /data at runtime.
|
||||||
|
# /data is a Docker volume that masks build-time COPY.
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
echo "[entrypoint] Installing nextcloud nodes into /data..."
|
||||||
|
|
||||||
|
SRC="/opt/nextcloud-nodes/node-red-contrib-nextcloud-ocs"
|
||||||
|
DST_DIR="/data/node_modules"
|
||||||
|
DST="$DST_DIR/node-red-contrib-nextcloud-ocs"
|
||||||
|
|
||||||
|
echo "[entrypoint] Source: $SRC"
|
||||||
|
echo "[entrypoint] Target: $DST"
|
||||||
|
|
||||||
|
if [ ! -d "$SRC" ]; then
|
||||||
|
echo "[entrypoint] ERROR: source directory missing: $SRC" >&2
|
||||||
|
ls -la /opt || true
|
||||||
|
ls -la /opt/nextcloud-nodes || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "$DST_DIR"
|
||||||
|
rm -rf "$DST"
|
||||||
|
cp -a "$SRC" "$DST_DIR/"
|
||||||
|
|
||||||
|
if [ ! -f "$DST/package.json" ]; then
|
||||||
|
echo "[entrypoint] ERROR: install did not produce $DST/package.json" >&2
|
||||||
|
echo "[entrypoint] Debug: listing /data and /data/node_modules" >&2
|
||||||
|
ls -la /data || true
|
||||||
|
ls -la "$DST_DIR" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Ensure Node-RED can read editor files and load nodes as the non-root user.
|
||||||
|
chown -R node-red:node-red /data "$DST_DIR" "$DST" || true
|
||||||
|
chmod -R a+rX "$DST" || true
|
||||||
|
|
||||||
|
export NODE_RED_USER_DIR=/data
|
||||||
|
|
||||||
|
# Start Node-RED explicitly with /data as userDir so it discovers
|
||||||
|
# nodes from /data/node_modules/** based on each module's package.json.
|
||||||
|
exec node-red --userDir /data
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('collectives', {
|
||||||
|
category: 'nextcloud',
|
||||||
|
color: '#0082C9',
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
nextcloud: { type: "nextcloud-config", required: true },
|
||||||
|
operation: { value: "collective:list" },
|
||||||
|
// Path params
|
||||||
|
collectiveId: { value: "" },
|
||||||
|
pageId: { value: "" },
|
||||||
|
token: { value: "" },
|
||||||
|
parentId: { value: "" },
|
||||||
|
tagId: { value: "" },
|
||||||
|
attachmentId: { value: "" },
|
||||||
|
newCollectiveId: { value: "" },
|
||||||
|
itemId: { value: "" },
|
||||||
|
settingKey: { value: "" },
|
||||||
|
// Body fields
|
||||||
|
bodyName: { value: "" },
|
||||||
|
bodyEmoji: { value: "" },
|
||||||
|
bodyColor: { value: "" },
|
||||||
|
bodyLevel: { value: "" },
|
||||||
|
bodyMode: { value: "" },
|
||||||
|
bodyTitle: { value: "" },
|
||||||
|
bodyEditable: { value: "" },
|
||||||
|
bodyPassword: { value: "" },
|
||||||
|
bodyFullWidth: { value: "" },
|
||||||
|
bodyTemplateId: { value: "" },
|
||||||
|
bodyPageOrder: { value: "" },
|
||||||
|
bodyShowMembers: { value: "" },
|
||||||
|
bodyShowRecentPages: { value: "" },
|
||||||
|
bodyFavoritePages: { value: "" },
|
||||||
|
bodyIndex: { value: "" },
|
||||||
|
bodyCopy: { value: "" },
|
||||||
|
bodySubpageOrder: { value: "" },
|
||||||
|
bodyParentId: { value: "" },
|
||||||
|
bodyCircle: { value: "" },
|
||||||
|
bodySearchString: { value: "" },
|
||||||
|
bodyQuery: { value: "" },
|
||||||
|
bodyLimit: { value: "" },
|
||||||
|
bodyKey: { value: "" },
|
||||||
|
bodyValue: { value: "" },
|
||||||
|
bodySessionToken: { value: "" }
|
||||||
|
},
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 1,
|
||||||
|
icon: "collectives.svg",
|
||||||
|
label: function() { return this.name || "Collectives"; },
|
||||||
|
oneditprepare: function() {
|
||||||
|
var node = this;
|
||||||
|
|
||||||
|
// Which path params and body fields are needed per operation
|
||||||
|
var opMeta = {
|
||||||
|
'collective:list': { method: 'GET', pathParams: [], bodyFields: [] },
|
||||||
|
'collective:create': { method: 'POST', pathParams: [], bodyFields: ['name','emoji'] },
|
||||||
|
'collective:update': { method: 'PUT', pathParams: ['id'], bodyFields: ['emoji'] },
|
||||||
|
'collective:trash': { method: 'DELETE', pathParams: ['id'], bodyFields: [] },
|
||||||
|
'collective:editLevel': { method: 'PUT', pathParams: ['id'], bodyFields: ['level'] },
|
||||||
|
'collective:shareLevel': { method: 'PUT', pathParams: ['id'], bodyFields: ['level'] },
|
||||||
|
'collective:pageMode': { method: 'PUT', pathParams: ['id'], bodyFields: ['mode'] },
|
||||||
|
'trash:list': { method: 'GET', pathParams: [], bodyFields: [] },
|
||||||
|
'trash:restore': { method: 'PATCH', pathParams: ['id'], bodyFields: [] },
|
||||||
|
'trash:delete': { method: 'DELETE', pathParams: ['id'], bodyFields: ['circle'] },
|
||||||
|
'share:listCollectiveShares': { method: 'GET', pathParams: ['collectiveId'], bodyFields: [] },
|
||||||
|
'share:createCollectiveShare': { method: 'POST', pathParams: ['collectiveId'], bodyFields: ['password'] },
|
||||||
|
'share:updateCollectiveShare': { method: 'PUT', pathParams: ['collectiveId','token'], bodyFields: ['editable','password'] },
|
||||||
|
'share:deleteCollectiveShare': { method: 'DELETE', pathParams: ['collectiveId','token'], bodyFields: [] },
|
||||||
|
'share:createPageShare': { method: 'POST', pathParams: ['collectiveId','pageId'], bodyFields: ['password'] },
|
||||||
|
'share:updatePageShare': { method: 'PUT', pathParams: ['collectiveId','pageId','token'], bodyFields: ['editable','password'] },
|
||||||
|
'share:deletePageShare': { method: 'DELETE', pathParams: ['collectiveId','pageId','token'], bodyFields: [] },
|
||||||
|
'search:recentPages': { method: 'GET', pathParams: [], bodyFields: ['query','limit'] },
|
||||||
|
'search:content': { method: 'GET', pathParams: ['collectiveId'], bodyFields: ['searchString'] },
|
||||||
|
'page:list': { method: 'GET', pathParams: ['collectiveId'], bodyFields: [] },
|
||||||
|
'page:get': { method: 'GET', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'page:create': { method: 'POST', pathParams: ['collectiveId','parentId'], bodyFields: ['title','templateId'] },
|
||||||
|
'page:moveOrCopy': { method: 'PUT', pathParams: ['collectiveId','id'], bodyFields: ['parentId','title','index','copy'] },
|
||||||
|
'page:trash': { method: 'DELETE', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'page:touch': { method: 'GET', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'page:fullWidth': { method: 'PUT', pathParams: ['collectiveId','id'], bodyFields: ['fullWidth'] },
|
||||||
|
'page:emoji': { method: 'PUT', pathParams: ['collectiveId','id'], bodyFields: ['emoji'] },
|
||||||
|
'page:subpageOrder': { method: 'PUT', pathParams: ['collectiveId','id'], bodyFields: ['subpageOrder'] },
|
||||||
|
'page:moveToCollective': { method: 'PUT', pathParams: ['collectiveId','id','newCollectiveId'], bodyFields: ['parentId','index','copy'] },
|
||||||
|
'page:addTag': { method: 'PUT', pathParams: ['collectiveId','id','tagId'], bodyFields: [] },
|
||||||
|
'page:removeTag': { method: 'DELETE', pathParams: ['collectiveId','id','tagId'], bodyFields: [] },
|
||||||
|
'page:listAttachments': { method: 'GET', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'page:uploadAttachment': { method: 'POST', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'page:renameAttachment': { method: 'PUT', pathParams: ['collectiveId','id','attachmentId'], bodyFields: ['name'] },
|
||||||
|
'page:deleteAttachment': { method: 'DELETE', pathParams: ['collectiveId','id','attachmentId'], bodyFields: [] },
|
||||||
|
'page:restoreAttachment': { method: 'PATCH', pathParams: ['collectiveId','id','attachmentId'], bodyFields: [] },
|
||||||
|
'page_trash:list': { method: 'GET', pathParams: ['collectiveId'], bodyFields: [] },
|
||||||
|
'page_trash:restore': { method: 'PATCH', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'page_trash:delete': { method: 'DELETE', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'template:list': { method: 'GET', pathParams: ['collectiveId'], bodyFields: [] },
|
||||||
|
'template:create': { method: 'POST', pathParams: ['collectiveId','id'], bodyFields: ['title','parentId'] },
|
||||||
|
'template:rename': { method: 'PUT', pathParams: ['collectiveId','id'], bodyFields: ['title'] },
|
||||||
|
'template:delete': { method: 'DELETE', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'template:emoji': { method: 'PUT', pathParams: ['collectiveId','id'], bodyFields: ['emoji'] },
|
||||||
|
'tag:list': { method: 'GET', pathParams: ['collectiveId'], bodyFields: [] },
|
||||||
|
'tag:create': { method: 'POST', pathParams: ['collectiveId'], bodyFields: ['name','color'] },
|
||||||
|
'tag:update': { method: 'PUT', pathParams: ['collectiveId','id'], bodyFields: ['name','color'] },
|
||||||
|
'tag:delete': { method: 'DELETE', pathParams: ['collectiveId','id'], bodyFields: [] },
|
||||||
|
'userSettings:pageOrder': { method: 'PUT', pathParams: ['collectiveId'], bodyFields: ['pageOrder'] },
|
||||||
|
'userSettings:showMembers': { method: 'PUT', pathParams: ['collectiveId'], bodyFields: ['showMembers'] },
|
||||||
|
'userSettings:showRecentPages':{ method: 'PUT', pathParams: ['collectiveId'], bodyFields: ['showRecentPages'] },
|
||||||
|
'userSettings:favoritePages': { method: 'PUT', pathParams: ['collectiveId'], bodyFields: ['favoritePages'] },
|
||||||
|
'session:create': { method: 'POST', pathParams: ['collectiveId'], bodyFields: [] },
|
||||||
|
'session:sync': { method: 'PUT', pathParams: ['collectiveId'], bodyFields: ['token'] },
|
||||||
|
'session:close': { method: 'DELETE', pathParams: ['collectiveId'], bodyFields: ['token'] },
|
||||||
|
'settings:get': { method: 'GET', pathParams: ['settingKey'], bodyFields: [] },
|
||||||
|
'settings:set': { method: 'POST', pathParams: [], bodyFields: ['key','value'] },
|
||||||
|
'public:getCollective': { method: 'GET', pathParams: ['token'], bodyFields: [] },
|
||||||
|
'public:listPages': { method: 'GET', pathParams: ['token'], bodyFields: [] },
|
||||||
|
'public:getPage': { method: 'GET', pathParams: ['token','id'], bodyFields: [] },
|
||||||
|
'public:createPage': { method: 'POST', pathParams: ['token','parentId'], bodyFields: ['title','templateId'] },
|
||||||
|
'public:moveOrCopyPage': { method: 'PUT', pathParams: ['token','id'], bodyFields: ['parentId','title','index','copy'] },
|
||||||
|
'public:trashPage': { method: 'DELETE', pathParams: ['token','id'], bodyFields: [] },
|
||||||
|
'public:touchPage': { method: 'GET', pathParams: ['token','id'], bodyFields: [] },
|
||||||
|
'public:fullWidth': { method: 'PUT', pathParams: ['token','id'], bodyFields: ['fullWidth'] },
|
||||||
|
'public:emoji': { method: 'PUT', pathParams: ['token','id'], bodyFields: ['emoji'] },
|
||||||
|
'public:subpageOrder': { method: 'PUT', pathParams: ['token','id'], bodyFields: ['subpageOrder'] },
|
||||||
|
'public:addTag': { method: 'PUT', pathParams: ['token','id','tagId'], bodyFields: [] },
|
||||||
|
'public:removeTag': { method: 'DELETE', pathParams: ['token','id','tagId'], bodyFields: [] },
|
||||||
|
'public:listAttachments': { method: 'GET', pathParams: ['token','id'], bodyFields: [] },
|
||||||
|
'public:uploadAttachment': { method: 'POST', pathParams: ['token','id'], bodyFields: [] },
|
||||||
|
'public:renameAttachment': { method: 'PUT', pathParams: ['token','id','attachmentId'], bodyFields: ['name'] },
|
||||||
|
'public:deleteAttachment': { method: 'DELETE', pathParams: ['token','id','attachmentId'], bodyFields: [] },
|
||||||
|
'public:restoreAttachment': { method: 'PATCH', pathParams: ['token','id','attachmentId'], bodyFields: [] },
|
||||||
|
'public:searchContent': { method: 'GET', pathParams: ['token'], bodyFields: ['searchString'] },
|
||||||
|
'public:listTrash': { method: 'GET', pathParams: ['token'], bodyFields: [] },
|
||||||
|
'public:restoreTrash': { method: 'PATCH', pathParams: ['token','id'], bodyFields: [] },
|
||||||
|
'public:deleteTrash': { method: 'DELETE', pathParams: ['token','id'], bodyFields: [] },
|
||||||
|
'public:listTemplates': { method: 'GET', pathParams: ['token'], bodyFields: [] },
|
||||||
|
'public:listTags': { method: 'GET', pathParams: ['token'], bodyFields: [] },
|
||||||
|
'public:createTag': { method: 'POST', pathParams: ['token'], bodyFields: ['name','color'] },
|
||||||
|
'public:updateTag': { method: 'PUT', pathParams: ['token','id'], bodyFields: ['name','color'] },
|
||||||
|
'public:deleteTag': { method: 'DELETE', pathParams: ['token','id'], bodyFields: [] }
|
||||||
|
};
|
||||||
|
|
||||||
|
// All path param IDs mapped to labels
|
||||||
|
var pathFields = {
|
||||||
|
'id': { label: 'ID', field: '#node-input-itemId' },
|
||||||
|
'collectiveId': { label: 'Collective ID', field: '#node-input-collectiveId' },
|
||||||
|
'pageId': { label: 'Page ID', field: '#node-input-pageId' },
|
||||||
|
'parentId': { label: 'Parent ID', field: '#node-input-parentId' },
|
||||||
|
'token': { label: 'Token', field: '#node-input-token' },
|
||||||
|
'tagId': { label: 'Tag ID', field: '#node-input-tagId' },
|
||||||
|
'attachmentId': { label: 'Attachment ID', field: '#node-input-attachmentId' },
|
||||||
|
'newCollectiveId': { label: 'New Collective ID', field: '#node-input-newCollectiveId' },
|
||||||
|
'settingKey': { label: 'Settings Key', field: '#node-input-settingKey' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// All body field IDs
|
||||||
|
var bodyFields = {
|
||||||
|
'name': '#body-name-row',
|
||||||
|
'emoji': '#body-emoji-row',
|
||||||
|
'color': '#body-color-row',
|
||||||
|
'level': '#body-level-row',
|
||||||
|
'mode': '#body-mode-row',
|
||||||
|
'title': '#body-title-row',
|
||||||
|
'editable': '#body-editable-row',
|
||||||
|
'password': '#body-password-row',
|
||||||
|
'fullWidth': '#body-fullWidth-row',
|
||||||
|
'templateId': '#body-templateId-row',
|
||||||
|
'pageOrder': '#body-pageOrder-row',
|
||||||
|
'showMembers': '#body-showMembers-row',
|
||||||
|
'showRecentPages': '#body-showRecentPages-row',
|
||||||
|
'favoritePages': '#body-favoritePages-row',
|
||||||
|
'index': '#body-index-row',
|
||||||
|
'copy': '#body-copy-row',
|
||||||
|
'subpageOrder': '#body-subpageOrder-row',
|
||||||
|
'parentId': '#body-parentId-row',
|
||||||
|
'circle': '#body-circle-row',
|
||||||
|
'searchString': '#body-searchString-row',
|
||||||
|
'query': '#body-query-row',
|
||||||
|
'limit': '#body-limit-row',
|
||||||
|
'key': '#body-key-row',
|
||||||
|
'value': '#body-value-row',
|
||||||
|
'token': '#body-sessionToken-row'
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateFields() {
|
||||||
|
var op = $('#node-input-operation').val();
|
||||||
|
var meta = opMeta[op] || { pathParams: [], bodyFields: [] };
|
||||||
|
|
||||||
|
// Show/hide path param fields
|
||||||
|
Object.keys(pathFields).forEach(function(key) {
|
||||||
|
var visible = meta.pathParams.indexOf(key) !== -1;
|
||||||
|
$(pathFields[key].field).closest('.form-row').toggle(visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show/hide body fields
|
||||||
|
Object.keys(bodyFields).forEach(function(key) {
|
||||||
|
var visible = meta.bodyFields.indexOf(key) !== -1;
|
||||||
|
$(bodyFields[key]).toggle(visible);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update method badge
|
||||||
|
$('#op-method').text(meta.method || 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#node-input-operation').change(updateFields);
|
||||||
|
setTimeout(updateFields, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="collectives">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Collectives">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Nextcloud Config</label>
|
||||||
|
<input type="text" id="node-input-nextcloud" placeholder="Select config node">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="— Collectives —">
|
||||||
|
<option value="collective:list">List All Collectives</option>
|
||||||
|
<option value="collective:create">Create Collective</option>
|
||||||
|
<option value="collective:update">Update Collective</option>
|
||||||
|
<option value="collective:trash">Trash Collective</option>
|
||||||
|
<option value="collective:editLevel">Set Edit Level</option>
|
||||||
|
<option value="collective:shareLevel">Set Share Level</option>
|
||||||
|
<option value="collective:pageMode">Set Page Mode</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Trash —">
|
||||||
|
<option value="trash:list">List Trashed Collectives</option>
|
||||||
|
<option value="trash:restore">Restore Collective</option>
|
||||||
|
<option value="trash:delete">Delete Collective Permanently</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Shares —">
|
||||||
|
<option value="share:listCollectiveShares">List Collective Shares</option>
|
||||||
|
<option value="share:createCollectiveShare">Create Collective Share</option>
|
||||||
|
<option value="share:updateCollectiveShare">Update Collective Share</option>
|
||||||
|
<option value="share:deleteCollectiveShare">Delete Collective Share</option>
|
||||||
|
<option value="share:createPageShare">Create Page Share</option>
|
||||||
|
<option value="share:updatePageShare">Update Page Share</option>
|
||||||
|
<option value="share:deletePageShare">Delete Page Share</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Search —">
|
||||||
|
<option value="search:recentPages">Search Recent Pages</option>
|
||||||
|
<option value="search:content">Search Page Content</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Pages —">
|
||||||
|
<option value="page:list">List Pages</option>
|
||||||
|
<option value="page:get">Get Page</option>
|
||||||
|
<option value="page:create">Create Page</option>
|
||||||
|
<option value="page:moveOrCopy">Move/Copy Page</option>
|
||||||
|
<option value="page:trash">Trash Page</option>
|
||||||
|
<option value="page:touch">Touch Page</option>
|
||||||
|
<option value="page:fullWidth">Set Full Width</option>
|
||||||
|
<option value="page:emoji">Set Emoji</option>
|
||||||
|
<option value="page:subpageOrder">Set Subpage Order</option>
|
||||||
|
<option value="page:moveToCollective">Move to Collective</option>
|
||||||
|
<option value="page:addTag">Add Tag</option>
|
||||||
|
<option value="page:removeTag">Remove Tag</option>
|
||||||
|
<option value="page:listAttachments">List Attachments</option>
|
||||||
|
<option value="page:uploadAttachment">Upload Attachment</option>
|
||||||
|
<option value="page:renameAttachment">Rename Attachment</option>
|
||||||
|
<option value="page:deleteAttachment">Delete Attachment</option>
|
||||||
|
<option value="page:restoreAttachment">Restore Attachment</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Page Trash —">
|
||||||
|
<option value="page_trash:list">List Trashed Pages</option>
|
||||||
|
<option value="page_trash:restore">Restore Page</option>
|
||||||
|
<option value="page_trash:delete">Delete Page Permanently</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Templates —">
|
||||||
|
<option value="template:list">List Templates</option>
|
||||||
|
<option value="template:create">Create Template</option>
|
||||||
|
<option value="template:rename">Rename Template</option>
|
||||||
|
<option value="template:delete">Delete Template</option>
|
||||||
|
<option value="template:emoji">Set Template Emoji</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Tags —">
|
||||||
|
<option value="tag:list">List Tags</option>
|
||||||
|
<option value="tag:create">Create Tag</option>
|
||||||
|
<option value="tag:update">Update Tag</option>
|
||||||
|
<option value="tag:delete">Delete Tag</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— User Settings —">
|
||||||
|
<option value="userSettings:pageOrder">Set Page Order</option>
|
||||||
|
<option value="userSettings:showMembers">Set Show Members</option>
|
||||||
|
<option value="userSettings:showRecentPages">Set Show Recent Pages</option>
|
||||||
|
<option value="userSettings:favoritePages">Set Favorite Pages</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Sessions —">
|
||||||
|
<option value="session:create">Create Session</option>
|
||||||
|
<option value="session:sync">Sync Session</option>
|
||||||
|
<option value="session:close">Close Session</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Settings —">
|
||||||
|
<option value="settings:get">Get User Setting</option>
|
||||||
|
<option value="settings:set">Set User Setting</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="— Public (Share Token) —">
|
||||||
|
<option value="public:getCollective">Get Public Collective</option>
|
||||||
|
<option value="public:listPages">Public: List Pages</option>
|
||||||
|
<option value="public:getPage">Public: Get Page</option>
|
||||||
|
<option value="public:createPage">Public: Create Page</option>
|
||||||
|
<option value="public:moveOrCopyPage">Public: Move/Copy Page</option>
|
||||||
|
<option value="public:trashPage">Public: Trash Page</option>
|
||||||
|
<option value="public:touchPage">Public: Touch Page</option>
|
||||||
|
<option value="public:fullWidth">Public: Set Full Width</option>
|
||||||
|
<option value="public:emoji">Public: Set Emoji</option>
|
||||||
|
<option value="public:subpageOrder">Public: Set Subpage Order</option>
|
||||||
|
<option value="public:addTag">Public: Add Tag</option>
|
||||||
|
<option value="public:removeTag">Public: Remove Tag</option>
|
||||||
|
<option value="public:listAttachments">Public: List Attachments</option>
|
||||||
|
<option value="public:uploadAttachment">Public: Upload Attachment</option>
|
||||||
|
<option value="public:renameAttachment">Public: Rename Attachment</option>
|
||||||
|
<option value="public:deleteAttachment">Public: Delete Attachment</option>
|
||||||
|
<option value="public:restoreAttachment">Public: Restore Attachment</option>
|
||||||
|
<option value="public:searchContent">Public: Search Content</option>
|
||||||
|
<option value="public:listTrash">Public: List Trash</option>
|
||||||
|
<option value="public:restoreTrash">Public: Restore Trash</option>
|
||||||
|
<option value="public:deleteTrash">Public: Delete Trash</option>
|
||||||
|
<option value="public:listTemplates">Public: List Templates</option>
|
||||||
|
<option value="public:listTags">Public: List Tags</option>
|
||||||
|
<option value="public:createTag">Public: Create Tag</option>
|
||||||
|
<option value="public:updateTag">Public: Update Tag</option>
|
||||||
|
<option value="public:deleteTag">Public: Delete Tag</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<span id="op-method" style="margin-left:8px; color:#888; font-size:0.85em;">GET</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="margin:10px 0; border:none; border-top:1px solid #ccc;">
|
||||||
|
<div style="font-weight:bold; margin-bottom:6px;">Path Parameters (shown based on operation)</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-collectiveId">Collective ID</label>
|
||||||
|
<input type="text" id="node-input-collectiveId" placeholder="Overridable via msg.collectiveId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-pageId">Page ID</label>
|
||||||
|
<input type="text" id="node-input-pageId" placeholder="Overridable via msg.pageId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-itemId">ID</label>
|
||||||
|
<input type="text" id="node-input-itemId" placeholder="Overridable via msg.itemId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-parentId">Parent ID</label>
|
||||||
|
<input type="text" id="node-input-parentId" placeholder="Overridable via msg.parentId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-token">Token</label>
|
||||||
|
<input type="text" id="node-input-token" placeholder="Overridable via msg.token">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-tagId">Tag ID</label>
|
||||||
|
<input type="text" id="node-input-tagId" placeholder="Overridable via msg.tagId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-attachmentId">Attachment ID</label>
|
||||||
|
<input type="text" id="node-input-attachmentId" placeholder="Overridable via msg.attachmentId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-newCollectiveId">New Collective ID</label>
|
||||||
|
<input type="text" id="node-input-newCollectiveId" placeholder="Overridable via msg.newCollectiveId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-settingKey">Settings Key</label>
|
||||||
|
<input type="text" id="node-input-settingKey" placeholder="Overridable via msg.settingKey">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="margin:10px 0; border:none; border-top:1px solid #ccc;">
|
||||||
|
<div style="font-weight:bold; margin-bottom:6px;">Request Body Fields (shown based on operation)</div>
|
||||||
|
|
||||||
|
<div class="form-row" id="body-name-row">
|
||||||
|
<label for="node-input-bodyName">Name</label>
|
||||||
|
<input type="text" id="node-input-bodyName" placeholder="Overridable via msg.name">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-emoji-row">
|
||||||
|
<label for="node-input-bodyEmoji">Emoji</label>
|
||||||
|
<input type="text" id="node-input-bodyEmoji" placeholder="Overridable via msg.emoji">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-color-row">
|
||||||
|
<label for="node-input-bodyColor">Color</label>
|
||||||
|
<input type="text" id="node-input-bodyColor" placeholder="Overridable via msg.color">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-level-row">
|
||||||
|
<label for="node-input-bodyLevel">Level</label>
|
||||||
|
<input type="number" id="node-input-bodyLevel" placeholder="Overridable via msg.level">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-mode-row">
|
||||||
|
<label for="node-input-bodyMode">Mode</label>
|
||||||
|
<input type="number" id="node-input-bodyMode" placeholder="Overridable via msg.mode">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-title-row">
|
||||||
|
<label for="node-input-bodyTitle">Title</label>
|
||||||
|
<input type="text" id="node-input-bodyTitle" placeholder="Overridable via msg.title">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-editable-row">
|
||||||
|
<label for="node-input-bodyEditable">Editable (true/false)</label>
|
||||||
|
<input type="text" id="node-input-bodyEditable" placeholder="Overridable via msg.editable">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-password-row">
|
||||||
|
<label for="node-input-bodyPassword">Password</label>
|
||||||
|
<input type="text" id="node-input-bodyPassword" placeholder="Overridable via msg.password">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-fullWidth-row">
|
||||||
|
<label for="node-input-bodyFullWidth">Full Width (true/false)</label>
|
||||||
|
<input type="text" id="node-input-bodyFullWidth" placeholder="Overridable via msg.fullWidth">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-templateId-row">
|
||||||
|
<label for="node-input-bodyTemplateId">Template ID</label>
|
||||||
|
<input type="number" id="node-input-bodyTemplateId" placeholder="Overridable via msg.templateId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-pageOrder-row">
|
||||||
|
<label for="node-input-bodyPageOrder">Page Order</label>
|
||||||
|
<input type="number" id="node-input-bodyPageOrder" placeholder="Overridable via msg.pageOrder">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-showMembers-row">
|
||||||
|
<label for="node-input-bodyShowMembers">Show Members (true/false)</label>
|
||||||
|
<input type="text" id="node-input-bodyShowMembers" placeholder="Overridable via msg.showMembers">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-showRecentPages-row">
|
||||||
|
<label for="node-input-bodyShowRecentPages">Show Recent Pages (true/false)</label>
|
||||||
|
<input type="text" id="node-input-bodyShowRecentPages" placeholder="Overridable via msg.showRecentPages">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-favoritePages-row">
|
||||||
|
<label for="node-input-bodyFavoritePages">Favorite Pages (JSON array)</label>
|
||||||
|
<input type="text" id="node-input-bodyFavoritePages" placeholder="Overridable via msg.favoritePages">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-index-row">
|
||||||
|
<label for="node-input-bodyIndex">Index</label>
|
||||||
|
<input type="number" id="node-input-bodyIndex" placeholder="Overridable via msg.index">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-copy-row">
|
||||||
|
<label for="node-input-bodyCopy">Copy (true/false)</label>
|
||||||
|
<input type="text" id="node-input-bodyCopy" placeholder="Overridable via msg.copy">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-subpageOrder-row">
|
||||||
|
<label for="node-input-bodySubpageOrder">Subpage Order (JSON array)</label>
|
||||||
|
<input type="text" id="node-input-bodySubpageOrder" placeholder="Overridable via msg.subpageOrder">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-parentId-row">
|
||||||
|
<label for="node-input-bodyParentId">Target Parent ID</label>
|
||||||
|
<input type="number" id="node-input-bodyParentId" placeholder="Overridable via msg.parentId">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-circle-row">
|
||||||
|
<label for="node-input-bodyCircle">Delete Circle (true/false)</label>
|
||||||
|
<input type="text" id="node-input-bodyCircle" placeholder="Overridable via msg.circle">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-searchString-row">
|
||||||
|
<label for="node-input-bodySearchString">Search String</label>
|
||||||
|
<input type="text" id="node-input-bodySearchString" placeholder="Overridable via msg.searchString">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-query-row">
|
||||||
|
<label for="node-input-bodyQuery">Query</label>
|
||||||
|
<input type="text" id="node-input-bodyQuery" placeholder="Overridable via msg.query">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-limit-row">
|
||||||
|
<label for="node-input-bodyLimit">Limit</label>
|
||||||
|
<input type="number" id="node-input-bodyLimit" placeholder="Overridable via msg.limit">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-key-row">
|
||||||
|
<label for="node-input-bodyKey">Key</label>
|
||||||
|
<input type="text" id="node-input-bodyKey" placeholder="Overridable via msg.key">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-value-row">
|
||||||
|
<label for="node-input-bodyValue">Value</label>
|
||||||
|
<input type="text" id="node-input-bodyValue" placeholder="Overridable via msg.value">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="body-sessionToken-row">
|
||||||
|
<label for="node-input-bodySessionToken">Session Token</label>
|
||||||
|
<input type="text" id="node-input-bodySessionToken" placeholder="Overridable via msg.token">
|
||||||
|
</div>
|
||||||
|
<div class="form-tips">
|
||||||
|
<p>All path params and body fields can be overridden via <code>msg</code> properties at runtime.</p>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
// Operation definitions: operationId -> { method, pathTemplate, queryParams }
|
||||||
|
var OPERATIONS = {
|
||||||
|
// --- collective ---
|
||||||
|
'collective:list': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives' },
|
||||||
|
'collective:create': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives', body: ['name','emoji'] },
|
||||||
|
'collective:update': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{id}', body: ['emoji'] },
|
||||||
|
'collective:trash': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{id}' },
|
||||||
|
'collective:editLevel': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{id}/editLevel', body: ['level'] },
|
||||||
|
'collective:shareLevel': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{id}/shareLevel', body: ['level'] },
|
||||||
|
'collective:pageMode': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{id}/pageMode', body: ['mode'] },
|
||||||
|
// --- trash ---
|
||||||
|
'trash:list': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/trash' },
|
||||||
|
'trash:restore': { method: 'PATCH', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/trash/{id}' },
|
||||||
|
'trash:delete': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/trash/{id}', query: ['circle'] },
|
||||||
|
// --- share ---
|
||||||
|
'share:listCollectiveShares': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/shares' },
|
||||||
|
'share:createCollectiveShare': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/shares', body: ['password'] },
|
||||||
|
'share:updateCollectiveShare': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/shares/{token}', body: ['editable','password'] },
|
||||||
|
'share:deleteCollectiveShare': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/shares/{token}' },
|
||||||
|
'share:createPageShare': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{pageId}/shares', body: ['password'] },
|
||||||
|
'share:updatePageShare': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{pageId}/shares/{token}', body: ['editable','password'] },
|
||||||
|
'share:deletePageShare': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{pageId}/shares/{token}' },
|
||||||
|
// --- search ---
|
||||||
|
'search:recentPages': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/search/recent', query: ['query','limit'] },
|
||||||
|
'search:content': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/search', query: ['searchString'] },
|
||||||
|
// --- page ---
|
||||||
|
'page:list': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages' },
|
||||||
|
'page:get': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}' },
|
||||||
|
'page:create': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{parentId}', body: ['title','templateId'] },
|
||||||
|
'page:moveOrCopy': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}', body: ['parentId','title','index','copy'] },
|
||||||
|
'page:trash': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}' },
|
||||||
|
'page:touch': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/touch' },
|
||||||
|
'page:fullWidth': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/fullWidth', body: ['fullWidth'] },
|
||||||
|
'page:emoji': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/emoji', body: ['emoji'] },
|
||||||
|
'page:subpageOrder': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/subpageOrder', body: ['subpageOrder'] },
|
||||||
|
'page:moveToCollective': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/to/{newCollectiveId}', body: ['parentId','index','copy'] },
|
||||||
|
'page:addTag': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/tags/{tagId}' },
|
||||||
|
'page:removeTag': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/tags/{tagId}' },
|
||||||
|
'page:listAttachments': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/attachments' },
|
||||||
|
'page:uploadAttachment': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/attachments' },
|
||||||
|
'page:renameAttachment': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/attachments/{attachmentId}', body: ['name'] },
|
||||||
|
'page:deleteAttachment': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/attachments/{attachmentId}' },
|
||||||
|
'page:restoreAttachment': { method: 'PATCH', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/{id}/attachments/trash/{attachmentId}' },
|
||||||
|
// --- page_trash ---
|
||||||
|
'page_trash:list': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/trash' },
|
||||||
|
'page_trash:restore': { method: 'PATCH', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/trash/{id}' },
|
||||||
|
'page_trash:delete': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/trash/{id}' },
|
||||||
|
// --- template ---
|
||||||
|
'template:list': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/templates' },
|
||||||
|
'template:create': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/templates/{id}', body: ['title','parentId'] },
|
||||||
|
'template:rename': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/templates/{id}', body: ['title'] },
|
||||||
|
'template:delete': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/templates/{id}' },
|
||||||
|
'template:emoji': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/pages/templates/{id}/emoji', body: ['emoji'] },
|
||||||
|
// --- tag ---
|
||||||
|
'tag:list': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/tags' },
|
||||||
|
'tag:create': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/tags', body: ['name','color'] },
|
||||||
|
'tag:update': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/tags/{id}', body: ['name','color'] },
|
||||||
|
'tag:delete': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/tags/{id}' },
|
||||||
|
// --- collective_user_settings ---
|
||||||
|
'userSettings:pageOrder': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/userSettings/pageOrder', body: ['pageOrder'] },
|
||||||
|
'userSettings:showMembers': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/userSettings/showMembers', body: ['showMembers'] },
|
||||||
|
'userSettings:showRecentPages':{ method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/userSettings/showRecentPages', body: ['showRecentPages'] },
|
||||||
|
'userSettings:favoritePages': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/userSettings/favoritePages', body: ['favoritePages'] },
|
||||||
|
// --- session ---
|
||||||
|
'session:create': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/sessions' },
|
||||||
|
'session:sync': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/sessions', body: ['token'] },
|
||||||
|
'session:close': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/collectives/{collectiveId}/sessions', query: ['token'] },
|
||||||
|
// --- settings ---
|
||||||
|
'settings:get': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/settings/user/{key}' },
|
||||||
|
'settings:set': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/settings/user', body: ['key','value'] },
|
||||||
|
// --- public_collective ---
|
||||||
|
'public:getCollective': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}' },
|
||||||
|
// --- public_page ---
|
||||||
|
'public:listPages': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages' },
|
||||||
|
'public:getPage': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}' },
|
||||||
|
'public:createPage': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{parentId}', body: ['title','templateId'] },
|
||||||
|
'public:moveOrCopyPage': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}', body: ['parentId','title','index','copy'] },
|
||||||
|
'public:trashPage': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}' },
|
||||||
|
'public:touchPage': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/touch' },
|
||||||
|
'public:fullWidth': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/fullWidth', body: ['fullWidth'] },
|
||||||
|
'public:emoji': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/emoji', body: ['emoji'] },
|
||||||
|
'public:subpageOrder': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/subpageOrder', body: ['subpageOrder'] },
|
||||||
|
'public:addTag': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/tags/{tagId}' },
|
||||||
|
'public:removeTag': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/tags/{tagId}' },
|
||||||
|
'public:listAttachments': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/attachments' },
|
||||||
|
'public:uploadAttachment': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/attachments' },
|
||||||
|
'public:renameAttachment': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/attachments/{attachmentId}', body: ['name'] },
|
||||||
|
'public:deleteAttachment': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/attachments/{attachmentId}' },
|
||||||
|
'public:restoreAttachment': { method: 'PATCH', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/{id}/attachments/trash/{attachmentId}' },
|
||||||
|
'public:searchContent': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/search', query: ['searchString'] },
|
||||||
|
// --- public_page_trash ---
|
||||||
|
'public:listTrash': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/trash' },
|
||||||
|
'public:restoreTrash': { method: 'PATCH', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/trash/{id}' },
|
||||||
|
'public:deleteTrash': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/trash/{id}' },
|
||||||
|
// --- public_template ---
|
||||||
|
'public:listTemplates': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/pages/templates' },
|
||||||
|
// --- public_tag ---
|
||||||
|
'public:listTags': { method: 'GET', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/tags' },
|
||||||
|
'public:createTag': { method: 'POST', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/tags', body: ['name','color'] },
|
||||||
|
'public:updateTag': { method: 'PUT', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/tags/{id}', body: ['name','color'] },
|
||||||
|
'public:deleteTag': { method: 'DELETE', path: '/ocs/v2.php/apps/collectives/api/v1.0/p/collectives/{token}/tags/{id}' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function CollectivesNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'collective:list';
|
||||||
|
|
||||||
|
// Path parameter fields from config
|
||||||
|
this.collectiveId = config.collectiveId;
|
||||||
|
this.pageId = config.pageId;
|
||||||
|
this.token = config.token;
|
||||||
|
this.parentId = config.parentId;
|
||||||
|
this.tagId = config.tagId;
|
||||||
|
this.attachmentId = config.attachmentId;
|
||||||
|
this.newCollectiveId = config.newCollectiveId;
|
||||||
|
this.itemId = config.itemId; // generic {id}
|
||||||
|
this.settingKey = config.settingKey;
|
||||||
|
|
||||||
|
// Body fields
|
||||||
|
this.bodyName = config.bodyName;
|
||||||
|
this.bodyEmoji = config.bodyEmoji;
|
||||||
|
this.bodyColor = config.bodyColor;
|
||||||
|
this.bodyLevel = config.bodyLevel;
|
||||||
|
this.bodyMode = config.bodyMode;
|
||||||
|
this.bodyTitle = config.bodyTitle;
|
||||||
|
this.bodyEditable = config.bodyEditable;
|
||||||
|
this.bodyPassword = config.bodyPassword;
|
||||||
|
this.bodyFullWidth = config.bodyFullWidth;
|
||||||
|
this.bodyTemplateId = config.bodyTemplateId;
|
||||||
|
this.bodyPageOrder = config.bodyPageOrder;
|
||||||
|
this.bodyShowMembers = config.bodyShowMembers;
|
||||||
|
this.bodyShowRecentPages = config.bodyShowRecentPages;
|
||||||
|
this.bodyFavoritePages = config.bodyFavoritePages;
|
||||||
|
this.bodyIndex = config.bodyIndex;
|
||||||
|
this.bodyCopy = config.bodyCopy;
|
||||||
|
this.bodySubpageOrder = config.bodySubpageOrder;
|
||||||
|
this.bodyParentId = config.bodyParentId;
|
||||||
|
this.bodyCircle = config.bodyCircle;
|
||||||
|
this.bodySearchString = config.bodySearchString;
|
||||||
|
this.bodyQuery = config.bodyQuery;
|
||||||
|
this.bodyLimit = config.bodyLimit;
|
||||||
|
this.bodyKey = config.bodyKey;
|
||||||
|
this.bodyValue = config.bodyValue;
|
||||||
|
this.bodySessionToken = config.bodySessionToken;
|
||||||
|
|
||||||
|
if (!this.configNode) {
|
||||||
|
node.error("No Nextcloud configuration node selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) {
|
||||||
|
node.error("Unknown operation: " + (msg.operation || node.operation), msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
// Build path with substituted parameters (msg overrides config)
|
||||||
|
var path = op.path;
|
||||||
|
var subs = {
|
||||||
|
'{id}': msg.itemId || node.itemId || msg.pageId || node.pageId || '',
|
||||||
|
'{collectiveId}': msg.collectiveId || node.collectiveId || '',
|
||||||
|
'{pageId}': msg.pageId || node.pageId || '',
|
||||||
|
'{parentId}': msg.parentId || node.parentId || '',
|
||||||
|
'{token}': msg.token || node.token || '',
|
||||||
|
'{tagId}': msg.tagId || node.tagId || '',
|
||||||
|
'{attachmentId}': msg.attachmentId || node.attachmentId || '',
|
||||||
|
'{newCollectiveId}': msg.newCollectiveId || node.newCollectiveId || '',
|
||||||
|
'{key}': msg.settingKey || node.settingKey || ''
|
||||||
|
};
|
||||||
|
Object.keys(subs).forEach(function(k) {
|
||||||
|
path = path.replace(k, subs[k]);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Query parameters
|
||||||
|
var queryParts = [];
|
||||||
|
var queryDefs = msg.query || {};
|
||||||
|
if (op.query) {
|
||||||
|
op.query.forEach(function(q) {
|
||||||
|
var val = msg[q] !== undefined ? msg[q] : getBodyField(node, q);
|
||||||
|
if (val !== undefined && val !== '') {
|
||||||
|
queryParts.push(encodeURIComponent(q) + '=' + encodeURIComponent(val));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Also handle query overrides from msg
|
||||||
|
if (typeof msg.query === 'object') {
|
||||||
|
Object.keys(msg.query).forEach(function(k) {
|
||||||
|
if (op.query && op.query.indexOf(k) === -1) {
|
||||||
|
queryParts.push(encodeURIComponent(k) + '=' + encodeURIComponent(msg.query[k]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var fullUrl = baseUrl + path;
|
||||||
|
if (queryParts.length) {
|
||||||
|
fullUrl += '?' + queryParts.join('&');
|
||||||
|
}
|
||||||
|
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = {
|
||||||
|
'OCS-APIRequest': 'true',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build request body
|
||||||
|
var bodyObj = msg.body || {};
|
||||||
|
if (op.body) {
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var val = msg[b] !== undefined ? msg[b] : getBodyField(node, b);
|
||||||
|
if (val !== undefined && val !== '') {
|
||||||
|
bodyObj[b] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (Object.keys(bodyObj).length > 0) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
bodyStr = JSON.stringify(bodyObj);
|
||||||
|
}
|
||||||
|
// Allow raw body override
|
||||||
|
if (msg.rawBody) {
|
||||||
|
bodyStr = typeof msg.rawBody === 'string' ? msg.rawBody : JSON.stringify(msg.rawBody);
|
||||||
|
if (!headers['Content-Type']) headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.headers && typeof msg.headers === 'object') {
|
||||||
|
Object.keys(msg.headers).forEach(function(k) {
|
||||||
|
headers[k] = msg.headers[k];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
hostname: parsedUrl.hostname,
|
||||||
|
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsedUrl.path,
|
||||||
|
method: op.method,
|
||||||
|
headers: headers,
|
||||||
|
rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var transport = parsedUrl.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
node.log(op.method + ' ' + fullUrl);
|
||||||
|
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body = '';
|
||||||
|
res.on('data', function(chunk) { body += chunk; });
|
||||||
|
res.on('end', function() {
|
||||||
|
msg.statusCode = res.statusCode;
|
||||||
|
msg.operation = node.operation;
|
||||||
|
try {
|
||||||
|
msg.payload = JSON.parse(body);
|
||||||
|
} catch(e) {
|
||||||
|
msg.payload = body;
|
||||||
|
}
|
||||||
|
node.send(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', function(err) {
|
||||||
|
msg.error = err.message;
|
||||||
|
node.error(op.method + ' ' + fullUrl + ' — ' + err.message, msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bodyStr) {
|
||||||
|
req.write(bodyStr);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBodyField(node, name) {
|
||||||
|
var map = {
|
||||||
|
'name': node.bodyName, 'emoji': node.bodyEmoji, 'color': node.bodyColor,
|
||||||
|
'level': node.bodyLevel, 'mode': node.bodyMode, 'title': node.bodyTitle,
|
||||||
|
'editable': node.bodyEditable, 'password': node.bodyPassword,
|
||||||
|
'fullWidth': node.bodyFullWidth, 'templateId': node.bodyTemplateId,
|
||||||
|
'pageOrder': node.bodyPageOrder, 'showMembers': node.bodyShowMembers,
|
||||||
|
'showRecentPages': node.bodyShowRecentPages, 'favoritePages': node.bodyFavoritePages,
|
||||||
|
'index': node.bodyIndex, 'copy': node.bodyCopy, 'subpageOrder': node.bodySubpageOrder,
|
||||||
|
'parentId': node.bodyParentId, 'circle': node.bodyCircle,
|
||||||
|
'searchString': node.bodySearchString, 'query': node.bodyQuery, 'limit': node.bodyLimit,
|
||||||
|
'key': node.bodyKey, 'value': node.bodyValue, 'token': node.bodySessionToken
|
||||||
|
};
|
||||||
|
return map[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType("collectives", CollectivesNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('core',{category:'nextcloud',color:'#0066CC',
|
||||||
|
defaults:{name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"status:get"},
|
||||||
|
userId:{value:""},collectionId:{value:""},resourceType:{value:""},resourceId:{value:""},
|
||||||
|
providerId:{value:""},appId:{value:""},taskId:{value:""},teamId:{value:""},guestName:{value:""},size:{value:"64"},
|
||||||
|
filter:{value:""},baseResourceType:{value:""},baseResourceId:{value:""},
|
||||||
|
bodyPassword:{value:""},bodyText:{value:""},bodySearch:{value:""}},
|
||||||
|
inputs:1,outputs:1,icon:"core.svg",label:function(){return this.name||"Core";}});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="core">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Core"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Status & Capabilities"><option value="status:get">Server Status</option><option value="capabilities:get">Capabilities</option></optgroup>
|
||||||
|
<optgroup label="App Passwords"><option value="appPassword:get">Get App Password</option><option value="appPassword:oneTime">One-Time App Password</option><option value="appPassword:delete">Delete App Password</option><option value="appPassword:rotate">Rotate App Password</option><option value="appPassword:confirm">Confirm Password</option></optgroup>
|
||||||
|
<optgroup label="Navigation"><option value="nav:apps">Apps Navigation</option><option value="nav:settings">Settings Navigation</option></optgroup>
|
||||||
|
<optgroup label="Profile"><option value="profile:get">Get Profile</option><option value="profile:setVisibility">Set Visibility</option></optgroup>
|
||||||
|
<optgroup label="Search"><option value="autocomplete:get">Autocomplete</option><option value="search:providers">Search Providers</option><option value="search:execute">Search</option></optgroup>
|
||||||
|
<optgroup label="Hovercard"><option value="hovercard:get">Get Hovercard</option></optgroup>
|
||||||
|
<optgroup label="Collaboration"><option value="collab:get">Get Collection</option><option value="collab:addResource">Add Resource</option><option value="collab:removeResource">Remove Resource</option><option value="collab:rename">Rename Collection</option><option value="collab:search">Search Collections</option><option value="collab:byResource">Collections By Resource</option><option value="collab:create">Create Collection</option></optgroup>
|
||||||
|
<optgroup label="References"><option value="ref:extract">Extract Refs</option><option value="ref:resolve">Resolve Ref</option><option value="ref:resolveMany">Resolve Many</option><option value="ref:providers">Ref Providers</option><option value="ref:touchProvider">Touch Provider</option></optgroup>
|
||||||
|
<optgroup label="Previews & Avatars"><option value="preview:byPath">Preview By Path</option><option value="preview:byFileId">Preview By File ID</option><option value="avatar:user">User Avatar</option><option value="avatar:userDark">User Avatar Dark</option><option value="avatar:guest">Guest Avatar</option></optgroup>
|
||||||
|
<optgroup label="Auth"><option value="csrf:get">CSRF Token</option><option value="login:init">Login Flow Init</option><option value="login:poll">Login Flow Poll</option><option value="login:confirm">Confirm Password</option></optgroup>
|
||||||
|
<optgroup label="Device Wipe"><option value="wipe:check">Check Wipe</option><option value="wipe:done">Wipe Done</option></optgroup>
|
||||||
|
<optgroup label="OCM"><option value="ocm:discovery">OCM Discovery</option></optgroup>
|
||||||
|
<optgroup label="AI / Task Processing"><option value="task:types">Task Types</option><option value="task:schedule">Schedule Task</option><option value="task:get">Get Task</option><option value="task:delete">Delete Task</option><option value="task:list">List Tasks</option><option value="task:listByApp">Tasks By App</option><option value="task:cancel">Cancel Task</option></optgroup>
|
||||||
|
<optgroup label="AI / Text"><option value="text:types">Text Task Types</option><option value="text:schedule">Schedule Text Task</option><option value="text:get">Get Text Task</option><option value="text:delete">Delete Text Task</option><option value="text:listByApp">Text Tasks By App</option></optgroup>
|
||||||
|
<optgroup label="AI / Translation"><option value="translate:languages">Languages</option><option value="translate:text">Translate</option></optgroup>
|
||||||
|
<optgroup label="AI / Text-to-Image"><option value="t2i:available">Check Available</option><option value="t2i:schedule">Schedule Image</option><option value="t2i:get">Get Image Task</option><option value="t2i:delete">Delete Image Task</option><option value="t2i:image">Get Image</option><option value="t2i:listByApp">Image Tasks By App</option></optgroup>
|
||||||
|
<optgroup label="Teams"><option value="teams:resources">Team Resources</option><option value="teams:ofResource">Teams Of Resource</option></optgroup>
|
||||||
|
</select></div>
|
||||||
|
<hr><div class="form-tips"><p>60+ operations covering Server, Auth, Search, AI, Teams. Pass path params via <code>msg.*</code>. For complex input (task input, references array) use msg.input, msg.references etc.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
// Status
|
||||||
|
'status:get': { method:'GET', path:'/status.php' },
|
||||||
|
// Capabilities
|
||||||
|
'capabilities:get': { method:'GET', path:'/ocs/v2.php/cloud/capabilities' },
|
||||||
|
// App password
|
||||||
|
'appPassword:get': { method:'GET', path:'/ocs/v2.php/core/getapppassword' },
|
||||||
|
'appPassword:oneTime': { method:'GET', path:'/ocs/v2.php/core/getapppassword-onetime' },
|
||||||
|
'appPassword:delete': { method:'DELETE', path:'/ocs/v2.php/core/apppassword' },
|
||||||
|
'appPassword:rotate': { method:'POST', path:'/ocs/v2.php/core/apppassword/rotate' },
|
||||||
|
'appPassword:confirm': { method:'PUT', path:'/ocs/v2.php/core/apppassword/confirm', body:['password'] },
|
||||||
|
// Autocomplete
|
||||||
|
'autocomplete:get': { method:'GET', path:'/ocs/v2.php/core/autocomplete/get', query:['search','itemType','itemId','sorter','limit'] },
|
||||||
|
// Navigation
|
||||||
|
'nav:apps': { method:'GET', path:'/ocs/v2.php/core/navigation/apps', query:['absolute'] },
|
||||||
|
'nav:settings': { method:'GET', path:'/ocs/v2.php/core/navigation/settings', query:['absolute'] },
|
||||||
|
// Profile
|
||||||
|
'profile:get': { method:'GET', path:'/ocs/v2.php/profile/{userId}' },
|
||||||
|
'profile:setVisibility': { method:'PUT', path:'/ocs/v2.php/profile/{userId}', body:['paramId','visibility'] },
|
||||||
|
// Hover card
|
||||||
|
'hovercard:get': { method:'GET', path:'/ocs/v2.php/hovercard/v1/{userId}' },
|
||||||
|
// Collaboration resources
|
||||||
|
'collab:get': { method:'GET', path:'/ocs/v2.php/collaboration/resources/collections/{collectionId}' },
|
||||||
|
'collab:addResource': { method:'POST', path:'/ocs/v2.php/collaboration/resources/collections/{collectionId}', body:['resourceType','resourceId'] },
|
||||||
|
'collab:removeResource': { method:'DELETE', path:'/ocs/v2.php/collaboration/resources/collections/{collectionId}', query:['resourceType','resourceId'] },
|
||||||
|
'collab:rename': { method:'PUT', path:'/ocs/v2.php/collaboration/resources/collections/{collectionId}', body:['collectionName'] },
|
||||||
|
'collab:search': { method:'GET', path:'/ocs/v2.php/collaboration/resources/collections/search/{filter}' },
|
||||||
|
'collab:byResource': { method:'GET', path:'/ocs/v2.php/collaboration/resources/{resourceType}/{resourceId}' },
|
||||||
|
'collab:create': { method:'POST', path:'/ocs/v2.php/collaboration/resources/{baseResourceType}/{baseResourceId}', body:['name'] },
|
||||||
|
// References
|
||||||
|
'ref:extract': { method:'POST', path:'/ocs/v2.php/references/extract', body:['text','resolve','limit'] },
|
||||||
|
'ref:resolve': { method:'GET', path:'/ocs/v2.php/references/resolve', query:['reference'] },
|
||||||
|
'ref:resolveMany': { method:'POST', path:'/ocs/v2.php/references/resolve', body:['references','limit'] },
|
||||||
|
'ref:providers': { method:'GET', path:'/ocs/v2.php/references/providers' },
|
||||||
|
'ref:touchProvider': { method:'PUT', path:'/ocs/v2.php/references/provider/{providerId}', body:['timestamp'] },
|
||||||
|
// Preview
|
||||||
|
'preview:byPath': { method:'GET', path:'/index.php/core/preview.png', query:['file','x','y','a','forceIcon','mode'] },
|
||||||
|
'preview:byFileId': { method:'GET', path:'/index.php/core/preview', query:['fileId','x','y','a','forceIcon','mode'] },
|
||||||
|
// Avatars
|
||||||
|
'avatar:user': { method:'GET', path:'/index.php/avatar/{userId}/{size}' },
|
||||||
|
'avatar:userDark': { method:'GET', path:'/index.php/avatar/{userId}/{size}/dark' },
|
||||||
|
'avatar:guest': { method:'GET', path:'/index.php/avatar/guest/{guestName}/{size}' },
|
||||||
|
// CSRF
|
||||||
|
'csrf:get': { method:'GET', path:'/index.php/csrftoken' },
|
||||||
|
// Login flow
|
||||||
|
'login:init': { method:'POST', path:'/index.php/login/v2' },
|
||||||
|
'login:poll': { method:'POST', path:'/index.php/login/v2/poll', body:['token'] },
|
||||||
|
'login:confirm': { method:'POST', path:'/index.php/login/confirm', body:['password'] },
|
||||||
|
// Wipe
|
||||||
|
'wipe:check': { method:'POST', path:'/index.php/core/wipe/check', body:['token'] },
|
||||||
|
'wipe:done': { method:'POST', path:'/index.php/core/wipe/success', body:['token'] },
|
||||||
|
// OCM
|
||||||
|
'ocm:discovery': { method:'GET', path:'/index.php/ocm-provider' },
|
||||||
|
// Unified search
|
||||||
|
'search:providers': { method:'GET', path:'/ocs/v2.php/search/providers', query:['from'] },
|
||||||
|
'search:execute': { method:'GET', path:'/ocs/v2.php/search/providers/{providerId}/search', query:['term','sortOrder','limit','cursor','from'] },
|
||||||
|
// Task processing
|
||||||
|
'task:types': { method:'GET', path:'/ocs/v2.php/taskprocessing/tasktypes' },
|
||||||
|
'task:schedule': { method:'POST', path:'/ocs/v2.php/taskprocessing/schedule', body:['input','type','appId','customId'] },
|
||||||
|
'task:get': { method:'GET', path:'/ocs/v2.php/taskprocessing/task/{id}' },
|
||||||
|
'task:delete': { method:'DELETE', path:'/ocs/v2.php/taskprocessing/task/{id}' },
|
||||||
|
'task:list': { method:'GET', path:'/ocs/v2.php/taskprocessing/tasks', query:['taskType','customId'] },
|
||||||
|
'task:listByApp': { method:'GET', path:'/ocs/v2.php/taskprocessing/tasks/app/{appId}', query:['customId'] },
|
||||||
|
'task:cancel': { method:'POST', path:'/ocs/v2.php/taskprocessing/tasks/{taskId}/cancel' },
|
||||||
|
// Text processing
|
||||||
|
'text:types': { method:'GET', path:'/ocs/v2.php/textprocessing/tasktypes' },
|
||||||
|
'text:schedule': { method:'POST', path:'/ocs/v2.php/textprocessing/schedule', body:['input','type','appId','identifier'] },
|
||||||
|
'text:get': { method:'GET', path:'/ocs/v2.php/textprocessing/task/{id}' },
|
||||||
|
'text:delete': { method:'DELETE', path:'/ocs/v2.php/textprocessing/task/{id}' },
|
||||||
|
'text:listByApp': { method:'GET', path:'/ocs/v2.php/textprocessing/tasks/app/{appId}', query:['identifier'] },
|
||||||
|
// Translation
|
||||||
|
'translate:languages': { method:'GET', path:'/ocs/v2.php/translation/languages' },
|
||||||
|
'translate:text': { method:'POST', path:'/ocs/v2.php/translation/translate', body:['text','fromLanguage','toLanguage'] },
|
||||||
|
// Text to image
|
||||||
|
't2i:available': { method:'GET', path:'/ocs/v2.php/text2image/is_available' },
|
||||||
|
't2i:schedule': { method:'POST', path:'/ocs/v2.php/text2image/schedule', body:['input','appId','identifier','numberOfImages'] },
|
||||||
|
't2i:get': { method:'GET', path:'/ocs/v2.php/text2image/task/{id}' },
|
||||||
|
't2i:delete': { method:'DELETE', path:'/ocs/v2.php/text2image/task/{id}' },
|
||||||
|
't2i:image': { method:'GET', path:'/ocs/v2.php/text2image/task/{id}/image/{index}' },
|
||||||
|
't2i:listByApp': { method:'GET', path:'/ocs/v2.php/text2image/tasks/app/{appId}', query:['identifier'] },
|
||||||
|
// Teams
|
||||||
|
'teams:resources': { method:'GET', path:'/ocs/v2.php/teams/{teamId}/resources' },
|
||||||
|
'teams:ofResource': { method:'GET', path:'/ocs/v2.php/teams/resources/{providerId}/{resourceId}' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function CoreNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'status:get';
|
||||||
|
this.userId = config.userId; this.collectionId = config.collectionId;
|
||||||
|
this.resourceType = config.resourceType; this.resourceId = config.resourceId;
|
||||||
|
this.providerId = config.providerId; this.appId = config.appId;
|
||||||
|
this.taskId = config.taskId; this.teamId = config.teamId;
|
||||||
|
this.guestName = config.guestName; this.size = config.size;
|
||||||
|
this.filter = config.filter; this.baseResourceType = config.baseResourceType;
|
||||||
|
this.baseResourceId = config.baseResourceId;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No NC config node"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown: "+(msg.operation||node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl||'').replace(/\/+$/,'');
|
||||||
|
var uname = node.configNode.credentials.username;
|
||||||
|
var pw = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var path = op.path
|
||||||
|
.replace(/\{userId\}/g, msg.userId||node.userId||'')
|
||||||
|
.replace(/\{collectionId\}/g, msg.collectionId||node.collectionId||'')
|
||||||
|
.replace(/\{resourceType\}/g, msg.resourceType||node.resourceType||'')
|
||||||
|
.replace(/\{resourceId\}/g, msg.resourceId||node.resourceId||'')
|
||||||
|
.replace(/\{providerId\}/g, msg.providerId||node.providerId||'')
|
||||||
|
.replace(/\{appId\}/g, msg.appId||node.appId||'')
|
||||||
|
.replace(/\{id\}/g, msg.taskId||node.taskId||msg.id||'')
|
||||||
|
.replace(/\{taskId\}/g, msg.taskId||node.taskId||'')
|
||||||
|
.replace(/\{teamId\}/g, msg.teamId||node.teamId||'')
|
||||||
|
.replace(/\{guestName\}/g, msg.guestName||node.guestName||'')
|
||||||
|
.replace(/\{size\}/g, msg.size||node.size||'64')
|
||||||
|
.replace(/\{filter\}/g, msg.filter||node.filter||'')
|
||||||
|
.replace(/\{baseResourceType\}/g, msg.baseResourceType||node.baseResourceType||'')
|
||||||
|
.replace(/\{baseResourceId\}/g, msg.baseResourceId||node.baseResourceId||'')
|
||||||
|
.replace(/\{index\}/g, msg.index||'0');
|
||||||
|
|
||||||
|
var qp = [];
|
||||||
|
if (op.query) op.query.forEach(function(q) {
|
||||||
|
var v = msg[q]!==undefined ? msg[q] : getF(node,q);
|
||||||
|
if (v!==undefined&&v!=='') qp.push(encodeURIComponent(q)+'='+encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
var fu = baseUrl+path+(qp.length?'?'+qp.join('&'):'');
|
||||||
|
var pu = url.parse(fu);
|
||||||
|
|
||||||
|
var h = {'Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(uname+':'+pw).toString('base64')};
|
||||||
|
if (path.indexOf('/ocs/')===0) h['OCS-APIRequest']='true';
|
||||||
|
|
||||||
|
var bo={};
|
||||||
|
if (op.body) op.body.forEach(function(b){
|
||||||
|
var v=msg[b]!==undefined?msg[b]:getF(node,b);
|
||||||
|
if(v!==undefined&&v!=='') bo[b]=v;
|
||||||
|
});
|
||||||
|
var bs=null;
|
||||||
|
if (Object.keys(bo).length>0) {h['Content-Type']='application/json'; bs=JSON.stringify(bo);}
|
||||||
|
|
||||||
|
var o = {hostname:pu.hostname,port:pu.port||(pu.protocol==='https:'?443:80),
|
||||||
|
path:pu.path,method:op.method,headers:h,rejectUnauthorized:false};
|
||||||
|
node.log(op.method+' '+fu);
|
||||||
|
var t = pu.protocol==='https:'?https:http;
|
||||||
|
var r = t.request(o,function(res){
|
||||||
|
var b='';res.on('data',function(c){b+=c;});
|
||||||
|
res.on('end',function(){msg.statusCode=res.statusCode;msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(b);}catch(e){msg.payload=b;}node.send(msg);});
|
||||||
|
});
|
||||||
|
r.on('error',function(e){msg.error=e.message;node.error(op.method+' '+fu+' - '+e.message,msg);});
|
||||||
|
if(bs) r.write(bs);
|
||||||
|
r.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getF(node,n){var m={'password':node.bodyPassword,'text':node.bodyText,'search':node.bodySearch};return m[n];}
|
||||||
|
RED.nodes.registerType("core", CoreNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('dashboard', {
|
||||||
|
category: 'nextcloud', color: '#FF6600',
|
||||||
|
defaults: {
|
||||||
|
name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"widgets:list"},
|
||||||
|
bodySinceIds:{value:""},bodyLimit:{value:""},bodyLayout:{value:""},bodyStatuses:{value:""}
|
||||||
|
},
|
||||||
|
inputs:1,outputs:1,icon:"dashboard.svg",
|
||||||
|
label:function(){return this.name||"Dashboard";}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="dashboard">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Dashboard"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<option value="widgets:list">List Widgets</option><option value="widgetItems:v1">Get Widget Items v1</option>
|
||||||
|
<option value="widgetItems:v2">Get Widget Items v2</option><option value="layout:get">Get Layout</option>
|
||||||
|
<option value="layout:update">Update Layout</option><option value="statuses:get">Get Statuses</option>
|
||||||
|
<option value="statuses:update">Update Statuses</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Parameters</div>
|
||||||
|
<div class="form-row"><label>Since IDs</label><input type="text" id="node-input-bodySinceIds" placeholder="msg.sinceIds"></div>
|
||||||
|
<div class="form-row"><label>Limit</label><input type="text" id="node-input-bodyLimit" placeholder="msg.limit"></div>
|
||||||
|
<div class="form-row"><label>Layout (JSON array)</label><input type="text" id="node-input-bodyLayout" placeholder="msg.layout"></div>
|
||||||
|
<div class="form-row"><label>Statuses (JSON array)</label><input type="text" id="node-input-bodyStatuses" placeholder="msg.statuses"></div>
|
||||||
|
<div class="form-tips"><p>Layout/Statuses: pass as arrays via <code>msg.layout</code> or <code>msg.statuses</code>.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
'widgets:list': { method:'GET', path:'/ocs/v2.php/apps/dashboard/api/v1/widgets' },
|
||||||
|
'widgetItems:v1': { method:'GET', path:'/ocs/v2.php/apps/dashboard/api/v1/widget-items', query:['sinceIds','limit'] },
|
||||||
|
'widgetItems:v2': { method:'GET', path:'/ocs/v2.php/apps/dashboard/api/v2/widget-items', query:['sinceIds','limit'] },
|
||||||
|
'layout:get': { method:'GET', path:'/ocs/v2.php/apps/dashboard/api/v3/layout' },
|
||||||
|
'layout:update': { method:'POST', path:'/ocs/v2.php/apps/dashboard/api/v3/layout', body:['layout'] },
|
||||||
|
'statuses:get': { method:'GET', path:'/ocs/v2.php/apps/dashboard/api/v3/statuses' },
|
||||||
|
'statuses:update': { method:'POST', path:'/ocs/v2.php/apps/dashboard/api/v3/statuses', body:['statuses'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function DashboardNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'widgets:list';
|
||||||
|
this.bodySinceIds = config.bodySinceIds; this.bodyLimit = config.bodyLimit;
|
||||||
|
this.bodyLayout = config.bodyLayout; this.bodyStatuses = config.bodyStatuses;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No Nextcloud configuration node selected"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown operation: " + (msg.operation || node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
var path = op.path;
|
||||||
|
|
||||||
|
var queryParts = [];
|
||||||
|
if (op.query) {
|
||||||
|
op.query.forEach(function(q) {
|
||||||
|
var v = msg[q] !== undefined ? msg[q] : getField(node, q);
|
||||||
|
if (v !== undefined && v !== '') queryParts.push(encodeURIComponent(q) + '=' + encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var fullUrl = baseUrl + path + (queryParts.length ? '?' + queryParts.join('&') : '');
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = { 'OCS-APIRequest':'true','Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(username+':'+password).toString('base64') };
|
||||||
|
|
||||||
|
var bodyObj = {};
|
||||||
|
if (op.body) {
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var v = msg[b] !== undefined ? msg[b] : getField(node, b);
|
||||||
|
if (v !== undefined && v !== '') bodyObj[b] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (msg.layout && Array.isArray(msg.layout)) bodyObj.layout = msg.layout;
|
||||||
|
if (msg.statuses && Array.isArray(msg.statuses)) bodyObj.statuses = msg.statuses;
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (Object.keys(bodyObj).length > 0) { headers['Content-Type']='application/json'; bodyStr=JSON.stringify(bodyObj); }
|
||||||
|
|
||||||
|
var opts = { hostname:parsedUrl.hostname, port:parsedUrl.port||(parsedUrl.protocol==='https:'?443:80),
|
||||||
|
path:parsedUrl.path, method:op.method, headers:headers, rejectUnauthorized:false };
|
||||||
|
|
||||||
|
node.log(op.method+' '+fullUrl);
|
||||||
|
var transport = parsedUrl.protocol==='https:'?https:http;
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body=''; res.on('data',function(c){body+=c;});
|
||||||
|
res.on('end',function(){ msg.statusCode=res.statusCode; msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(body);}catch(e){msg.payload=body;} node.send(msg); });
|
||||||
|
});
|
||||||
|
req.on('error',function(err){msg.error=err.message;node.error(op.method+' '+fullUrl+' — '+err.message,msg);});
|
||||||
|
if(bodyStr) req.write(bodyStr);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getField(node,name) {
|
||||||
|
var m={ 'sinceIds':node.bodySinceIds,'limit':node.bodyLimit,'layout':node.bodyLayout,'statuses':node.bodyStatuses };
|
||||||
|
return m[name];
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("dashboard", DashboardNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('dav', {
|
||||||
|
category: 'nextcloud', color: '#33AA55',
|
||||||
|
defaults: {
|
||||||
|
name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"direct:url"},
|
||||||
|
userId:{value:""},
|
||||||
|
bodyFileId:{value:""},bodyExpirationTime:{value:""},
|
||||||
|
bodyFirstDay:{value:""},bodyLastDay:{value:""},bodyStatus:{value:""},
|
||||||
|
bodyMessage:{value:""},bodyReplacementUserId:{value:""},bodyLocation:{value:""}
|
||||||
|
},
|
||||||
|
inputs:1,outputs:1,icon:"dav.svg",
|
||||||
|
label:function(){return this.name||"DAV";}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="dav">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="DAV"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Direct Links"><option value="direct:url">Get Direct Link</option></optgroup>
|
||||||
|
<optgroup label="Events"><option value="events:upcoming">Upcoming Events</option></optgroup>
|
||||||
|
<optgroup label="Out of Office"><option value="ooo:current">Current Status</option><option value="ooo:get">Get Absence</option><option value="ooo:set">Set Absence</option><option value="ooo:clear">Clear Absence</option></optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Path / Query Params</div>
|
||||||
|
<div class="form-row"><label>User ID</label><input type="text" id="node-input-userId" placeholder="msg.userId"></div>
|
||||||
|
<div class="form-row"><label>File ID (direct link)</label><input type="text" id="node-input-bodyFileId" placeholder="msg.fileId"></div>
|
||||||
|
<div class="form-row"><label>Expiration (seconds)</label><input type="text" id="node-input-bodyExpirationTime" placeholder="msg.expirationTime"></div>
|
||||||
|
<div class="form-row"><label>Location (events)</label><input type="text" id="node-input-bodyLocation" placeholder="msg.location"></div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Out of Office Fields</div>
|
||||||
|
<div class="form-row"><label>First Day (YYYY-MM-DD)</label><input type="text" id="node-input-bodyFirstDay" placeholder="msg.firstDay"></div>
|
||||||
|
<div class="form-row"><label>Last Day (YYYY-MM-DD)</label><input type="text" id="node-input-bodyLastDay" placeholder="msg.lastDay"></div>
|
||||||
|
<div class="form-row"><label>Status Text</label><input type="text" id="node-input-bodyStatus" placeholder="msg.status"></div>
|
||||||
|
<div class="form-row"><label>Message</label><textarea id="node-input-bodyMessage" rows="2" placeholder="msg.message"></textarea></div>
|
||||||
|
<div class="form-row"><label>Replacement User ID</label><input type="text" id="node-input-bodyReplacementUserId" placeholder="msg.replacementUserId"></div>
|
||||||
|
<div class="form-tips"><p>All fields overridable via <code>msg.*</code>.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
'direct:url': { method:'POST', path:'/ocs/v2.php/apps/dav/api/v1/direct', body:['fileId','expirationTime'] },
|
||||||
|
'events:upcoming': { method:'GET', path:'/ocs/v2.php/apps/dav/api/v1/events/upcoming', query:['location'] },
|
||||||
|
'ooo:current': { method:'GET', path:'/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}/now' },
|
||||||
|
'ooo:get': { method:'GET', path:'/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}' },
|
||||||
|
'ooo:set': { method:'POST', path:'/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}', body:['firstDay','lastDay','status','message','replacementUserId'] },
|
||||||
|
'ooo:clear': { method:'DELETE',path:'/ocs/v2.php/apps/dav/api/v1/outOfOffice/{userId}' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function DavNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'direct:url';
|
||||||
|
this.userId = config.userId;
|
||||||
|
this.bodyFileId = config.bodyFileId; this.bodyExpirationTime = config.bodyExpirationTime;
|
||||||
|
this.bodyFirstDay = config.bodyFirstDay; this.bodyLastDay = config.bodyLastDay;
|
||||||
|
this.bodyStatus = config.bodyStatus; this.bodyMessage = config.bodyMessage;
|
||||||
|
this.bodyReplacementUserId = config.bodyReplacementUserId;
|
||||||
|
this.bodyLocation = config.bodyLocation;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No Nextcloud configuration node selected"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown operation: " + (msg.operation || node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var path = op.path.replace('{userId}', msg.userId || node.userId || '');
|
||||||
|
|
||||||
|
var queryParts = [];
|
||||||
|
if (op.query) {
|
||||||
|
op.query.forEach(function(q) {
|
||||||
|
var v = msg[q] !== undefined ? msg[q] : getField(node, q);
|
||||||
|
if (v !== undefined && v !== '') queryParts.push(encodeURIComponent(q) + '=' + encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var fullUrl = baseUrl + path + (queryParts.length ? '?' + queryParts.join('&') : '');
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = { 'OCS-APIRequest':'true','Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(username+':'+password).toString('base64') };
|
||||||
|
|
||||||
|
var bodyObj = {};
|
||||||
|
if (op.body) {
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var v = msg[b] !== undefined ? msg[b] : getField(node, b);
|
||||||
|
if (v !== undefined && v !== '') bodyObj[b] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (Object.keys(bodyObj).length > 0) { headers['Content-Type']='application/json'; bodyStr=JSON.stringify(bodyObj); }
|
||||||
|
|
||||||
|
var opts = { hostname:parsedUrl.hostname, port:parsedUrl.port||(parsedUrl.protocol==='https:'?443:80),
|
||||||
|
path:parsedUrl.path, method:op.method, headers:headers, rejectUnauthorized:false };
|
||||||
|
|
||||||
|
node.log(op.method+' '+fullUrl);
|
||||||
|
var transport = parsedUrl.protocol==='https:'?https:http;
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body=''; res.on('data',function(c){body+=c;});
|
||||||
|
res.on('end',function(){ msg.statusCode=res.statusCode; msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(body);}catch(e){msg.payload=body;} node.send(msg); });
|
||||||
|
});
|
||||||
|
req.on('error',function(err){msg.error=err.message;node.error(op.method+' '+fullUrl+' — '+err.message,msg);});
|
||||||
|
if(bodyStr) req.write(bodyStr);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getField(node,name) {
|
||||||
|
var m={ 'fileId':node.bodyFileId,'expirationTime':node.bodyExpirationTime,'firstDay':node.bodyFirstDay,
|
||||||
|
'lastDay':node.bodyLastDay,'status':node.bodyStatus,'message':node.bodyMessage,
|
||||||
|
'replacementUserId':node.bodyReplacementUserId,'location':node.bodyLocation };
|
||||||
|
return m[name];
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("dav", DavNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('file-operations', {
|
||||||
|
category: 'nextcloud',
|
||||||
|
color: '#FF9900',
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
nextcloud: { type: "nextcloud-config", required: true },
|
||||||
|
operation: { value: "list" },
|
||||||
|
ncUser: { value: "" },
|
||||||
|
ncPath: { value: "" },
|
||||||
|
ncFolder: { value: "" },
|
||||||
|
destination: { value: "" },
|
||||||
|
bodySharePath: { value: "" },
|
||||||
|
bodyShareType: { value: "" },
|
||||||
|
bodyPermissions: { value: "" },
|
||||||
|
bodyShareWith: { value: "" }
|
||||||
|
},
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 1,
|
||||||
|
icon: "file.svg",
|
||||||
|
label: function() { return this.name || "File Operations"; },
|
||||||
|
oneditprepare: function() {
|
||||||
|
var opMeta = {
|
||||||
|
'list': { method: 'PROPFIND', user: true, folder: true, path: false, dest: false, share: false },
|
||||||
|
'get': { method: 'GET', user: true, folder: false, path: true, dest: false, share: false },
|
||||||
|
'upload': { method: 'PUT', user: true, folder: false, path: true, dest: false, share: false },
|
||||||
|
'mkdir': { method: 'MKCOL', user: true, folder: true, path: false, dest: false, share: false },
|
||||||
|
'move': { method: 'MOVE', user: true, folder: false, path: true, dest: true, share: false },
|
||||||
|
'copy': { method: 'COPY', user: true, folder: false, path: true, dest: true, share: false },
|
||||||
|
'delete': { method: 'DELETE', user: true, folder: false, path: true, dest: false, share: false },
|
||||||
|
'listShares': { method: 'GET', user: false, folder: false, path: false, dest: false, share: false },
|
||||||
|
'createShare': { method: 'POST', user: false, folder: false, path: false, dest: false, share: true },
|
||||||
|
'deleteShare': { method: 'DELETE', user: false, folder: false, path: false, dest: false, share: false },
|
||||||
|
'fileInfo': { method: 'GET', user: false, folder: false, path: false, dest: false, share: false },
|
||||||
|
'favorites': { method: 'GET', user: false, folder: false, path: false, dest: false, share: false }
|
||||||
|
};
|
||||||
|
|
||||||
|
function updateFields() {
|
||||||
|
var op = $('#node-input-operation').val();
|
||||||
|
var m = opMeta[op] || {};
|
||||||
|
$('#user-row').toggle(m.user);
|
||||||
|
$('#folder-row').toggle(m.folder);
|
||||||
|
$('#path-row').toggle(m.path);
|
||||||
|
$('#dest-row').toggle(m.dest);
|
||||||
|
$('#share-fields').toggle(m.share);
|
||||||
|
$('#op-method-file').text(m.method || 'GET');
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#node-input-operation').change(updateFields);
|
||||||
|
setTimeout(updateFields, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="text/html" data-template-name="file-operations">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="File Operations">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Nextcloud Config</label>
|
||||||
|
<input type="text" id="node-input-nextcloud" placeholder="Select config node">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Read/List">
|
||||||
|
<option value="list">List Directory</option>
|
||||||
|
<option value="get">Download File</option>
|
||||||
|
<option value="fileInfo">Storage Stats</option>
|
||||||
|
<option value="favorites">List Favorites</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Write">
|
||||||
|
<option value="upload">Upload File</option>
|
||||||
|
<option value="mkdir">Create Directory</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Modify">
|
||||||
|
<option value="move">Move/Rename</option>
|
||||||
|
<option value="copy">Copy</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Delete">
|
||||||
|
<option value="delete">Delete File/Folder</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Sharing (OCS)">
|
||||||
|
<option value="listShares">List Shares</option>
|
||||||
|
<option value="createShare">Create Share</option>
|
||||||
|
<option value="deleteShare">Delete Share</option>
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
<span id="op-method-file" style="margin-left:8px; color:#888; font-size:0.85em;">PROPFIND</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr style="margin:10px 0; border:none; border-top:1px solid #ccc;">
|
||||||
|
|
||||||
|
<div class="form-row" id="user-row">
|
||||||
|
<label for="node-input-ncUser"><i class="fa fa-user"></i> User</label>
|
||||||
|
<input type="text" id="node-input-ncUser" placeholder="Defaults to config username. msg.ncUser override.">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="folder-row">
|
||||||
|
<label for="node-input-ncFolder"><i class="fa fa-folder"></i> Folder Path</label>
|
||||||
|
<input type="text" id="node-input-ncFolder" placeholder="e.g. Documents/Project. msg.ncFolder override.">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="path-row">
|
||||||
|
<label for="node-input-ncPath"><i class="fa fa-file"></i> File Path</label>
|
||||||
|
<input type="text" id="node-input-ncPath" placeholder="e.g. Documents/report.txt. msg.ncPath override.">
|
||||||
|
</div>
|
||||||
|
<div class="form-row" id="dest-row">
|
||||||
|
<label for="node-input-destination"><i class="fa fa-arrow-right"></i> Destination</label>
|
||||||
|
<input type="text" id="node-input-destination" placeholder="e.g. Archive/old.txt. msg.destination override.">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="share-fields" style="display:none;">
|
||||||
|
<hr style="margin:10px 0; border:none; border-top:1px solid #ccc;">
|
||||||
|
<div style="font-weight:bold; margin-bottom:6px;">Share Options</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-bodySharePath">File Path to Share</label>
|
||||||
|
<input type="text" id="node-input-bodySharePath" placeholder="e.g. /Documents/file.txt. msg.path override.">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-bodyShareType">Share Type</label>
|
||||||
|
<select id="node-input-bodyShareType">
|
||||||
|
<option value="">Default (msg.shareType)</option>
|
||||||
|
<option value="0">User</option>
|
||||||
|
<option value="1">Group</option>
|
||||||
|
<option value="3">Public Link</option>
|
||||||
|
<option value="4">Email</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-bodyPermissions">Permissions</label>
|
||||||
|
<input type="text" id="node-input-bodyPermissions" placeholder="e.g. 31 = all. msg.permissions override.">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-bodyShareWith">Share With</label>
|
||||||
|
<input type="text" id="node-input-bodyShareWith" placeholder="Username/group. msg.shareWith override.">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-tips">
|
||||||
|
<p>All fields overridable via <code>msg.*</code>. Upload: pass file content in <code>msg.payload</code>.</p>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
// Read/List
|
||||||
|
'list': { method: 'PROPFIND', path: '/remote.php/dav/files/{user}/{folder}' },
|
||||||
|
'get': { method: 'GET', path: '/remote.php/dav/files/{user}/{path}' },
|
||||||
|
// Write
|
||||||
|
'upload': { method: 'PUT', path: '/remote.php/dav/files/{user}/{path}' },
|
||||||
|
'mkdir': { method: 'MKCOL', path: '/remote.php/dav/files/{user}/{folder}' },
|
||||||
|
// Modify
|
||||||
|
'move': { method: 'MOVE', path: '/remote.php/dav/files/{user}/{path}', destHeader: true },
|
||||||
|
'copy': { method: 'COPY', path: '/remote.php/dav/files/{user}/{path}', destHeader: true },
|
||||||
|
// Delete
|
||||||
|
'delete': { method: 'DELETE', path: '/remote.php/dav/files/{user}/{path}' },
|
||||||
|
// OCS sharing
|
||||||
|
'listShares': { method: 'GET', path: '/ocs/v2.php/apps/files_sharing/api/v1/shares' },
|
||||||
|
'createShare': { method: 'POST', path: '/ocs/v2.php/apps/files_sharing/api/v1/shares', body: ['path','shareType','permissions','shareWith'] },
|
||||||
|
'deleteShare': { method: 'DELETE', path: '/ocs/v2.php/apps/files_sharing/api/v1/shares/{id}' },
|
||||||
|
// File info
|
||||||
|
'fileInfo': { method: 'GET', path: '/ocs/v2.php/apps/files/api/v1/stats' },
|
||||||
|
'favorites': { method: 'GET', path: '/ocs/v2.php/apps/files/api/v1/files' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function FileOperationsNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'list';
|
||||||
|
this.ncUser = config.ncUser || '';
|
||||||
|
this.ncPath = config.ncPath || '';
|
||||||
|
this.ncFolder = config.ncFolder || '';
|
||||||
|
this.destination = config.destination || '';
|
||||||
|
this.bodySharePath = config.bodySharePath;
|
||||||
|
this.bodyShareType = config.bodyShareType;
|
||||||
|
this.bodyPermissions = config.bodyPermissions;
|
||||||
|
this.bodyShareWith = config.bodyShareWith;
|
||||||
|
|
||||||
|
if (!this.configNode) {
|
||||||
|
node.error("No Nextcloud configuration node selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) {
|
||||||
|
node.error("Unknown operation: " + (msg.operation || node.operation), msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var ncUser = msg.ncUser || node.ncUser || username;
|
||||||
|
var ncPath = msg.ncPath || node.ncPath || '';
|
||||||
|
var ncFolder = msg.ncFolder || node.ncFolder || '';
|
||||||
|
var dest = msg.destination || node.destination || '';
|
||||||
|
|
||||||
|
var path = op.path
|
||||||
|
.replace('{user}', encodeURIComponent(ncUser))
|
||||||
|
.replace('{path}', ncPath)
|
||||||
|
.replace('{folder}', ncFolder)
|
||||||
|
.replace('{id}', msg.id || '');
|
||||||
|
|
||||||
|
var fullUrl = baseUrl + path;
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = {
|
||||||
|
'Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
|
||||||
|
};
|
||||||
|
|
||||||
|
// WebDAV requests need depth header for PROPFIND
|
||||||
|
if (op.method === 'PROPFIND') {
|
||||||
|
headers['Depth'] = msg.depth || '1';
|
||||||
|
headers['Content-Type'] = 'application/xml';
|
||||||
|
}
|
||||||
|
|
||||||
|
// MOVE/COPY use Destination header
|
||||||
|
if (op.destHeader && dest) {
|
||||||
|
headers['Destination'] = baseUrl + '/remote.php/dav/files/' + encodeURIComponent(ncUser) + '/' + dest;
|
||||||
|
}
|
||||||
|
|
||||||
|
// OCS sharing headers
|
||||||
|
if (op.path.indexOf('/ocs/') === 0) {
|
||||||
|
headers['OCS-APIRequest'] = 'true';
|
||||||
|
headers['Accept'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.headers && typeof msg.headers === 'object') {
|
||||||
|
Object.keys(msg.headers).forEach(function(k) {
|
||||||
|
headers[k] = msg.headers[k];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (op.body) {
|
||||||
|
var bodyObj = {};
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var val = msg[b] !== undefined ? msg[b] : getBodyField(node, b);
|
||||||
|
if (val !== undefined && val !== '') {
|
||||||
|
bodyObj[b] = val;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (Object.keys(bodyObj).length > 0) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
bodyStr = JSON.stringify(bodyObj);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (msg.body) {
|
||||||
|
headers['Content-Type'] = msg.contentType || 'application/json';
|
||||||
|
bodyStr = typeof msg.body === 'string' ? msg.body : JSON.stringify(msg.body);
|
||||||
|
}
|
||||||
|
if (msg.payload && op.method === 'PUT') {
|
||||||
|
bodyStr = typeof msg.payload === 'string' ? msg.payload : JSON.stringify(msg.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
hostname: parsedUrl.hostname,
|
||||||
|
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsedUrl.path,
|
||||||
|
method: op.method,
|
||||||
|
headers: headers,
|
||||||
|
rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
var transport = parsedUrl.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
node.log(op.method + ' ' + fullUrl);
|
||||||
|
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body = '';
|
||||||
|
res.on('data', function(chunk) { body += chunk; });
|
||||||
|
res.on('end', function() {
|
||||||
|
msg.statusCode = res.statusCode;
|
||||||
|
msg.operation = msg.operation || node.operation;
|
||||||
|
// Try JSON, fallback to XML/text
|
||||||
|
try { msg.payload = JSON.parse(body); }
|
||||||
|
catch(e) {
|
||||||
|
// PROPFIND returns XML
|
||||||
|
msg.payload = body;
|
||||||
|
}
|
||||||
|
node.send(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', function(err) {
|
||||||
|
msg.error = err.message;
|
||||||
|
node.error(op.method + ' ' + fullUrl + ' — ' + err.message, msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (bodyStr) {
|
||||||
|
req.write(bodyStr);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBodyField(node, name) {
|
||||||
|
var map = {
|
||||||
|
'path': node.bodySharePath, 'shareType': node.bodyShareType,
|
||||||
|
'permissions': node.bodyPermissions, 'shareWith': node.bodyShareWith
|
||||||
|
};
|
||||||
|
return map[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType("file-operations", FileOperationsNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('filesharing',{category:'nextcloud',color:'#22AA66',
|
||||||
|
defaults:{name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"shares:list"},shareId:{value:""},token:{value:""}},
|
||||||
|
inputs:1,outputs:1,icon:"filesharing.svg",label:function(){return this.name||"File Sharing";}});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="filesharing">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="File Sharing"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Shares"><option value="shares:list">List Shares</option><option value="shares:get">Get Share</option><option value="shares:create">Create Share</option><option value="shares:update">Update Share</option><option value="shares:delete">Delete Share</option><option value="shares:inherited">Inherited Shares</option><option value="shares:pending">Pending Shares</option><option value="shares:accept">Accept Share</option><option value="shares:sendEmail">Send Share Email</option><option value="shares:token">Generate Token</option></optgroup>
|
||||||
|
<optgroup label="Deleted Shares"><option value="deleted:list">List Deleted</option><option value="deleted:restore">Restore Deleted</option></optgroup>
|
||||||
|
<optgroup label="Sharees"><option value="sharees:search">Search Sharees</option><option value="sharees:recommended">Recommended Sharees</option></optgroup>
|
||||||
|
<optgroup label="Remote Shares"><option value="remote:list">List Remote</option><option value="remote:pending">Pending Remote</option><option value="remote:get">Get Remote</option><option value="remote:accept">Accept Remote</option><option value="remote:decline">Decline Remote</option><option value="remote:unshare">Unshare Remote</option></optgroup>
|
||||||
|
<optgroup label="Preview & Info"><option value="preview:get">Public Preview</option><option value="info:get">Share Info</option></optgroup>
|
||||||
|
</select></div>
|
||||||
|
<hr><div class="form-row"><label>Share ID</label><input type="text" id="node-input-shareId" placeholder="msg.shareId"></div>
|
||||||
|
<div class="form-row"><label>Share Token</label><input type="text" id="node-input-token" placeholder="msg.token"></div>
|
||||||
|
<div class="form-tips"><p>Create share: pass <code>msg.path</code>, <code>msg.shareType</code>, <code>msg.shareWith</code>. Update: <code>msg.permissions</code>, <code>msg.expireDate</code>, etc.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
// Shares
|
||||||
|
'shares:list': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares', query:['shared_with_me','reshares','subfiles','path','include_tags'] },
|
||||||
|
'shares:get': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares/{id}', query:['include_tags'] },
|
||||||
|
'shares:create': { method:'POST', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares', body:['path','permissions','shareType','shareWith','password','expireDate','note','label','publicUpload','sendPasswordByTalk','attributes'] },
|
||||||
|
'shares:update': { method:'PUT', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares/{id}', body:['permissions','password','expireDate','note','label','hideDownload','token','sendPasswordByTalk'] },
|
||||||
|
'shares:delete': { method:'DELETE', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares/{id}' },
|
||||||
|
'shares:inherited': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares/inherited', query:['path'] },
|
||||||
|
'shares:pending': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares/pending' },
|
||||||
|
'shares:accept': { method:'POST', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares/pending/{id}' },
|
||||||
|
'shares:sendEmail': { method:'POST', path:'/ocs/v2.php/apps/files_sharing/api/v1/shares/{id}/send-email', body:['password'] },
|
||||||
|
'shares:token': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/token' },
|
||||||
|
// Deleted shares
|
||||||
|
'deleted:list': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/deletedshares' },
|
||||||
|
'deleted:restore': { method:'POST', path:'/ocs/v2.php/apps/files_sharing/api/v1/deletedshares/{id}' },
|
||||||
|
// Sharees
|
||||||
|
'sharees:search': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/sharees', query:['search','itemType','page','perPage','shareType','lookup'] },
|
||||||
|
'sharees:recommended':{ method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/sharees_recommended', query:['itemType','shareType'] },
|
||||||
|
// Remote shares
|
||||||
|
'remote:list': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/remote_shares' },
|
||||||
|
'remote:pending': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending' },
|
||||||
|
'remote:get': { method:'GET', path:'/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/{id}' },
|
||||||
|
'remote:accept': { method:'POST', path:'/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/{id}' },
|
||||||
|
'remote:decline': { method:'DELETE', path:'/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/pending/{id}' },
|
||||||
|
'remote:unshare': { method:'DELETE', path:'/ocs/v2.php/apps/files_sharing/api/v1/remote_shares/{id}' },
|
||||||
|
// Preview + Info
|
||||||
|
'preview:get': { method:'GET', path:'/index.php/apps/files_sharing/publicpreview/{token}', query:['file','x','y','a'] },
|
||||||
|
'info:get': { method:'POST', path:'/index.php/apps/files_sharing/shareinfo', body:['t','password','dir','depth'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function FSNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'shares:list';
|
||||||
|
this.shareId = config.shareId; this.token = config.token;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No NC config node"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown: "+(msg.operation||node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl||'').replace(/\/+$/,'');
|
||||||
|
var uname = node.configNode.credentials.username;
|
||||||
|
var pw = node.configNode.credentials.password;
|
||||||
|
var path = op.path
|
||||||
|
.replace(/\{id\}/g, msg.shareId||node.shareId||'')
|
||||||
|
.replace(/\{token\}/g, msg.token||node.token||'');
|
||||||
|
|
||||||
|
var qp = [];
|
||||||
|
if (op.query) op.query.forEach(function(q) {
|
||||||
|
var v = msg[q]!==undefined ? msg[q] : '';
|
||||||
|
if (v!==undefined&&v!=='') qp.push(encodeURIComponent(q)+'='+encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
var fu = baseUrl+path+(qp.length?'?'+qp.join('&'):'');
|
||||||
|
var pu = url.parse(fu);
|
||||||
|
|
||||||
|
var h = {'Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(uname+':'+pw).toString('base64')};
|
||||||
|
if (path.indexOf('/ocs/')===0) h['OCS-APIRequest']='true';
|
||||||
|
|
||||||
|
var bo={};
|
||||||
|
if (op.body) op.body.forEach(function(b){
|
||||||
|
var v=msg[b]!==undefined?msg[b]:'';
|
||||||
|
if(v!==undefined&&v!=='') bo[b]=v;
|
||||||
|
});
|
||||||
|
var bs=null;
|
||||||
|
if (Object.keys(bo).length>0) {h['Content-Type']='application/json'; bs=JSON.stringify(bo);}
|
||||||
|
|
||||||
|
var o = {hostname:pu.hostname,port:pu.port||(pu.protocol==='https:'?443:80),
|
||||||
|
path:pu.path,method:op.method,headers:h,rejectUnauthorized:false};
|
||||||
|
node.log(op.method+' '+fu);
|
||||||
|
var t = pu.protocol==='https:'?https:http;
|
||||||
|
var r = t.request(o,function(res){
|
||||||
|
var b='';res.on('data',function(c){b+=c;});
|
||||||
|
res.on('end',function(){msg.statusCode=res.statusCode;msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(b);}catch(e){msg.payload=b;}node.send(msg);});
|
||||||
|
});
|
||||||
|
r.on('error',function(e){msg.error=e.message;node.error(op.method+' '+fu+' - '+e.message,msg);});
|
||||||
|
if(bs) r.write(bs);
|
||||||
|
r.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("filesharing", FSNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('mail', {
|
||||||
|
category: 'nextcloud',
|
||||||
|
color: '#CC9900',
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" }, nextcloud: { type: "nextcloud-config", required: true },
|
||||||
|
operation: { value: "account:list" },
|
||||||
|
messageId: { value: "" }, attachmentId: { value: "" }, mailboxId: { value: "" },
|
||||||
|
bodyAccountId: { value: "" }, bodyFromEmail: { value: "" }, bodySubject: { value: "" },
|
||||||
|
bodyBody: { value: "" }, bodyIsHtml: { value: "" }, bodyTo: { value: "" },
|
||||||
|
bodyCc: { value: "" }, bodyBcc: { value: "" }, bodyReferences: { value: "" },
|
||||||
|
bodyCursor: { value: "" }, bodyFilter: { value: "" }, bodyLimit: { value: "" }, bodyView: { value: "" }
|
||||||
|
},
|
||||||
|
inputs: 1, outputs: 1, icon: "mail.svg",
|
||||||
|
label: function() { return this.name || "Mail"; },
|
||||||
|
oneditprepare: function() {
|
||||||
|
var opMeta = {
|
||||||
|
'account:list': { method:'GET', params:false, send:false, mailbox:false },
|
||||||
|
'message:get': { method:'GET', params:false, send:false, mailbox:false },
|
||||||
|
'message:getRaw': { method:'GET', params:false, send:false, mailbox:false },
|
||||||
|
'message:attachment':{ method:'GET', params:false, send:false, mailbox:false },
|
||||||
|
'mailbox:list': { method:'GET', params:false, send:false, mailbox:false },
|
||||||
|
'mailbox:messages': { method:'GET', params:true, send:false, mailbox:true },
|
||||||
|
'message:send': { method:'POST',params:false, send:true, mailbox:false }
|
||||||
|
};
|
||||||
|
function update() {
|
||||||
|
var m = opMeta[$('#node-input-operation').val()] || {};
|
||||||
|
$('#mail-params').toggle(m.params); $('#mail-send').toggle(m.send);
|
||||||
|
$('#mail-mailbox').toggle(m.mailbox); $('#op-method-mail').text(m.method || 'GET');
|
||||||
|
}
|
||||||
|
$('#node-input-operation').change(update); setTimeout(update, 100);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="mail">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Mail"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Nextcloud Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Accounts"><option value="account:list">List Accounts</option></optgroup>
|
||||||
|
<optgroup label="Messages"><option value="message:get">Get Message</option><option value="message:getRaw">Get Raw Message</option><option value="message:attachment">Get Attachment</option><option value="message:send">Send Email</option></optgroup>
|
||||||
|
<optgroup label="Mailboxes"><option value="mailbox:list">List Mailboxes</option><option value="mailbox:messages">List Messages</option></optgroup>
|
||||||
|
</select><span id="op-method-mail" style="margin-left:8px;color:#888;font-size:0.85em;">GET</span>
|
||||||
|
</div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Path Parameters</div>
|
||||||
|
<div class="form-row"><label>Message ID</label><input type="text" id="node-input-messageId" placeholder="msg.messageId"></div>
|
||||||
|
<div class="form-row"><label>Attachment ID</label><input type="text" id="node-input-attachmentId" placeholder="msg.attachmentId"></div>
|
||||||
|
<div class="form-row" id="mail-mailbox"><label>Mailbox ID</label><input type="text" id="node-input-mailboxId" placeholder="msg.mailboxId"></div>
|
||||||
|
<div id="mail-params" style="display:none;"><hr><div style="font-weight:bold;margin-bottom:6px;">Query Params</div>
|
||||||
|
<div class="form-row"><label>Cursor</label><input type="text" id="node-input-bodyCursor" placeholder="msg.cursor"></div>
|
||||||
|
<div class="form-row"><label>Filter</label><input type="text" id="node-input-bodyFilter" placeholder="msg.filter"></div>
|
||||||
|
<div class="form-row"><label>Limit</label><input type="text" id="node-input-bodyLimit" placeholder="msg.limit"></div>
|
||||||
|
<div class="form-row"><label>View</label><input type="text" id="node-input-bodyView" placeholder="msg.view"></div>
|
||||||
|
</div>
|
||||||
|
<div id="mail-send" style="display:none;"><hr><div style="font-weight:bold;margin-bottom:6px;">Send Email</div>
|
||||||
|
<div class="form-row"><label>Account ID</label><input type="text" id="node-input-bodyAccountId" placeholder="msg.accountId"></div>
|
||||||
|
<div class="form-row"><label>From Email</label><input type="text" id="node-input-bodyFromEmail" placeholder="msg.fromEmail"></div>
|
||||||
|
<div class="form-row"><label>Subject</label><input type="text" id="node-input-bodySubject" placeholder="msg.subject"></div>
|
||||||
|
<div class="form-row"><label>Body</label><textarea id="node-input-bodyBody" rows="3" placeholder="msg.body"></textarea></div>
|
||||||
|
<div class="form-row"><label>Is HTML</label><input type="text" id="node-input-bodyIsHtml" placeholder="true/false (msg.isHtml)"></div>
|
||||||
|
<div class="form-row"><label>To (JSON)</label><input type="text" id="node-input-bodyTo" placeholder='[{"email":"a@b.com"}]'></div>
|
||||||
|
<div class="form-row"><label>CC (JSON)</label><input type="text" id="node-input-bodyCc" placeholder='[{"email":"a@b.com"}]'></div>
|
||||||
|
<div class="form-row"><label>BCC (JSON)</label><input type="text" id="node-input-bodyBcc" placeholder='[{"email":"a@b.com"}]'></div>
|
||||||
|
<div class="form-row"><label>References</label><input type="text" id="node-input-bodyReferences" placeholder="msg.references"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-tips"><p>All fields overridable via <code>msg.*</code>. To/CC/BCC: pass arrays in msg.to/msg.cc/msg.bcc.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
'account:list': { method: 'GET', path: '/ocs/v2.php/apps/mail/account/list' },
|
||||||
|
'message:get': { method: 'GET', path: '/ocs/v2.php/apps/mail/message/{id}' },
|
||||||
|
'message:getRaw': { method: 'GET', path: '/ocs/v2.php/apps/mail/message/{id}/raw' },
|
||||||
|
'message:attachment': { method: 'GET', path: '/ocs/v2.php/apps/mail/message/{id}/attachment/{attachmentId}' },
|
||||||
|
'mailbox:list': { method: 'GET', path: '/ocs/v2.php/apps/mail/ocs/mailboxes', query: ['accountId'] },
|
||||||
|
'mailbox:messages': { method: 'GET', path: '/ocs/v2.php/apps/mail/ocs/mailboxes/{mailboxId}/messages', query: ['cursor','filter','limit','view'] },
|
||||||
|
'message:send': { method: 'POST', path: '/ocs/v2.php/apps/mail/message/send',
|
||||||
|
body: ['accountId','fromEmail','subject','body','isHtml','to','cc','bcc','references'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function MailNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'account:list';
|
||||||
|
this.messageId = config.messageId;
|
||||||
|
this.attachmentId = config.attachmentId;
|
||||||
|
this.mailboxId = config.mailboxId;
|
||||||
|
this.bodyAccountId = config.bodyAccountId;
|
||||||
|
this.bodyFromEmail = config.bodyFromEmail;
|
||||||
|
this.bodySubject = config.bodySubject;
|
||||||
|
this.bodyBody = config.bodyBody;
|
||||||
|
this.bodyIsHtml = config.bodyIsHtml;
|
||||||
|
this.bodyTo = config.bodyTo;
|
||||||
|
this.bodyCc = config.bodyCc;
|
||||||
|
this.bodyBcc = config.bodyBcc;
|
||||||
|
this.bodyReferences = config.bodyReferences;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No Nextcloud configuration node selected"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown operation: " + (msg.operation || node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var path = op.path
|
||||||
|
.replace('{id}', msg.messageId || node.messageId || '')
|
||||||
|
.replace('{attachmentId}', msg.attachmentId || node.attachmentId || '')
|
||||||
|
.replace('{mailboxId}', msg.mailboxId || node.mailboxId || '');
|
||||||
|
|
||||||
|
var queryParts = [];
|
||||||
|
if (op.query) {
|
||||||
|
op.query.forEach(function(q) {
|
||||||
|
var v = msg[q] !== undefined ? msg[q] : getField(node, q);
|
||||||
|
if (v !== undefined && v !== '') queryParts.push(encodeURIComponent(q) + '=' + encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var fullUrl = baseUrl + path + (queryParts.length ? '?' + queryParts.join('&') : '');
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = {
|
||||||
|
'OCS-APIRequest': 'true', 'Accept': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
|
||||||
|
};
|
||||||
|
|
||||||
|
var bodyObj = {};
|
||||||
|
if (op.body) {
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var v = msg[b] !== undefined ? msg[b] : getField(node, b);
|
||||||
|
if (v !== undefined && v !== '') bodyObj[b] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Handle complex send fields: to/cc/bcc can be msg arrays
|
||||||
|
if (msg.to && Array.isArray(msg.to)) bodyObj.to = msg.to;
|
||||||
|
if (msg.cc && Array.isArray(msg.cc)) bodyObj.cc = msg.cc;
|
||||||
|
if (msg.bcc && Array.isArray(msg.bcc)) bodyObj.bcc = msg.bcc;
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (Object.keys(bodyObj).length > 0) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
bodyStr = JSON.stringify(bodyObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
hostname: parsedUrl.hostname,
|
||||||
|
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsedUrl.path, method: op.method, headers: headers, rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
node.log(op.method + ' ' + fullUrl);
|
||||||
|
|
||||||
|
var transport = parsedUrl.protocol === 'https:' ? https : http;
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body = '';
|
||||||
|
res.on('data', function(chunk) { body += chunk; });
|
||||||
|
res.on('end', function() {
|
||||||
|
msg.statusCode = res.statusCode; msg.operation = msg.operation || node.operation;
|
||||||
|
try { msg.payload = JSON.parse(body); } catch(e) { msg.payload = body; }
|
||||||
|
node.send(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', function(err) { msg.error = err.message; node.error(op.method + ' ' + fullUrl + ' — ' + err.message, msg); });
|
||||||
|
if (bodyStr) req.write(bodyStr);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getField(node, name) {
|
||||||
|
var m = { 'accountId': node.bodyAccountId, 'fromEmail': node.bodyFromEmail,
|
||||||
|
'subject': node.bodySubject, 'body': node.bodyBody, 'isHtml': node.bodyIsHtml,
|
||||||
|
'references': node.bodyReferences, 'cursor': node.bodyCursor, 'filter': node.bodyFilter,
|
||||||
|
'limit': node.bodyLimit, 'view': node.bodyView };
|
||||||
|
return m[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType("mail", MailNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('nextcloud-config', {
|
||||||
|
category: 'config',
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
baseUrl: { value: "https://cloud.hannesahlin.se" }
|
||||||
|
},
|
||||||
|
credentials: {
|
||||||
|
username: { type: "text" },
|
||||||
|
password: { type: "password" }
|
||||||
|
},
|
||||||
|
label: function() {
|
||||||
|
return this.name || "Nextcloud Config";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="nextcloud-config">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-config-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-config-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-config-input-baseUrl"><i class="fa fa-globe"></i> Nextcloud URL</label>
|
||||||
|
<input type="text" id="node-config-input-baseUrl" placeholder="https://cloud.hannesahlin.se">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-config-input-username"><i class="fa fa-user"></i> Username</label>
|
||||||
|
<input type="text" id="node-config-input-username" placeholder="admin">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-config-input-password"><i class="fa fa-lock"></i> App Password</label>
|
||||||
|
<input type="password" id="node-config-input-password" placeholder="App password">
|
||||||
|
</div>
|
||||||
|
<div class="form-tips">
|
||||||
|
<p>Generate an app password in Nextcloud under Settings > Security > Devices & sessions.</p>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
function NextcloudConfigNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
this.name = config.name;
|
||||||
|
this.baseUrl = config.baseUrl;
|
||||||
|
this.username = config.username;
|
||||||
|
this.password = config.password;
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("nextcloud-config", NextcloudConfigNode, {
|
||||||
|
credentials: {
|
||||||
|
username: { type: "text" },
|
||||||
|
password: { type: "password" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('oauth2',{category:'nextcloud',color:'#994400',
|
||||||
|
defaults:{name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"oauth:token"}},
|
||||||
|
inputs:1,outputs:1,icon:"oauth2.svg",label:function(){return this.name||"OAuth2";}});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="oauth2">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="OAuth2"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<option value="oauth:authorize">Authorize (GET)</option><option value="oauth:token">Get Token (POST)</option>
|
||||||
|
</select></div>
|
||||||
|
<div class="form-tips"><p>Pass OAuth params via <code>msg.client_id</code>, <code>msg.grant_type</code>, <code>msg.code</code>, <code>msg.refresh_token</code> etc.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
'oauth:authorize': { method:'GET', path:'/index.php/apps/oauth2/authorize', query:['client_id','state','response_type','redirect_uri'] },
|
||||||
|
'oauth:token': { method:'POST', path:'/index.php/apps/oauth2/api/v1/token', body:['grant_type','code','refresh_token','client_id','client_secret'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function OAuth2Node(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'oauth:token';
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No NC config node"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown: "+(msg.operation||node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl||'').replace(/\/+$/,'');
|
||||||
|
var uname = node.configNode.credentials.username;
|
||||||
|
var pw = node.configNode.credentials.password;
|
||||||
|
var path = op.path;
|
||||||
|
|
||||||
|
var qp = [];
|
||||||
|
if (op.query) op.query.forEach(function(q) {
|
||||||
|
var v = msg[q]!==undefined ? msg[q] : '';
|
||||||
|
if (v!==undefined&&v!=='') qp.push(encodeURIComponent(q)+'='+encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
var fu = baseUrl+path+(qp.length?'?'+qp.join('&'):'');
|
||||||
|
var pu = url.parse(fu);
|
||||||
|
|
||||||
|
var h = {'Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(uname+':'+pw).toString('base64')};
|
||||||
|
|
||||||
|
var bo={};
|
||||||
|
if (op.body) op.body.forEach(function(b){
|
||||||
|
var v=msg[b]!==undefined?msg[b]:'';
|
||||||
|
if(v!==undefined&&v!=='') bo[b]=v;
|
||||||
|
});
|
||||||
|
var bs=null;
|
||||||
|
if (Object.keys(bo).length>0) {h['Content-Type']='application/json'; bs=JSON.stringify(bo);}
|
||||||
|
|
||||||
|
var o = {hostname:pu.hostname,port:pu.port||(pu.protocol==='https:'?443:80),
|
||||||
|
path:pu.path,method:op.method,headers:h,rejectUnauthorized:false};
|
||||||
|
node.log(op.method+' '+fu);
|
||||||
|
var t = pu.protocol==='https:'?https:http;
|
||||||
|
var r = t.request(o,function(res){
|
||||||
|
var b='';res.on('data',function(c){b+=c;});
|
||||||
|
res.on('end',function(){msg.statusCode=res.statusCode;msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(b);}catch(e){msg.payload=b;}node.send(msg);});
|
||||||
|
});
|
||||||
|
r.on('error',function(e){msg.error=e.message;node.error(op.method+' '+fu+' - '+e.message,msg);});
|
||||||
|
if(bs) r.write(bs);
|
||||||
|
r.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("oauth2", OAuth2Node);
|
||||||
|
};
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('ocs-api', {
|
||||||
|
category: 'nextcloud',
|
||||||
|
color: '#0082C9',
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" },
|
||||||
|
nextcloud: { type: "nextcloud-config", required: true },
|
||||||
|
endpoint: { value: "/ocs/v2.php/cloud/user" },
|
||||||
|
method: { value: "GET" }
|
||||||
|
},
|
||||||
|
inputs: 1,
|
||||||
|
outputs: 1,
|
||||||
|
icon: "file.svg",
|
||||||
|
label: function() {
|
||||||
|
return this.name || "OCS API";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="ocs-api">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
|
||||||
|
<input type="text" id="node-input-name" placeholder="Name">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Nextcloud Config</label>
|
||||||
|
<input type="text" id="node-input-nextcloud" placeholder="Select config node">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-endpoint"><i class="fa fa-link"></i> OCS Endpoint</label>
|
||||||
|
<input type="text" id="node-input-endpoint" placeholder="/ocs/v2.php/cloud/user">
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="node-input-method"><i class="fa fa-arrow-right"></i> Method</label>
|
||||||
|
<select id="node-input-method">
|
||||||
|
<option value="GET">GET</option>
|
||||||
|
<option value="POST">POST</option>
|
||||||
|
<option value="PUT">PUT</option>
|
||||||
|
<option value="DELETE">DELETE</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-tips">
|
||||||
|
<p>Overrides: pass <code>msg.endpoint</code>, <code>msg.method</code>, <code>msg.body</code>, or <code>msg.headers</code>.</p>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
function OcsApiNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.endpoint = config.endpoint || '/ocs/v2.php/cloud/user';
|
||||||
|
this.method = config.method || 'GET';
|
||||||
|
|
||||||
|
if (!this.configNode) {
|
||||||
|
node.error("No Nextcloud configuration node selected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var baseUrl = node.configNode.baseUrl;
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
// Allow msg to override endpoint
|
||||||
|
var apiPath = msg.endpoint || node.endpoint;
|
||||||
|
var apiMethod = msg.method || node.method;
|
||||||
|
|
||||||
|
// Build full URL
|
||||||
|
var fullUrl = baseUrl.replace(/\/+$/, '') + '/' + apiPath.replace(/^\/+/, '');
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
// Add OCS headers
|
||||||
|
var headers = {
|
||||||
|
'OCS-APIRequest': 'true',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
|
||||||
|
};
|
||||||
|
|
||||||
|
if (msg.headers && typeof msg.headers === 'object') {
|
||||||
|
Object.keys(msg.headers).forEach(function(k) {
|
||||||
|
headers[k] = msg.headers[k];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
hostname: parsedUrl.hostname,
|
||||||
|
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsedUrl.path,
|
||||||
|
method: apiMethod,
|
||||||
|
headers: headers,
|
||||||
|
rejectUnauthorized: false // Allow self-signed certs
|
||||||
|
};
|
||||||
|
|
||||||
|
var transport = parsedUrl.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
node.log(apiMethod + ' ' + fullUrl);
|
||||||
|
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body = '';
|
||||||
|
res.on('data', function(chunk) { body += chunk; });
|
||||||
|
res.on('end', function() {
|
||||||
|
msg.statusCode = res.statusCode;
|
||||||
|
try {
|
||||||
|
msg.payload = JSON.parse(body);
|
||||||
|
} catch(e) {
|
||||||
|
msg.payload = body;
|
||||||
|
}
|
||||||
|
node.send(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', function(err) {
|
||||||
|
msg.error = err.message;
|
||||||
|
node.error(apiMethod + ' ' + fullUrl + ' — ' + err.message, msg);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (msg.body) {
|
||||||
|
req.write(typeof msg.body === 'string' ? msg.body : JSON.stringify(msg.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("ocs-api", OcsApiNode);
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "node-red-contrib-nextcloud-ocs",
|
||||||
|
"version": "0.8.0",
|
||||||
|
"description": "Node-RED nodes for Nextcloud — 16 nodes covering full OCS API surface",
|
||||||
|
"keywords": ["node-red", "nextcloud", "ocs", "api"],
|
||||||
|
"node-red": {
|
||||||
|
"nodes": {
|
||||||
|
"nextcloud-config": "nextcloud-config.js",
|
||||||
|
"ocs-api": "ocs-api.js",
|
||||||
|
"collectives": "collectives.js",
|
||||||
|
"file-operations": "file-operations.js",
|
||||||
|
"mail": "mail.js",
|
||||||
|
"tables": "tables.js",
|
||||||
|
"talk": "talk.js",
|
||||||
|
"webhooks": "webhooks.js",
|
||||||
|
"dashboard": "dashboard.js",
|
||||||
|
"dav": "dav.js",
|
||||||
|
"core": "core.js",
|
||||||
|
"oauth2": "oauth2.js",
|
||||||
|
"provisioning": "provisioning.js",
|
||||||
|
"filesharing": "filesharing.js",
|
||||||
|
"userstatus": "userstatus.js",
|
||||||
|
"settings": "settings.js"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('provisioning',{category:'nextcloud',color:'#CC3333',
|
||||||
|
defaults:{name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"users:list"},
|
||||||
|
userId:{value:""},groupId:{value:""},appId:{value:""},configKey:{value:""},collectionName:{value:""},app:{value:""},key:{value:""},
|
||||||
|
bodySearch:{value:""},bodyLimit:{value:""},bodyOffset:{value:""},bodyUserid:{value:""},bodyPassword:{value:""},
|
||||||
|
bodyDisplayName:{value:""},bodyEmail:{value:""},bodyQuota:{value:""},bodyLanguage:{value:""},bodyKey:{value:""},
|
||||||
|
bodyValue:{value:""},bodyGroupid:{value:""},bodyLocation:{value:""},bodyConfigValue:{value:""}},
|
||||||
|
inputs:1,outputs:1,icon:"provisioning.svg",label:function(){return this.name||"Provisioning";}});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="provisioning">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Provisioning"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Groups"><option value="groups:list">List Groups</option><option value="groups:details">Groups Details</option><option value="groups:get">Get Group</option><option value="groups:users">Group Users</option><option value="groups:usersDetails">Group Users Details</option></optgroup>
|
||||||
|
<optgroup label="Users"><option value="users:list">List Users</option><option value="users:details">Users Details</option><option value="users:disabled">Disabled Users</option><option value="users:create">Create User</option><option value="users:get">Get User</option><option value="users:edit">Edit User</option><option value="users:editMulti">Edit Multi-Value</option><option value="users:delete">Delete User</option><option value="users:enable">Enable User</option><option value="users:disable">Disable User</option><option value="users:wipe">Wipe Devices</option><option value="users:current">Current User</option><option value="users:editableFields">Editable Fields</option><option value="users:editableFor">Editable Fields For User</option><option value="users:apps">Enabled Apps</option><option value="users:groups">User Groups</option><option value="users:groupsDetails">User Groups Details</option><option value="users:subadminDetails">Subadmin Details</option><option value="users:addToGroup">Add To Group</option><option value="users:removeFromGroup">Remove From Group</option><option value="users:welcome">Resend Welcome</option><option value="users:searchByPhone">Search By Phone</option></optgroup>
|
||||||
|
<optgroup label="Config & Preferences"><option value="config:setApp">Set App Config</option><option value="pref:set">Set Preference</option><option value="pref:delete">Delete Preference</option><option value="pref:setMultiple">Set Multiple Prefs</option><option value="pref:deleteMultiple">Delete Multiple Prefs</option></optgroup>
|
||||||
|
</select></div>
|
||||||
|
<hr><div class="form-row"><label>User ID</label><input type="text" id="node-input-userId" placeholder="msg.userId"></div>
|
||||||
|
<div class="form-row"><label>Group ID</label><input type="text" id="node-input-groupId" placeholder="msg.groupId"></div>
|
||||||
|
<div class="form-row"><label>App ID (prefs)</label><input type="text" id="node-input-appId" placeholder="msg.appId"></div>
|
||||||
|
<div class="form-row"><label>Config Key (prefs)</label><input type="text" id="node-input-configKey" placeholder="msg.configKey"></div>
|
||||||
|
<div class="form-tips"><p>Admin access required. Create user: pass groups via <code>msg.groups</code> array. Multiple prefs: <code>msg.configs</code> object.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
// Groups
|
||||||
|
'groups:list': { method:'GET', path:'/ocs/v2.php/cloud/groups', query:['search','limit','offset'] },
|
||||||
|
'groups:details': { method:'GET', path:'/ocs/v2.php/cloud/groups/details', query:['search','limit','offset'] },
|
||||||
|
'groups:get': { method:'GET', path:'/ocs/v2.php/cloud/groups/{groupId}' },
|
||||||
|
'groups:users': { method:'GET', path:'/ocs/v2.php/cloud/groups/{groupId}/users' },
|
||||||
|
'groups:usersDetails':{ method:'GET', path:'/ocs/v2.php/cloud/groups/{groupId}/users/details', query:['search','limit','offset'] },
|
||||||
|
// Users
|
||||||
|
'users:list': { method:'GET', path:'/ocs/v2.php/cloud/users', query:['search','limit','offset'] },
|
||||||
|
'users:details': { method:'GET', path:'/ocs/v2.php/cloud/users/details', query:['search','limit','offset'] },
|
||||||
|
'users:disabled': { method:'GET', path:'/ocs/v2.php/cloud/users/disabled', query:['search','limit','offset'] },
|
||||||
|
'users:create': { method:'POST', path:'/ocs/v2.php/cloud/users', body:['userid','password','displayName','email','quota','language'] },
|
||||||
|
'users:get': { method:'GET', path:'/ocs/v2.php/cloud/users/{userId}' },
|
||||||
|
'users:edit': { method:'PUT', path:'/ocs/v2.php/cloud/users/{userId}', body:['key','value'] },
|
||||||
|
'users:editMulti': { method:'PUT', path:'/ocs/v2.php/cloud/users/{userId}/{collectionName}', body:['key','value'] },
|
||||||
|
'users:delete': { method:'DELETE', path:'/ocs/v2.php/cloud/users/{userId}' },
|
||||||
|
'users:enable': { method:'PUT', path:'/ocs/v2.php/cloud/users/{userId}/enable' },
|
||||||
|
'users:disable': { method:'PUT', path:'/ocs/v2.php/cloud/users/{userId}/disable' },
|
||||||
|
'users:wipe': { method:'POST', path:'/ocs/v2.php/cloud/users/{userId}/wipe' },
|
||||||
|
'users:current': { method:'GET', path:'/ocs/v2.php/cloud/user' },
|
||||||
|
'users:editableFields':{ method:'GET', path:'/ocs/v2.php/cloud/user/fields' },
|
||||||
|
'users:editableFor': { method:'GET', path:'/ocs/v2.php/cloud/user/fields/{userId}' },
|
||||||
|
'users:apps': { method:'GET', path:'/ocs/v2.php/cloud/user/apps' },
|
||||||
|
'users:groups': { method:'GET', path:'/ocs/v2.php/cloud/users/{userId}/groups' },
|
||||||
|
'users:groupsDetails':{ method:'GET', path:'/ocs/v2.php/cloud/users/{userId}/groups/details' },
|
||||||
|
'users:subadminDetails':{ method:'GET', path:'/ocs/v2.php/cloud/users/{userId}/subadmins/details' },
|
||||||
|
'users:addToGroup': { method:'POST', path:'/ocs/v2.php/cloud/users/{userId}/groups', body:['groupid'] },
|
||||||
|
'users:removeFromGroup':{ method:'DELETE', path:'/ocs/v2.php/cloud/users/{userId}/groups', query:['groupid'] },
|
||||||
|
'users:welcome': { method:'POST', path:'/ocs/v2.php/cloud/users/{userId}/welcome' },
|
||||||
|
'users:searchByPhone':{ method:'POST', path:'/ocs/v2.php/cloud/users/search/by-phone', body:['location','search'] },
|
||||||
|
// App config
|
||||||
|
'config:setApp': { method:'POST', path:'/ocs/v2.php/apps/provisioning_api/api/v1/config/apps/{app}/{key}', body:['value'] },
|
||||||
|
// Preferences
|
||||||
|
'pref:set': { method:'POST', path:'/ocs/v2.php/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}', body:['configValue'] },
|
||||||
|
'pref:delete': { method:'DELETE', path:'/ocs/v2.php/apps/provisioning_api/api/v1/config/users/{appId}/{configKey}' },
|
||||||
|
'pref:setMultiple': { method:'POST', path:'/ocs/v2.php/apps/provisioning_api/api/v1/config/users/{appId}', body:['configs'] },
|
||||||
|
'pref:deleteMultiple':{ method:'DELETE', path:'/ocs/v2.php/apps/provisioning_api/api/v1/config/users/{appId}', query:['configKeys[]'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function ProvisioningNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'users:list';
|
||||||
|
this.userId = config.userId; this.groupId = config.groupId;
|
||||||
|
this.appId = config.appId; this.configKey = config.configKey;
|
||||||
|
this.collectionName = config.collectionName; this.app = config.app; this.key = config.key;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No NC config node"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown: "+(msg.operation||node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl||'').replace(/\/+$/,'');
|
||||||
|
var uname = node.configNode.credentials.username;
|
||||||
|
var pw = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var path = op.path
|
||||||
|
.replace(/\{userId\}/g, msg.userId||node.userId||'')
|
||||||
|
.replace(/\{groupId\}/g, msg.groupId||node.groupId||'')
|
||||||
|
.replace(/\{appId\}/g, msg.appId||node.appId||'')
|
||||||
|
.replace(/\{configKey\}/g, msg.configKey||node.configKey||'')
|
||||||
|
.replace(/\{collectionName\}/g, msg.collectionName||node.collectionName||'')
|
||||||
|
.replace(/\{app\}/g, msg.app||node.app||'')
|
||||||
|
.replace(/\{key\}/g, msg.key||node.key||'');
|
||||||
|
|
||||||
|
var qp = [];
|
||||||
|
if (op.query) op.query.forEach(function(q) {
|
||||||
|
var v = msg[q]!==undefined ? msg[q] : getF(node,q);
|
||||||
|
if (v!==undefined&&v!=='') qp.push(encodeURIComponent(q)+'='+encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
var fu = baseUrl+path+(qp.length?'?'+qp.join('&'):'');
|
||||||
|
var pu = url.parse(fu);
|
||||||
|
|
||||||
|
var h = {'OCS-APIRequest':'true','Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(uname+':'+pw).toString('base64')};
|
||||||
|
|
||||||
|
var bo={};
|
||||||
|
if (op.body) op.body.forEach(function(b){
|
||||||
|
var v=msg[b]!==undefined?msg[b]:getF(node,b);
|
||||||
|
if(v!==undefined&&v!=='') bo[b]=v;
|
||||||
|
});
|
||||||
|
// Special: groups array for user creation, configs object
|
||||||
|
if (msg.groups && Array.isArray(msg.groups)) bo.groups = msg.groups;
|
||||||
|
if (msg.subadmin && Array.isArray(msg.subadmin)) bo.subadmin = msg.subadmin;
|
||||||
|
if (msg.configs && typeof msg.configs==='object') bo.configs = msg.configs;
|
||||||
|
|
||||||
|
var bs=null;
|
||||||
|
if (Object.keys(bo).length>0) {h['Content-Type']='application/json'; bs=JSON.stringify(bo);}
|
||||||
|
|
||||||
|
var o = {hostname:pu.hostname,port:pu.port||(pu.protocol==='https:'?443:80),
|
||||||
|
path:pu.path,method:op.method,headers:h,rejectUnauthorized:false};
|
||||||
|
node.log(op.method+' '+fu);
|
||||||
|
var t = pu.protocol==='https:'?https:http;
|
||||||
|
var r = t.request(o,function(res){
|
||||||
|
var b='';res.on('data',function(c){b+=c;});
|
||||||
|
res.on('end',function(){msg.statusCode=res.statusCode;msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(b);}catch(e){msg.payload=b;}node.send(msg);});
|
||||||
|
});
|
||||||
|
r.on('error',function(e){msg.error=e.message;node.error(op.method+' '+fu+' - '+e.message,msg);});
|
||||||
|
if(bs) r.write(bs);
|
||||||
|
r.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getF(node,n){var m={'search':node.bodySearch,'limit':node.bodyLimit,'offset':node.bodyOffset,'userid':node.bodyUserid,
|
||||||
|
'password':node.bodyPassword,'displayName':node.bodyDisplayName,'email':node.bodyEmail,'quota':node.bodyQuota,
|
||||||
|
'language':node.bodyLanguage,'key':node.bodyKey,'value':node.bodyValue,'groupid':node.bodyGroupid,
|
||||||
|
'location':node.bodyLocation,'configValue':node.bodyConfigValue};return m[n];}
|
||||||
|
RED.nodes.registerType("provisioning", ProvisioningNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('settings',{category:'nextcloud',color:'#666666',
|
||||||
|
defaults:{name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"forms:list"}},
|
||||||
|
inputs:1,outputs:1,icon:"settings.svg",label:function(){return this.name||"Settings";}});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="settings">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Settings"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<option value="log:download">Download Log</option><option value="forms:list">List Forms</option>
|
||||||
|
<option value="forms:setValue">Set Form Value</option><option value="forms:setSensitive">Set Sensitive Value</option>
|
||||||
|
</select></div>
|
||||||
|
<div class="form-tips"><p>Admin access required for log download. Form values: pass <code>msg.app</code>, <code>msg.formId</code>, <code>msg.fieldId</code>, <code>msg.value</code>.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
'log:download': { method:'GET', path:'/index.php/settings/admin/log/download' },
|
||||||
|
'forms:list': { method:'GET', path:'/ocs/v2.php/settings/api/declarative/forms' },
|
||||||
|
'forms:setValue': { method:'POST', path:'/ocs/v2.php/settings/api/declarative/value', body:['app','formId','fieldId','value'] },
|
||||||
|
'forms:setSensitive': { method:'POST', path:'/ocs/v2.php/settings/api/declarative/value-sensitive', body:['app','formId','fieldId','value'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
function SettingsNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'forms:list';
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No NC config node"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown: "+(msg.operation||node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl||'').replace(/\/+$/,'');
|
||||||
|
var uname = node.configNode.credentials.username;
|
||||||
|
var pw = node.configNode.credentials.password;
|
||||||
|
var fu = baseUrl+op.path;
|
||||||
|
var pu = url.parse(fu);
|
||||||
|
|
||||||
|
var h = {'Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(uname+':'+pw).toString('base64')};
|
||||||
|
if (op.path.indexOf('/ocs/')===0) h['OCS-APIRequest']='true';
|
||||||
|
|
||||||
|
var bo={};
|
||||||
|
if (op.body) op.body.forEach(function(b){
|
||||||
|
var v=msg[b]!==undefined?msg[b]:'';
|
||||||
|
if(v!==undefined&&v!=='') bo[b]=v;
|
||||||
|
});
|
||||||
|
var bs=null;
|
||||||
|
if (Object.keys(bo).length>0) {h['Content-Type']='application/json'; bs=JSON.stringify(bo);}
|
||||||
|
|
||||||
|
var o = {hostname:pu.hostname,port:pu.port||(pu.protocol==='https:'?443:80),
|
||||||
|
path:pu.path,method:op.method,headers:h,rejectUnauthorized:false};
|
||||||
|
node.log(op.method+' '+fu);
|
||||||
|
var t = pu.protocol==='https:'?https:http;
|
||||||
|
var r = t.request(o,function(res){
|
||||||
|
var b='';res.on('data',function(c){b+=c;});
|
||||||
|
res.on('end',function(){msg.statusCode=res.statusCode;msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(b);}catch(e){msg.payload=b;}node.send(msg);});
|
||||||
|
});
|
||||||
|
r.on('error',function(e){msg.error=e.message;node.error(op.method+' '+fu+' - '+e.message,msg);});
|
||||||
|
if(bs) r.write(bs);
|
||||||
|
r.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("settings", SettingsNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('tables', {
|
||||||
|
category: 'nextcloud', color: '#3399FF',
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" }, nextcloud: { type: "nextcloud-config", required: true },
|
||||||
|
operation: { value: "table:list" },
|
||||||
|
tableId: { value: "" }, viewId: { value: "" }, rowId: { value: "" }, shareId: { value: "" },
|
||||||
|
nodeId: { value: "" }, nodeType: { value: "table" }, nodeCollection: { value: "tables" },
|
||||||
|
token: { value: "" }, contextId: { value: "" },
|
||||||
|
bodyTitle: { value: "" }, bodyEmoji: { value: "" }, bodyData: { value: "" },
|
||||||
|
bodyName: { value: "" }, bodyReceiver: { value: "" }, bodyReceiverType: { value: "" },
|
||||||
|
bodyBaseNodeId: { value: "" }, bodyBaseNodeType: { value: "table" }, bodyPassword: { value: "" },
|
||||||
|
bodyDescription: { value: "" }, bodyTemplate: { value: "" }, bodyNewOwnerId: { value: "" },
|
||||||
|
bodyLimit: { value: "" }, bodyOffset: { value: "" }
|
||||||
|
},
|
||||||
|
inputs: 1, outputs: 1, icon: "table.svg",
|
||||||
|
label: function() { return this.name || "Tables"; }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="tables">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Tables"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Tables"><option value="table:list">List Tables</option><option value="table:get">Get Table</option><option value="table:create">Create Table</option><option value="table:update">Update Table</option><option value="table:delete">Delete Table</option><option value="table:scheme">Get Scheme</option><option value="table:transfer">Transfer Ownership</option></optgroup>
|
||||||
|
<optgroup label="Init"><option value="init:index">Get Index</option></optgroup>
|
||||||
|
<optgroup label="Columns"><option value="column:list">List Columns</option><option value="column:get">Get Column</option><option value="column:text">Create Text Column</option><option value="column:number">Create Number Column</option><option value="column:selection">Create Selection Column</option><option value="column:datetime">Create Datetime Column</option><option value="column:usergroup">Create Usergroup Column</option></optgroup>
|
||||||
|
<optgroup label="Rows"><option value="row:list">List Rows</option><option value="row:create">Create Row</option><option value="row:get">Get Row</option><option value="row:update">Update Row</option><option value="row:delete">Delete Row</option></optgroup>
|
||||||
|
<optgroup label="Views"><option value="view:list">List Views</option><option value="view:get">Get View</option><option value="view:create">Create View</option><option value="view:update">Update View</option><option value="view:delete">Delete View</option></optgroup>
|
||||||
|
<optgroup label="Shares"><option value="share:listTable">List Table Shares</option><option value="share:listView">List View Shares</option><option value="share:create">Create Share</option><option value="share:update">Update Share</option><option value="share:delete">Delete Share</option><option value="share:linkCreate">Create Link Share</option></optgroup>
|
||||||
|
<optgroup label="Contexts"><option value="context:list">List Contexts</option><option value="context:get">Get Context</option><option value="context:create">Create Context</option><option value="context:update">Update Context</option><option value="context:delete">Delete Context</option></optgroup>
|
||||||
|
<optgroup label="Favorites"><option value="favorite:add">Add Favorite</option><option value="favorite:remove">Remove Favorite</option></optgroup>
|
||||||
|
<optgroup label="Import"><option value="import:table">Import Data</option></optgroup>
|
||||||
|
<optgroup label="Public Links"><option value="public:rows">Get Public Rows</option><option value="public:columns">Get Public Columns</option></optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Path Parameters</div>
|
||||||
|
<div class="form-row"><label>Table ID</label><input type="text" id="node-input-tableId" placeholder="msg.tableId"></div>
|
||||||
|
<div class="form-row"><label>View ID</label><input type="text" id="node-input-viewId" placeholder="msg.viewId"></div>
|
||||||
|
<div class="form-row"><label>Row ID</label><input type="text" id="node-input-rowId" placeholder="msg.rowId"></div>
|
||||||
|
<div class="form-row"><label>Share ID</label><input type="text" id="node-input-shareId" placeholder="msg.shareId"></div>
|
||||||
|
<div class="form-row"><label>Node ID</label><input type="text" id="node-input-nodeId" placeholder="msg.nodeId"></div>
|
||||||
|
<div class="form-row"><label>Node Type</label><select id="node-input-nodeType"><option value="table">table</option><option value="view">view</option></select></div>
|
||||||
|
<div class="form-row"><label>Node Collection</label><select id="node-input-nodeCollection"><option value="tables">tables</option><option value="views">views</option></select></div>
|
||||||
|
<div class="form-row"><label>Token (public)</label><input type="text" id="node-input-token" placeholder="msg.token"></div>
|
||||||
|
<div class="form-row"><label>Context ID</label><input type="text" id="node-input-contextId" placeholder="msg.contextId"></div>
|
||||||
|
<div class="form-tips"><p>All params overridable via <code>msg.*</code>. Row data: pass in <code>msg.data</code> as object keyed by column ID.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,162 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
// Tables (api v2 OCS)
|
||||||
|
'table:list': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/tables' },
|
||||||
|
'table:get': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/tables/{id}' },
|
||||||
|
'table:create': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/tables', body:['title','emoji','description','template'] },
|
||||||
|
'table:update': { method:'PUT', path:'/ocs/v2.php/apps/tables/api/2/tables/{id}', body:['title','emoji','description','archived'] },
|
||||||
|
'table:delete': { method:'DELETE', path:'/ocs/v2.php/apps/tables/api/2/tables/{id}' },
|
||||||
|
'table:scheme': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/tables/scheme/{id}' },
|
||||||
|
'table:transfer': { method:'PUT', path:'/ocs/v2.php/apps/tables/api/2/tables/{id}/transfer', body:['newOwnerUserId'] },
|
||||||
|
// Init
|
||||||
|
'init:index': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/init' },
|
||||||
|
// Columns (api v2)
|
||||||
|
'column:list': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/columns/{nodeType}/{nodeId}' },
|
||||||
|
'column:get': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/columns/{id}' },
|
||||||
|
'column:text': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/columns/text', body:['baseNodeId','title','textDefault','textMaxLength','textUnique','mandatory','baseNodeType','selectedViewIds'] },
|
||||||
|
'column:number': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/columns/number', body:['baseNodeId','title','numberDefault','numberDecimals','numberMin','numberMax','mandatory','baseNodeType'] },
|
||||||
|
'column:selection': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/columns/selection', body:['baseNodeId','title','selectionOptions','selectionDefault','mandatory','baseNodeType'] },
|
||||||
|
'column:datetime': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/columns/datetime', body:['baseNodeId','title','datetimeDefault','mandatory','baseNodeType'] },
|
||||||
|
'column:usergroup': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/columns/usergroup', body:['baseNodeId','title','usergroupDefault','usergroupMultipleItems','usergroupSelectUsers','usergroupSelectGroups','mandatory','baseNodeType'] },
|
||||||
|
// Rows (api v2)
|
||||||
|
'row:list': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/{nodeCollection}/{nodeId}/rows', query:['limit','offset'] },
|
||||||
|
'row:create': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/{nodeCollection}/{nodeId}/rows', body:['data'] },
|
||||||
|
// Generic row ops (api v1)
|
||||||
|
'row:get': { method:'GET', path:'/index.php/apps/tables/api/1/rows/{rowId}' },
|
||||||
|
'row:update': { method:'PUT', path:'/index.php/apps/tables/api/1/rows/{rowId}', body:['data'] },
|
||||||
|
'row:delete': { method:'DELETE', path:'/index.php/apps/tables/api/1/rows/{rowId}' },
|
||||||
|
// Views
|
||||||
|
'view:list': { method:'GET', path:'/index.php/apps/tables/api/1/tables/{tableId}/views' },
|
||||||
|
'view:get': { method:'GET', path:'/index.php/apps/tables/api/1/views/{viewId}' },
|
||||||
|
'view:create': { method:'POST', path:'/index.php/apps/tables/api/1/tables/{tableId}/views', body:['title','emoji'] },
|
||||||
|
'view:update': { method:'PUT', path:'/index.php/apps/tables/api/1/views/{viewId}', body:['data'] },
|
||||||
|
'view:delete': { method:'DELETE', path:'/index.php/apps/tables/api/1/views/{viewId}' },
|
||||||
|
// Shares
|
||||||
|
'share:listTable': { method:'GET', path:'/index.php/apps/tables/api/1/tables/{tableId}/shares' },
|
||||||
|
'share:listView': { method:'GET', path:'/index.php/apps/tables/api/1/views/{viewId}/shares' },
|
||||||
|
'share:create': { method:'POST', path:'/index.php/apps/tables/api/1/shares', body:['nodeId','nodeType','receiver','receiverType','permissionRead','permissionCreate','permissionUpdate','permissionDelete','permissionManage'] },
|
||||||
|
'share:update': { method:'PUT', path:'/index.php/apps/tables/api/1/shares/{shareId}', body:['permissionType','permissionValue'] },
|
||||||
|
'share:delete': { method:'DELETE', path:'/index.php/apps/tables/api/1/shares/{shareId}' },
|
||||||
|
'share:linkCreate': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/{nodeCollection}/{nodeId}/share', body:['password'] },
|
||||||
|
// Contexts
|
||||||
|
'context:list': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/contexts' },
|
||||||
|
'context:get': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/contexts/{contextId}' },
|
||||||
|
'context:create': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/contexts', body:['name','iconName','description'] },
|
||||||
|
'context:update': { method:'PUT', path:'/ocs/v2.php/apps/tables/api/2/contexts/{contextId}', body:['name','iconName','description'] },
|
||||||
|
'context:delete': { method:'DELETE', path:'/ocs/v2.php/apps/tables/api/2/contexts/{contextId}' },
|
||||||
|
// Favorites
|
||||||
|
'favorite:add': { method:'POST', path:'/ocs/v2.php/apps/tables/api/2/favorites/{nodeType}/{nodeId}' },
|
||||||
|
'favorite:remove': { method:'DELETE', path:'/ocs/v2.php/apps/tables/api/2/favorites/{nodeType}/{nodeId}' },
|
||||||
|
// Import
|
||||||
|
'import:table': { method:'POST', path:'/index.php/apps/tables/api/1/import/table/{tableId}', body:['path','createMissingColumns'] },
|
||||||
|
// Public rows
|
||||||
|
'public:rows': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/public/{token}/rows', query:['limit','offset'] },
|
||||||
|
'public:columns': { method:'GET', path:'/ocs/v2.php/apps/tables/api/2/public/{token}/columns' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function TablesNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'table:list';
|
||||||
|
this.tableId = config.tableId; this.viewId = config.viewId; this.rowId = config.rowId;
|
||||||
|
this.shareId = config.shareId; this.nodeId = config.nodeId; this.nodeType = config.nodeType;
|
||||||
|
this.nodeCollection = config.nodeCollection; this.token = config.token; this.contextId = config.contextId;
|
||||||
|
this.bodyTitle = config.bodyTitle; this.bodyEmoji = config.bodyEmoji; this.bodyData = config.bodyData;
|
||||||
|
this.bodyName = config.bodyName; this.bodyReceiver = config.bodyReceiver; this.bodyReceiverType = config.bodyReceiverType;
|
||||||
|
this.bodyBaseNodeId = config.bodyBaseNodeId; this.bodyBaseNodeType = config.bodyBaseNodeType;
|
||||||
|
if (!this.configNode) { node.error("No Nextcloud configuration node selected"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown operation: " + (msg.operation || node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var path = op.path
|
||||||
|
.replace(/\{id\}/g, msg.id || node.nodeId || '')
|
||||||
|
.replace(/\{tableId\}/g, msg.tableId || node.tableId || '')
|
||||||
|
.replace(/\{viewId\}/g, msg.viewId || node.viewId || '')
|
||||||
|
.replace(/\{rowId\}/g, msg.rowId || node.rowId || '')
|
||||||
|
.replace(/\{shareId\}/g, msg.shareId || node.shareId || '')
|
||||||
|
.replace(/\{nodeId\}/g, msg.nodeId || node.nodeId || '')
|
||||||
|
.replace(/\{nodeType\}/g, msg.nodeType || node.nodeType || 'table')
|
||||||
|
.replace(/\{nodeCollection\}/g, msg.nodeCollection || node.nodeCollection || 'tables')
|
||||||
|
.replace(/\{token\}/g, msg.token || node.token || '')
|
||||||
|
.replace(/\{contextId\}/g, msg.contextId || node.contextId || '');
|
||||||
|
|
||||||
|
var queryParts = [];
|
||||||
|
if (op.query) {
|
||||||
|
op.query.forEach(function(q) {
|
||||||
|
var v = msg[q] !== undefined ? msg[q] : getField(node, q);
|
||||||
|
if (v !== undefined && v !== '') queryParts.push(encodeURIComponent(q) + '=' + encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var fullUrl = baseUrl + path + (queryParts.length ? '?' + queryParts.join('&') : '');
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
|
||||||
|
};
|
||||||
|
if (path.indexOf('/ocs/') === 0) headers['OCS-APIRequest'] = 'true';
|
||||||
|
|
||||||
|
var bodyObj = {};
|
||||||
|
if (op.body) {
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var v = msg[b] !== undefined ? msg[b] : getField(node, b);
|
||||||
|
if (v !== undefined && v !== '') bodyObj[b] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (msg.data !== undefined) bodyObj.data = msg.data;
|
||||||
|
if (msg.body && typeof msg.body === 'object') Object.assign(bodyObj, msg.body);
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (Object.keys(bodyObj).length > 0) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
bodyStr = JSON.stringify(bodyObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
hostname: parsedUrl.hostname,
|
||||||
|
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsedUrl.path, method: op.method, headers: headers, rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
node.log(op.method + ' ' + fullUrl);
|
||||||
|
|
||||||
|
var transport = parsedUrl.protocol === 'https:' ? https : http;
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body = '';
|
||||||
|
res.on('data', function(chunk) { body += chunk; });
|
||||||
|
res.on('end', function() {
|
||||||
|
msg.statusCode = res.statusCode; msg.operation = msg.operation || node.operation;
|
||||||
|
try { msg.payload = JSON.parse(body); } catch(e) { msg.payload = body; }
|
||||||
|
node.send(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', function(err) { msg.error = err.message; node.error(op.method + ' ' + fullUrl + ' — ' + err.message, msg); });
|
||||||
|
if (bodyStr) req.write(bodyStr);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getField(node, name) {
|
||||||
|
var m = { 'title': node.bodyTitle, 'emoji': node.bodyEmoji, 'name': node.bodyName,
|
||||||
|
'receiver': node.bodyReceiver, 'receiverType': node.bodyReceiverType,
|
||||||
|
'baseNodeId': node.bodyBaseNodeId, 'baseNodeType': node.bodyBaseNodeType,
|
||||||
|
'data': node.bodyData, 'limit': node.bodyLimit, 'offset': node.bodyOffset,
|
||||||
|
'password': node.bodyPassword, 'description': node.bodyDescription,
|
||||||
|
'template': node.bodyTemplate, 'nodeId': node.nodeId, 'nodeType': node.nodeType,
|
||||||
|
'newOwnerUserId': node.bodyNewOwnerId };
|
||||||
|
return m[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType("tables", TablesNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('talk', {
|
||||||
|
category: 'nextcloud', color: '#0082C9',
|
||||||
|
defaults: {
|
||||||
|
name: { value: "" }, nextcloud: { type: "nextcloud-config", required: true },
|
||||||
|
operation: { value: "room:list" },
|
||||||
|
token: { value: "" }, messageId: { value: "" }, attendeeId: { value: "" },
|
||||||
|
pollId: { value: "" }, banId: { value: "" }, threadId: { value: "" },
|
||||||
|
fileId: { value: "" }, shareToken: { value: "" },
|
||||||
|
bodyMessage: { value: "" }, bodyRoomName: { value: "" }, bodyRoomType: { value: "" },
|
||||||
|
bodyLevel: { value: "" }, bodyReplyTo: { value: "" }, bodySilent: { value: "" },
|
||||||
|
bodyDescription: { value: "" }, bodyQuestion: { value: "" }, bodyPassword: { value: "" },
|
||||||
|
bodyEmoji: { value: "" }, bodyTimestamp: { value: "" }, bodyReaction: { value: "" },
|
||||||
|
bodyActorType: { value: "" }, bodyActorId: { value: "" }, bodyFlags: { value: "" },
|
||||||
|
bodyState: { value: "" }, bodyKey: { value: "" }, bodyValue: { value: "" },
|
||||||
|
bodyCalendarUri: { value: "" }, bodyStart: { value: "" }, bodyThreadTitle: { value: "" },
|
||||||
|
bodySendAt: { value: "" }, bodyPinUntil: { value: "" }, bodyMode: { value: "" },
|
||||||
|
bodyAmount: { value: "" }, bodyStatus: { value: "" }
|
||||||
|
},
|
||||||
|
inputs: 1, outputs: 1, icon: "talk.svg",
|
||||||
|
label: function() { return this.name || "Talk"; }
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="talk">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Talk"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<optgroup label="Rooms">
|
||||||
|
<option value="room:list">List Rooms</option><option value="room:listed">List Listed Rooms</option>
|
||||||
|
<option value="room:get">Get Room</option><option value="room:create">Create Room</option>
|
||||||
|
<option value="room:rename">Rename Room</option><option value="room:delete">Delete Room</option>
|
||||||
|
<option value="room:noteToSelf">Note to Self</option>
|
||||||
|
<option value="room:description">Set Description</option>
|
||||||
|
<option value="room:password">Set Password</option>
|
||||||
|
<option value="room:readOnly">Set Read-Only</option>
|
||||||
|
<option value="room:listable">Set Listable</option>
|
||||||
|
<option value="room:archive">Archive</option><option value="room:unarchive">Unarchive</option>
|
||||||
|
<option value="room:important">Mark Important</option><option value="room:unimportant">Unmark Important</option>
|
||||||
|
<option value="room:sensitive">Mark Sensitive</option><option value="room:insensitive">Unmark Sensitive</option>
|
||||||
|
<option value="room:messageExpiration">Set Message Expiration</option>
|
||||||
|
<option value="room:capabilities">Get Capabilities</option>
|
||||||
|
<option value="room:scheduleMeeting">Schedule Meeting</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Favorites & Notifications">
|
||||||
|
<option value="favorite:add">Add Favorite</option><option value="favorite:remove">Remove Favorite</option>
|
||||||
|
<option value="notify:level">Set Notification Level</option><option value="notify:calls">Set Call Notifications</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Public/Private">
|
||||||
|
<option value="room:public">Make Public</option><option value="room:private">Make Private</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Participants">
|
||||||
|
<option value="participant:list">List Participants</option><option value="participant:add">Add Participant</option>
|
||||||
|
<option value="participant:remove">Remove Attendee</option><option value="participant:removeSelf">Leave Room</option>
|
||||||
|
<option value="participant:join">Join Room</option><option value="participant:leave">Leave Active</option>
|
||||||
|
<option value="participant:sessionState">Set Session State</option>
|
||||||
|
<option value="participant:promote">Promote Moderator</option><option value="participant:demote">Demote Moderator</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Permissions">
|
||||||
|
<option value="permissions:default">Set Default Permissions</option>
|
||||||
|
<option value="permissions:call">Set Call Permissions</option>
|
||||||
|
<option value="permissions:attendee">Set Attendee Permissions</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Chat">
|
||||||
|
<option value="chat:send">Send Message</option><option value="chat:receive">Receive Messages</option>
|
||||||
|
<option value="chat:delete">Delete Message</option><option value="chat:edit">Edit Message</option>
|
||||||
|
<option value="chat:context">Get Context</option><option value="chat:reminderSet">Set Reminder</option>
|
||||||
|
<option value="chat:reminderGet">Get Reminder</option><option value="chat:reminderDelete">Delete Reminder</option>
|
||||||
|
<option value="chat:readMarker">Set Read Marker</option><option value="chat:unread">Mark Unread</option>
|
||||||
|
<option value="chat:pin">Pin Message</option><option value="chat:unpin">Unpin Message</option>
|
||||||
|
<option value="chat:clearHistory">Clear History</option>
|
||||||
|
<option value="chat:share">Share Object</option><option value="chat:sharedOverview">Shared Overview</option>
|
||||||
|
<option value="chat:sharedObjects">Shared Objects</option>
|
||||||
|
<option value="chat:mentions">Search Mentions</option>
|
||||||
|
<option value="chat:schedule">Schedule Message</option>
|
||||||
|
<option value="chat:scheduledGet">Get Scheduled</option><option value="chat:scheduledDelete">Delete Scheduled</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Call">
|
||||||
|
<option value="call:peers">Get Peers</option><option value="call:join">Join Call</option>
|
||||||
|
<option value="call:updateFlags">Update Flags</option><option value="call:leave">Leave Call</option>
|
||||||
|
<option value="call:ring">Ring Attendee</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Reactions"><option value="reaction:add">Add Reaction</option><option value="reaction:delete">Delete Reaction</option><option value="reaction:list">List Reactions</option></optgroup>
|
||||||
|
<optgroup label="Polls"><option value="poll:create">Create Poll</option><option value="poll:get">Get Poll</option><option value="poll:vote">Vote</option><option value="poll:close">Close Poll</option></optgroup>
|
||||||
|
<optgroup label="Bans"><option value="ban:list">List Bans</option><option value="ban:add">Ban Actor</option><option value="ban:remove">Unban</option></optgroup>
|
||||||
|
<optgroup label="Avatar"><option value="avatar:get">Get Avatar</option><option value="avatar:emoji">Set Emoji Avatar</option><option value="avatar:delete">Delete Avatar</option></optgroup>
|
||||||
|
<optgroup label="Breakout Rooms"><option value="breakout:configure">Configure</option><option value="breakout:start">Start</option><option value="breakout:stop">Stop</option><option value="breakout:remove">Remove</option><option value="breakout:broadcast">Broadcast</option><option value="breakout:list">List Breakout Rooms</option></optgroup>
|
||||||
|
<optgroup label="Recording"><option value="recording:start">Start</option><option value="recording:stop">Stop</option></optgroup>
|
||||||
|
<optgroup label="Signaling"><option value="signaling:settings">Get Settings</option></optgroup>
|
||||||
|
<optgroup label="Files"><option value="file:getRoomByFileId">Get Room by File ID</option><option value="file:getRoomByShare">Get Room by Share Token</option></optgroup>
|
||||||
|
<optgroup label="Threads"><option value="thread:recent">Recent Threads</option><option value="thread:subscribed">Subscribed Threads</option><option value="thread:get">Get Thread</option><option value="thread:rename">Rename Thread</option><option value="thread:notify">Set Thread Notification</option></optgroup>
|
||||||
|
<optgroup label="Calendar"><option value="calendar:dashboard">Dashboard Events</option><option value="calendar:mutual">Mutual Events</option></optgroup>
|
||||||
|
<optgroup label="Settings"><option value="settings:set">Set User Setting</option></optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Path Parameters</div>
|
||||||
|
<div class="form-row"><label>Room Token</label><input type="text" id="node-input-token" placeholder="msg.token"></div>
|
||||||
|
<div class="form-row"><label>Message ID</label><input type="text" id="node-input-messageId" placeholder="msg.messageId"></div>
|
||||||
|
<div class="form-row"><label>Attendee ID</label><input type="text" id="node-input-attendeeId" placeholder="msg.attendeeId"></div>
|
||||||
|
<div class="form-row"><label>Poll ID</label><input type="text" id="node-input-pollId" placeholder="msg.pollId"></div>
|
||||||
|
<div class="form-row"><label>Ban ID</label><input type="text" id="node-input-banId" placeholder="msg.banId"></div>
|
||||||
|
<div class="form-row"><label>Thread ID</label><input type="text" id="node-input-threadId" placeholder="msg.threadId"></div>
|
||||||
|
<div class="form-row"><label>File ID</label><input type="text" id="node-input-fileId" placeholder="msg.fileId"></div>
|
||||||
|
<div class="form-row"><label>Share Token</label><input type="text" id="node-input-shareToken" placeholder="msg.shareToken"></div>
|
||||||
|
<div class="form-tips"><p>All path params and body fields overridable via <code>msg.*</code>. Poll options: pass as <code>msg.options</code> array.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
// --- Room ---
|
||||||
|
'room:list': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/room', query:['noStatusUpdate','includeStatus','modifiedSince','includeLastMessage'] },
|
||||||
|
'room:listed': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/listed-room', query:['searchTerm'] },
|
||||||
|
'room:get': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}' },
|
||||||
|
'room:create': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room', body:['roomType','roomName','objectType','objectId','password','readOnly','listable','description','emoji'] },
|
||||||
|
'room:rename': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}', body:['roomName'] },
|
||||||
|
'room:delete': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}' },
|
||||||
|
'room:noteToSelf': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/room/note-to-self' },
|
||||||
|
'room:description': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/description', body:['description'] },
|
||||||
|
'room:password': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/password', body:['password'] },
|
||||||
|
'room:readOnly': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/read-only', body:['state'] },
|
||||||
|
'room:listable': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/listable', body:['scope'] },
|
||||||
|
'room:archive': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/archive' },
|
||||||
|
'room:unarchive': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/archive' },
|
||||||
|
'room:important': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/important' },
|
||||||
|
'room:unimportant': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/important' },
|
||||||
|
'room:sensitive': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/sensitive' },
|
||||||
|
'room:insensitive': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/sensitive' },
|
||||||
|
'room:messageExpiration': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/message-expiration', body:['seconds'] },
|
||||||
|
'room:capabilities': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/capabilities' },
|
||||||
|
'room:scheduleMeeting': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/meeting', body:['calendarUri','start','end','title','description'] },
|
||||||
|
// --- Favorites ---
|
||||||
|
'favorite:add': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/favorite' },
|
||||||
|
'favorite:remove': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/favorite' },
|
||||||
|
// --- Notification ---
|
||||||
|
'notify:level': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/notify', body:['level'] },
|
||||||
|
'notify:calls': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/notify-calls', body:['level'] },
|
||||||
|
// --- Public/Private ---
|
||||||
|
'room:public': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/public', body:['password'] },
|
||||||
|
'room:private': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/public' },
|
||||||
|
// --- Participants ---
|
||||||
|
'participant:list': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/participants', query:['includeStatus'] },
|
||||||
|
'participant:add': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/participants', body:['newParticipant','source'] },
|
||||||
|
'participant:removeSelf': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/participants/self' },
|
||||||
|
'participant:remove': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/attendees', query:['attendeeId'] },
|
||||||
|
'participant:join': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/participants/active', body:['password','force'] },
|
||||||
|
'participant:leave': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/participants/active' },
|
||||||
|
'participant:sessionState': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/participants/state', body:['state'] },
|
||||||
|
'participant:promote': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/moderators', body:['attendeeId'] },
|
||||||
|
'participant:demote': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/moderators', query:['attendeeId'] },
|
||||||
|
// --- Permissions ---
|
||||||
|
'permissions:default': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/permissions/default', body:['permissions'] },
|
||||||
|
'permissions:call': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/permissions/call', body:['permissions'] },
|
||||||
|
'permissions:attendee': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/attendees/permissions', body:['attendeeId','method','permissions'] },
|
||||||
|
// --- Chat ---
|
||||||
|
'chat:send': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}', body:['message','replyTo','silent','threadTitle','threadId'] },
|
||||||
|
'chat:receive': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}', query:['lookIntoFuture','limit','lastKnownMessageId','timeout','setReadMarker','threadId'] },
|
||||||
|
'chat:delete': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}' },
|
||||||
|
'chat:edit': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}', body:['message'] },
|
||||||
|
'chat:context': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}/context', query:['limit','threadId'] },
|
||||||
|
'chat:reminderSet': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}/reminder', body:['timestamp'] },
|
||||||
|
'chat:reminderGet': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}/reminder' },
|
||||||
|
'chat:reminderDelete': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}/reminder' },
|
||||||
|
'chat:readMarker': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/read', body:['lastReadMessage'] },
|
||||||
|
'chat:unread': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/read' },
|
||||||
|
'chat:pin': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}/pin', body:['pinUntil'] },
|
||||||
|
'chat:unpin': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/{messageId}/pin' },
|
||||||
|
'chat:clearHistory': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}' },
|
||||||
|
'chat:share': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/share', body:['objectType','objectId','metaData'] },
|
||||||
|
'chat:sharedOverview': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/share/overview', query:['limit'] },
|
||||||
|
'chat:sharedObjects': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/share', query:['objectType','limit','lastKnownMessageId'] },
|
||||||
|
'chat:mentions': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/mentions', query:['search','limit','includeStatus'] },
|
||||||
|
'chat:schedule': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/schedule', body:['message','sendAt','replyTo','silent'] },
|
||||||
|
'chat:scheduledGet': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/schedule' },
|
||||||
|
'chat:scheduledDelete': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/schedule/{messageId}' },
|
||||||
|
// --- Call ---
|
||||||
|
'call:peers': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/call/{token}' },
|
||||||
|
'call:join': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/call/{token}', body:['flags','silent','recordingConsent'] },
|
||||||
|
'call:updateFlags': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v4/call/{token}', body:['flags'] },
|
||||||
|
'call:leave': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v4/call/{token}', query:['all'] },
|
||||||
|
'call:ring': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v4/call/{token}/ring/{attendeeId}' },
|
||||||
|
// --- Reaction ---
|
||||||
|
'reaction:add': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/reaction/{token}/{messageId}', body:['reaction'] },
|
||||||
|
'reaction:delete': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/reaction/{token}/{messageId}', query:['reaction'] },
|
||||||
|
'reaction:list': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/reaction/{token}/{messageId}', query:['reaction'] },
|
||||||
|
// --- Poll ---
|
||||||
|
'poll:create': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/poll/{token}', body:['question','options','resultMode','maxVotes','draft','threadId'] },
|
||||||
|
'poll:get': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/poll/{token}/{pollId}' },
|
||||||
|
'poll:vote': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/poll/{token}/{pollId}', body:['optionIds'] },
|
||||||
|
'poll:close': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/poll/{token}/{pollId}' },
|
||||||
|
// --- Ban ---
|
||||||
|
'ban:list': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/ban/{token}' },
|
||||||
|
'ban:add': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/ban/{token}', body:['actorType','actorId','internalNote'] },
|
||||||
|
'ban:remove': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/ban/{token}/{banId}' },
|
||||||
|
// --- Avatar ---
|
||||||
|
'avatar:get': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar' },
|
||||||
|
'avatar:emoji': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar/emoji', body:['emoji','color'] },
|
||||||
|
'avatar:delete': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/room/{token}/avatar' },
|
||||||
|
// --- Breakout rooms ---
|
||||||
|
'breakout:configure': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/breakout-rooms/{token}', body:['mode','amount','attendeeMap'] },
|
||||||
|
'breakout:start': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/breakout-rooms/{token}/rooms' },
|
||||||
|
'breakout:stop': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/breakout-rooms/{token}/rooms' },
|
||||||
|
'breakout:remove': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/breakout-rooms/{token}' },
|
||||||
|
'breakout:broadcast': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/breakout-rooms/{token}/broadcast', body:['message'] },
|
||||||
|
'breakout:list': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/breakout-rooms' },
|
||||||
|
// --- Recording ---
|
||||||
|
'recording:start': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/recording/{token}', body:['status'] },
|
||||||
|
'recording:stop': { method:'DELETE', path:'/ocs/v2.php/apps/spreed/api/v1/recording/{token}' },
|
||||||
|
// --- Signaling ---
|
||||||
|
'signaling:settings': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v3/signaling/settings', query:['token'] },
|
||||||
|
// --- Files integration ---
|
||||||
|
'file:getRoomByFileId': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/file/{fileId}' },
|
||||||
|
'file:getRoomByShare': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/publicshare/{shareToken}' },
|
||||||
|
// --- Thread ---
|
||||||
|
'thread:recent': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/threads/recent', query:['limit'] },
|
||||||
|
'thread:subscribed': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/subscribed-threads', query:['limit','offset'] },
|
||||||
|
'thread:get': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/threads/{threadId}' },
|
||||||
|
'thread:rename': { method:'PUT', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/threads/{threadId}', body:['threadTitle'] },
|
||||||
|
'thread:notify': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/chat/{token}/threads/{messageId}/notify', body:['level'] },
|
||||||
|
// --- User settings ---
|
||||||
|
'settings:set': { method:'POST', path:'/ocs/v2.php/apps/spreed/api/v1/settings/user', body:['key','value'] },
|
||||||
|
// --- Calendar integration ---
|
||||||
|
'calendar:dashboard': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/dashboard/events' },
|
||||||
|
'calendar:mutual': { method:'GET', path:'/ocs/v2.php/apps/spreed/api/v4/room/{token}/mutual-events' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function TalkNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'room:list';
|
||||||
|
this.token = config.token; this.messageId = config.messageId;
|
||||||
|
this.attendeeId = config.attendeeId; this.pollId = config.pollId;
|
||||||
|
this.banId = config.banId; this.threadId = config.threadId; this.fileId = config.fileId;
|
||||||
|
this.shareToken = config.shareToken;
|
||||||
|
// Body fields
|
||||||
|
this.bodyMessage = config.bodyMessage; this.bodyRoomName = config.bodyRoomName;
|
||||||
|
this.bodyRoomType = config.bodyRoomType; this.bodyLevel = config.bodyLevel;
|
||||||
|
this.bodyReplyTo = config.bodyReplyTo; this.bodySilent = config.bodySilent;
|
||||||
|
this.bodyDescription = config.bodyDescription; this.bodyQuestion = config.bodyQuestion;
|
||||||
|
this.bodyPassword = config.bodyPassword; this.bodyEmoji = config.bodyEmoji;
|
||||||
|
this.bodyTimestamp = config.bodyTimestamp; this.bodyReaction = config.bodyReaction;
|
||||||
|
this.bodyActorType = config.bodyActorType; this.bodyActorId = config.bodyActorId;
|
||||||
|
this.bodyFlags = config.bodyFlags; this.bodyState = config.bodyState;
|
||||||
|
this.bodyKey = config.bodyKey; this.bodyValue = config.bodyValue;
|
||||||
|
this.bodyCalendarUri = config.bodyCalendarUri; this.bodyStart = config.bodyStart;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No Nextcloud configuration node selected"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown operation: " + (msg.operation || node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var path = op.path
|
||||||
|
.replace(/\{token\}/g, msg.token || node.token || '')
|
||||||
|
.replace(/\{messageId\}/g, msg.messageId || node.messageId || '')
|
||||||
|
.replace(/\{attendeeId\}/g, msg.attendeeId || node.attendeeId || '')
|
||||||
|
.replace(/\{pollId\}/g, msg.pollId || node.pollId || '')
|
||||||
|
.replace(/\{banId\}/g, msg.banId || node.banId || '')
|
||||||
|
.replace(/\{threadId\}/g, msg.threadId || node.threadId || '')
|
||||||
|
.replace(/\{fileId\}/g, msg.fileId || node.fileId || '')
|
||||||
|
.replace(/\{shareToken\}/g, msg.shareToken || node.shareToken || '');
|
||||||
|
|
||||||
|
var queryParts = [];
|
||||||
|
if (op.query) {
|
||||||
|
op.query.forEach(function(q) {
|
||||||
|
var v = msg[q] !== undefined ? msg[q] : getField(node, q);
|
||||||
|
if (v !== undefined && v !== '') queryParts.push(encodeURIComponent(q) + '=' + encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var fullUrl = baseUrl + path + (queryParts.length ? '?' + queryParts.join('&') : '');
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = {
|
||||||
|
'OCS-APIRequest': 'true', 'Accept': 'application/json',
|
||||||
|
'Authorization': 'Basic ' + Buffer.from(username + ':' + password).toString('base64')
|
||||||
|
};
|
||||||
|
|
||||||
|
var bodyObj = {};
|
||||||
|
if (op.body) {
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var v = msg[b] !== undefined ? msg[b] : getField(node, b);
|
||||||
|
if (v !== undefined && v !== '') bodyObj[b] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Handle special array fields
|
||||||
|
if (msg.options && Array.isArray(msg.options)) bodyObj.options = msg.options;
|
||||||
|
if (msg.optionIds && Array.isArray(msg.optionIds)) bodyObj.optionIds = msg.optionIds;
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (Object.keys(bodyObj).length > 0) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
bodyStr = JSON.stringify(bodyObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
var opts = {
|
||||||
|
hostname: parsedUrl.hostname,
|
||||||
|
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: parsedUrl.path, method: op.method, headers: headers, rejectUnauthorized: false
|
||||||
|
};
|
||||||
|
|
||||||
|
node.log(op.method + ' ' + fullUrl);
|
||||||
|
|
||||||
|
var transport = parsedUrl.protocol === 'https:' ? https : http;
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body = '';
|
||||||
|
res.on('data', function(chunk) { body += chunk; });
|
||||||
|
res.on('end', function() {
|
||||||
|
msg.statusCode = res.statusCode; msg.operation = msg.operation || node.operation;
|
||||||
|
try { msg.payload = JSON.parse(body); } catch(e) { msg.payload = body; }
|
||||||
|
node.send(msg);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
req.on('error', function(err) { msg.error = err.message; node.error(op.method + ' ' + fullUrl + ' — ' + err.message, msg); });
|
||||||
|
if (bodyStr) req.write(bodyStr);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getField(node, name) {
|
||||||
|
var m = { 'message': node.bodyMessage, 'roomName': node.bodyRoomName, 'roomType': node.bodyRoomType,
|
||||||
|
'level': node.bodyLevel, 'replyTo': node.bodyReplyTo, 'silent': node.bodySilent,
|
||||||
|
'description': node.bodyDescription, 'question': node.bodyQuestion, 'password': node.bodyPassword,
|
||||||
|
'emoji': node.bodyEmoji, 'timestamp': node.bodyTimestamp, 'reaction': node.bodyReaction,
|
||||||
|
'actorType': node.bodyActorType, 'actorId': node.bodyActorId, 'flags': node.bodyFlags,
|
||||||
|
'state': node.bodyState, 'key': node.bodyKey, 'value': node.bodyValue,
|
||||||
|
'calendarUri': node.bodyCalendarUri, 'start': node.bodyStart, 'threadTitle': node.bodyThreadTitle,
|
||||||
|
'sendAt': node.bodySendAt, 'pinUntil': node.bodyPinUntil, 'mode': node.bodyMode,
|
||||||
|
'amount': node.bodyAmount, 'status': node.bodyStatus };
|
||||||
|
return m[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
RED.nodes.registerType("talk", TalkNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('userstatus',{category:'nextcloud',color:'#55AAFF',
|
||||||
|
defaults:{name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"status:my"},userId:{value:""},messageId:{value:""}},
|
||||||
|
inputs:1,outputs:1,icon:"userstatus.svg",label:function(){return this.name||"User Status";}});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="userstatus">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="User Status"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<option value="status:my">My Status</option><option value="status:set">Set Status Type</option>
|
||||||
|
<option value="status:list">List All Statuses</option><option value="status:find">Find User Status</option>
|
||||||
|
<option value="status:predefined">Predefined Statuses</option><option value="status:setPredefined">Set Predefined Msg</option>
|
||||||
|
<option value="status:setCustom">Set Custom Msg</option><option value="status:clear">Clear Message</option>
|
||||||
|
<option value="status:revert">Revert Status</option><option value="status:heartbeat">Heartbeat</option>
|
||||||
|
</select></div>
|
||||||
|
<hr><div class="form-row"><label>User ID</label><input type="text" id="node-input-userId" placeholder="msg.userId"></div>
|
||||||
|
<div class="form-row"><label>Message ID</label><input type="text" id="node-input-messageId" placeholder="msg.messageId"></div>
|
||||||
|
<div class="form-tips"><p>Status types: <code>online</code>, <code>away</code>, <code>dnd</code>, <code>busy</code>, <code>offline</code>, <code>invisible</code>.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
'status:my': { method:'GET', path:'/ocs/v2.php/apps/user_status/api/v1/user_status' },
|
||||||
|
'status:set': { method:'PUT', path:'/ocs/v2.php/apps/user_status/api/v1/user_status/status', body:['statusType'] },
|
||||||
|
'status:predefined': { method:'GET', path:'/ocs/v2.php/apps/user_status/api/v1/predefined_statuses' },
|
||||||
|
'status:setPredefined':{ method:'PUT', path:'/ocs/v2.php/apps/user_status/api/v1/user_status/message/predefined', body:['messageId','clearAt'] },
|
||||||
|
'status:setCustom': { method:'PUT', path:'/ocs/v2.php/apps/user_status/api/v1/user_status/message/custom', body:['statusIcon','message','clearAt'] },
|
||||||
|
'status:clear': { method:'DELETE', path:'/ocs/v2.php/apps/user_status/api/v1/user_status/message' },
|
||||||
|
'status:revert': { method:'DELETE', path:'/ocs/v2.php/apps/user_status/api/v1/user_status/revert/{messageId}' },
|
||||||
|
'status:heartbeat': { method:'PUT', path:'/ocs/v2.php/apps/user_status/api/v1/heartbeat', body:['status'] },
|
||||||
|
'status:list': { method:'GET', path:'/ocs/v2.php/apps/user_status/api/v1/statuses', query:['limit','offset'] },
|
||||||
|
'status:find': { method:'GET', path:'/ocs/v2.php/apps/user_status/api/v1/statuses/{userId}' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function USNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'status:my';
|
||||||
|
this.userId = config.userId; this.messageId = config.messageId;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No NC config node"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown: "+(msg.operation||node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl||'').replace(/\/+$/,'');
|
||||||
|
var uname = node.configNode.credentials.username;
|
||||||
|
var pw = node.configNode.credentials.password;
|
||||||
|
var path = op.path
|
||||||
|
.replace(/\{userId\}/g, msg.userId||node.userId||'')
|
||||||
|
.replace(/\{messageId\}/g, msg.messageId||node.messageId||'');
|
||||||
|
|
||||||
|
var qp = [];
|
||||||
|
if (op.query) op.query.forEach(function(q) {
|
||||||
|
var v = msg[q]!==undefined ? msg[q] : '';
|
||||||
|
if (v!==undefined&&v!=='') qp.push(encodeURIComponent(q)+'='+encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
var fu = baseUrl+path+(qp.length?'?'+qp.join('&'):'');
|
||||||
|
var pu = url.parse(fu);
|
||||||
|
|
||||||
|
var h = {'OCS-APIRequest':'true','Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(uname+':'+pw).toString('base64')};
|
||||||
|
|
||||||
|
var bo={};
|
||||||
|
if (op.body) op.body.forEach(function(b){
|
||||||
|
var v=msg[b]!==undefined?msg[b]:'';
|
||||||
|
if(v!==undefined&&v!=='') bo[b]=v;
|
||||||
|
});
|
||||||
|
var bs=null;
|
||||||
|
if (Object.keys(bo).length>0) {h['Content-Type']='application/json'; bs=JSON.stringify(bo);}
|
||||||
|
|
||||||
|
var o = {hostname:pu.hostname,port:pu.port||(pu.protocol==='https:'?443:80),
|
||||||
|
path:pu.path,method:op.method,headers:h,rejectUnauthorized:false};
|
||||||
|
node.log(op.method+' '+fu);
|
||||||
|
var t = pu.protocol==='https:'?https:http;
|
||||||
|
var r = t.request(o,function(res){
|
||||||
|
var b='';res.on('data',function(c){b+=c;});
|
||||||
|
res.on('end',function(){msg.statusCode=res.statusCode;msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(b);}catch(e){msg.payload=b;}node.send(msg);});
|
||||||
|
});
|
||||||
|
r.on('error',function(e){msg.error=e.message;node.error(op.method+' '+fu+' - '+e.message,msg);});
|
||||||
|
if(bs) r.write(bs);
|
||||||
|
r.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("userstatus", USNode);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<script type="text/javascript">
|
||||||
|
RED.nodes.registerType('webhooks', {
|
||||||
|
category: 'nextcloud', color: '#AA44CC',
|
||||||
|
defaults: {
|
||||||
|
name:{value:""},nextcloud:{type:"nextcloud-config",required:true},operation:{value:"webhook:list"},
|
||||||
|
webhookId:{value:""},appId:{value:""},
|
||||||
|
bodyHttpMethod:{value:""},bodyUri:{value:""},bodyEvent:{value:""},
|
||||||
|
bodyEventFilter:{value:""},bodyUserIdFilter:{value:""},bodyHeaders:{value:""},
|
||||||
|
bodyAuthMethod:{value:""},bodyAuthData:{value:""},bodyTokenNeeded:{value:""}
|
||||||
|
},
|
||||||
|
inputs:1,outputs:1,icon:"webhook.svg",
|
||||||
|
label:function(){return this.name||"Webhooks";}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<script type="text/html" data-template-name="webhooks">
|
||||||
|
<div class="form-row"><label for="node-input-name"><i class="fa fa-tag"></i> Name</label><input type="text" id="node-input-name" placeholder="Webhooks"></div>
|
||||||
|
<div class="form-row"><label for="node-input-nextcloud"><i class="fa fa-cloud"></i> Config</label><input type="text" id="node-input-nextcloud" placeholder="Select config node"></div>
|
||||||
|
<div class="form-row"><label for="node-input-operation"><i class="fa fa-cog"></i> Operation</label>
|
||||||
|
<select id="node-input-operation" style="width:100%;">
|
||||||
|
<option value="webhook:list">List Webhooks</option><option value="webhook:get">Get Webhook</option>
|
||||||
|
<option value="webhook:create">Create Webhook</option><option value="webhook:update">Update Webhook</option>
|
||||||
|
<option value="webhook:delete">Delete Webhook</option><option value="webhook:deleteByApp">Delete by App ID</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Path Params</div>
|
||||||
|
<div class="form-row"><label>Webhook ID</label><input type="text" id="node-input-webhookId" placeholder="msg.webhookId"></div>
|
||||||
|
<div class="form-row"><label>App ID</label><input type="text" id="node-input-appId" placeholder="msg.appId"></div>
|
||||||
|
<hr><div style="font-weight:bold;margin-bottom:6px;">Create/Update Fields</div>
|
||||||
|
<div class="form-row"><label>HTTP Method</label><input type="text" id="node-input-bodyHttpMethod" placeholder="GET/POST/PUT/DELETE"></div>
|
||||||
|
<div class="form-row"><label>URI</label><input type="text" id="node-input-bodyUri" placeholder="https://..."></div>
|
||||||
|
<div class="form-row"><label>Event</label><input type="text" id="node-input-bodyEvent" placeholder="Event class name"></div>
|
||||||
|
<div class="form-row"><label>Auth Method</label><select id="node-input-bodyAuthMethod"><option value="">None</option><option value="none">none</option><option value="header">header</option></select></div>
|
||||||
|
<div class="form-tips"><p>Complex fields (eventFilter, headers, authData, tokenNeeded): pass as <code>msg.eventFilter</code> etc. objects.</p></div>
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
module.exports = function(RED) {
|
||||||
|
var https = require('https');
|
||||||
|
var http = require('http');
|
||||||
|
var url = require('url');
|
||||||
|
|
||||||
|
var OPERATIONS = {
|
||||||
|
'webhook:list': { method:'GET', path:'/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks', query:['uri'] },
|
||||||
|
'webhook:get': { method:'GET', path:'/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/{id}' },
|
||||||
|
'webhook:create': { method:'POST', path:'/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks',
|
||||||
|
body:['httpMethod','uri','event','eventFilter','userIdFilter','headers','authMethod','authData','tokenNeeded'] },
|
||||||
|
'webhook:update': { method:'POST', path:'/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/{id}',
|
||||||
|
body:['httpMethod','uri','event','eventFilter','userIdFilter','headers','authMethod','authData','tokenNeeded'] },
|
||||||
|
'webhook:delete': { method:'DELETE', path:'/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/{id}' },
|
||||||
|
'webhook:deleteByApp':{ method:'DELETE', path:'/ocs/v2.php/apps/webhook_listeners/api/v1/webhooks/byappid/{appid}' }
|
||||||
|
};
|
||||||
|
|
||||||
|
function WebhooksNode(config) {
|
||||||
|
RED.nodes.createNode(this, config);
|
||||||
|
var node = this;
|
||||||
|
this.configNode = RED.nodes.getNode(config.nextcloud);
|
||||||
|
this.operation = config.operation || 'webhook:list';
|
||||||
|
this.webhookId = config.webhookId; this.appId = config.appId;
|
||||||
|
this.bodyHttpMethod = config.bodyHttpMethod; this.bodyUri = config.bodyUri;
|
||||||
|
this.bodyEvent = config.bodyEvent; this.bodyEventFilter = config.bodyEventFilter;
|
||||||
|
this.bodyUserIdFilter = config.bodyUserIdFilter; this.bodyHeaders = config.bodyHeaders;
|
||||||
|
this.bodyAuthMethod = config.bodyAuthMethod; this.bodyAuthData = config.bodyAuthData;
|
||||||
|
this.bodyTokenNeeded = config.bodyTokenNeeded;
|
||||||
|
|
||||||
|
if (!this.configNode) { node.error("No Nextcloud configuration node selected"); return; }
|
||||||
|
|
||||||
|
node.on('input', function(msg) {
|
||||||
|
var op = OPERATIONS[msg.operation || node.operation];
|
||||||
|
if (!op) { node.error("Unknown operation: " + (msg.operation || node.operation), msg); return; }
|
||||||
|
|
||||||
|
var baseUrl = (node.configNode.baseUrl || '').replace(/\/+$/, '');
|
||||||
|
var username = node.configNode.credentials.username;
|
||||||
|
var password = node.configNode.credentials.password;
|
||||||
|
|
||||||
|
var path = op.path
|
||||||
|
.replace('{id}', msg.webhookId || node.webhookId || '')
|
||||||
|
.replace('{appid}', msg.appId || node.appId || '');
|
||||||
|
|
||||||
|
var queryParts = [];
|
||||||
|
if (op.query) {
|
||||||
|
op.query.forEach(function(q) {
|
||||||
|
var v = msg[q] !== undefined ? msg[q] : getField(node, q);
|
||||||
|
if (v !== undefined && v !== '') queryParts.push(encodeURIComponent(q) + '=' + encodeURIComponent(v));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var fullUrl = baseUrl + path + (queryParts.length ? '?' + queryParts.join('&') : '');
|
||||||
|
var parsedUrl = url.parse(fullUrl);
|
||||||
|
|
||||||
|
var headers = { 'OCS-APIRequest':'true','Accept':'application/json',
|
||||||
|
'Authorization':'Basic '+Buffer.from(username+':'+password).toString('base64') };
|
||||||
|
|
||||||
|
var bodyObj = {};
|
||||||
|
if (op.body) {
|
||||||
|
op.body.forEach(function(b) {
|
||||||
|
var v = msg[b] !== undefined ? msg[b] : getField(node, b);
|
||||||
|
if (v !== undefined && v !== '') bodyObj[b] = v;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (msg.eventFilter) bodyObj.eventFilter = msg.eventFilter;
|
||||||
|
if (msg.headers && typeof msg.headers === 'object') bodyObj.headers = msg.headers;
|
||||||
|
if (msg.authData) bodyObj.authData = msg.authData;
|
||||||
|
if (msg.tokenNeeded) bodyObj.tokenNeeded = msg.tokenNeeded;
|
||||||
|
|
||||||
|
var bodyStr = null;
|
||||||
|
if (Object.keys(bodyObj).length > 0) { headers['Content-Type']='application/json'; bodyStr=JSON.stringify(bodyObj); }
|
||||||
|
|
||||||
|
var opts = { hostname:parsedUrl.hostname, port:parsedUrl.port||(parsedUrl.protocol==='https:'?443:80),
|
||||||
|
path:parsedUrl.path, method:op.method, headers:headers, rejectUnauthorized:false };
|
||||||
|
|
||||||
|
node.log(op.method+' '+fullUrl);
|
||||||
|
var transport = parsedUrl.protocol==='https:'?https:http;
|
||||||
|
var req = transport.request(opts, function(res) {
|
||||||
|
var body=''; res.on('data',function(c){body+=c;});
|
||||||
|
res.on('end',function(){ msg.statusCode=res.statusCode; msg.operation=msg.operation||node.operation;
|
||||||
|
try{msg.payload=JSON.parse(body);}catch(e){msg.payload=body;} node.send(msg); });
|
||||||
|
});
|
||||||
|
req.on('error',function(err){msg.error=err.message;node.error(op.method+' '+fullUrl+' — '+err.message,msg);});
|
||||||
|
if(bodyStr) req.write(bodyStr);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function getField(node,name) {
|
||||||
|
var m={ 'httpMethod':node.bodyHttpMethod,'uri':node.bodyUri,'event':node.bodyEvent,'uri':node.bodyUri };
|
||||||
|
return m[name];
|
||||||
|
}
|
||||||
|
RED.nodes.registerType("webhooks", WebhooksNode);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user