Zammad API nodes for Node-RED
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "node-red-contrib-nextcloud-zammad",
|
||||
"version": "1.0.0",
|
||||
"description": "Node-RED nodes for Zammad API integration",
|
||||
"keywords": ["node-red", "zammad", "ticketing", "support"],
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://kode.hannesahlin.se/n3wk/nextcloud-nodered-zammad"
|
||||
},
|
||||
"node-red": {
|
||||
"nodes": {
|
||||
"zammad-config": "zammad-config.js",
|
||||
"zammad-api": "zammad-api.js",
|
||||
"zammad-ticket": "zammad-ticket.js",
|
||||
"zammad-user": "zammad-user.js",
|
||||
"zammad-organization": "zammad-organization.js"
|
||||
}
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("zammad-api", {
|
||||
category: "Zammad",
|
||||
color: "#3e8e41",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
config: { type: "zammad-config", required: true }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-exchange",
|
||||
label: function () {
|
||||
return this.name || "Zammad API";
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
},
|
||||
oneditprepare: function () {
|
||||
$("#node-input-config").val(this.config ? this.config.id : "");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="zammad-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="Zammad API">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
|
||||
<input type="text" id="node-input-config">
|
||||
</div>
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
|
||||
Override via message properties:
|
||||
<ul style="margin:4px 0; padding-left:18px;">
|
||||
<li><b>msg.method</b> – GET, POST, PUT, DELETE</li>
|
||||
<li><b>msg.endpoint</b> – /tickets, /users, etc.</li>
|
||||
<li><b>msg.payload</b> – request body (object or JSON string)</li>
|
||||
<li><b>msg.headers</b> – additional headers object</li>
|
||||
</ul>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="zammad-api">
|
||||
<p>Generic Zammad API caller. Sends any HTTP method to any endpoint.</p>
|
||||
<h3>Inputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.method<span class="property-type">string</span></dt><dd>HTTP method (GET, POST, PUT, DELETE). Default: GET</dd>
|
||||
<dt>msg.endpoint<span class="property-type">string</span></dt><dd>API path e.g. /tickets, /users/search</dd>
|
||||
<dt>msg.payload<span class="property-type">object|string</span></dt><dd>Request body</dd>
|
||||
<dt>msg.headers<span class="property-type">object</span></dt><dd>Additional HTTP headers</dd>
|
||||
</dl>
|
||||
<h3>Outputs</h3>
|
||||
<dl class="message-properties">
|
||||
<dt>msg.payload<span class="property-type">object</span></dt><dd>Parsed JSON response body</dd>
|
||||
<dt>msg.statusCode<span class="property-type">number</span></dt><dd>HTTP status code</dd>
|
||||
</dl>
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
function ZammadApiNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.configNode = RED.nodes.getNode(config.config);
|
||||
if (!node.configNode) {
|
||||
node.error("No zammad-config node configured");
|
||||
return;
|
||||
}
|
||||
|
||||
node.status({ fill: "blue", shape: "ring", text: "ready" });
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
const method = (msg.method || "GET").toUpperCase();
|
||||
const endpoint = msg.endpoint || msg.path || "/";
|
||||
const body = msg.payload !== undefined ? msg.payload : msg.body;
|
||||
const extraHeaders = msg.headers || {};
|
||||
|
||||
node.status({ fill: "yellow", shape: "dot", text: method + " " + endpoint });
|
||||
|
||||
node.configNode.sendRequest(method, endpoint, body, extraHeaders, function (err, result) {
|
||||
if (err) {
|
||||
node.status({ fill: "red", shape: "ring", text: "error" });
|
||||
if (done) done(err);
|
||||
else node.error(err, msg);
|
||||
return;
|
||||
}
|
||||
|
||||
msg.payload = result.body;
|
||||
msg.statusCode = result.statusCode;
|
||||
msg.zammad = result;
|
||||
|
||||
node.status({ fill: "green", shape: "dot", text: result.statusCode });
|
||||
send(msg);
|
||||
if (done) done();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("zammad-api", ZammadApiNode);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("zammad-config", {
|
||||
category: "config",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
baseUrl: { value: "https://support.hanssonit.se/api/v1", required: true },
|
||||
useToken: { value: true }
|
||||
},
|
||||
credentials: {
|
||||
token: { type: "text" },
|
||||
username: { type: "text" },
|
||||
password: { type: "password" }
|
||||
},
|
||||
label: function () {
|
||||
return this.name || this.baseUrl || "Zammad";
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="zammad-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="Zammad Config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-baseUrl"><i class="fa fa-server"></i> Base URL</label>
|
||||
<input type="text" id="node-config-input-baseUrl" placeholder="https://support.hanssonit.se/api/v1">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-useToken"><i class="fa fa-lock"></i> Use Token Auth</label>
|
||||
<input type="checkbox" id="node-config-input-useToken" checked style="width:auto;">
|
||||
</div>
|
||||
<div id="zammad-token-section">
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-token"><i class="fa fa-key"></i> API Token</label>
|
||||
<input type="text" id="node-config-input-token" autocomplete="off">
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:4px;">
|
||||
Create token at https://support.hanssonit.se while logged in.<br>
|
||||
Required permission: <b>ticket.agent</b>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="zammad-basic-section" style="display:none;">
|
||||
<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="Hannes">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-config-input-password"><i class="fa fa-key"></i> Password</label>
|
||||
<input type="password" id="node-config-input-password" autocomplete="off">
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="zammad-config">
|
||||
<p>Zammad API configuration node.</p>
|
||||
<h3>Details</h3>
|
||||
<p>Configure your Zammad instance connection.</p>
|
||||
<ul>
|
||||
<li><b>Base URL</b> – Must be <code>https://support.hanssonit.se/api/v1</code> (do not use zammad.kafit.se).</li>
|
||||
<li><b>Token Auth</b> – Preferred. Create token on support.hanssonit.se with <code>ticket.agent</code> permission.</li>
|
||||
<li><b>Basic Auth</b> – Username is typically just <code>Hannes</code> (not full email).</li>
|
||||
</ul>
|
||||
</script>
|
||||
@@ -0,0 +1,59 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
const https = require("https");
|
||||
const http = require("http");
|
||||
|
||||
function ZammadConfigNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.baseUrl = (config.baseUrl || "https://support.hanssonit.se/api/v1").replace(/\/+$/, "");
|
||||
node.useToken = config.useToken !== false;
|
||||
node.token = config.token || "";
|
||||
node.username = config.username || "";
|
||||
node.password = config.password || "";
|
||||
|
||||
node.sendRequest = function (method, path, body, msgHeaders, callback) {
|
||||
const url = new URL(node.baseUrl + path);
|
||||
const parsedBody = body !== undefined ? (typeof body === "string" ? body : JSON.stringify(body)) : undefined;
|
||||
|
||||
const headers = Object.assign({ "Accept": "application/json", "Content-Type": "application/json" }, msgHeaders || {});
|
||||
if (parsedBody !== undefined) headers["Content-Length"] = Buffer.byteLength(parsedBody);
|
||||
|
||||
if (node.useToken && node.token) {
|
||||
headers["Authorization"] = "Token token=" + node.token;
|
||||
} else if (node.username && node.password) {
|
||||
headers["Authorization"] = "Basic " + Buffer.from(node.username + ":" + node.password).toString("base64");
|
||||
}
|
||||
|
||||
const options = {
|
||||
method, hostname: url.hostname,
|
||||
port: url.port || (url.protocol === "https:" ? 443 : 80),
|
||||
path: url.pathname + url.search, headers, rejectUnauthorized: false
|
||||
};
|
||||
|
||||
const req = (url.protocol === "https:" ? https : http).request(options, function (res) {
|
||||
let data = "";
|
||||
res.on("data", chunk => data += chunk);
|
||||
res.on("end", () => {
|
||||
let parsed = null;
|
||||
if (data) { try { parsed = JSON.parse(data); } catch (e) { parsed = data; } }
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
callback(null, { statusCode: res.statusCode, body: parsed, headers: res.headers });
|
||||
} else {
|
||||
const err = new Error("Zammad API returned status " + res.statusCode + ": " + data);
|
||||
err.statusCode = res.statusCode;
|
||||
err.body = parsed;
|
||||
callback(err);
|
||||
}
|
||||
});
|
||||
});
|
||||
req.on("error", callback);
|
||||
if (parsedBody !== undefined) req.write(parsedBody);
|
||||
req.end();
|
||||
};
|
||||
}
|
||||
|
||||
RED.nodes.registerType("zammad-config", ZammadConfigNode);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("zammad-organization", {
|
||||
category: "Zammad",
|
||||
color: "#3e8e41",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
config: { type: "zammad-config", required: true },
|
||||
operation: { value: "list" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-building",
|
||||
label: function () {
|
||||
return this.name || ("Zammad Org " + this.operation);
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
},
|
||||
oneditprepare: function () {
|
||||
$("#node-input-config").val(this.config ? this.config.id : "");
|
||||
},
|
||||
oneditsave: function () {
|
||||
this.operation = $("#node-select-operation").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="zammad-organization">
|
||||
<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="Zammad Organization">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
|
||||
<input type="text" id="node-input-config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
|
||||
<select id="node-select-operation">
|
||||
<option value="list">List Organizations</option>
|
||||
<option value="search">Search Organizations</option>
|
||||
<option value="get">Get Organization by ID</option>
|
||||
<option value="create">Create Organization</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
|
||||
<b>Search</b>: msg.query – e.g. <code>name:LRG Invest AB</code><br>
|
||||
<b>Create</b>: msg.name, msg.note, msg.street, msg.zip, msg.city, msg.country
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="zammad-organization">
|
||||
<p>Zammad organization operations: list, search, get, create.</p>
|
||||
</script>
|
||||
@@ -0,0 +1,83 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
function ZammadOrganizationNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.configNode = RED.nodes.getNode(config.config);
|
||||
if (!node.configNode) {
|
||||
node.error("No zammad-config node configured");
|
||||
return;
|
||||
}
|
||||
|
||||
node.operation = config.operation || "list";
|
||||
|
||||
node.status({ fill: "blue", shape: "ring", text: "ready" });
|
||||
|
||||
function doRequest(method, path, body, msg, done) {
|
||||
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
|
||||
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
|
||||
if (err) {
|
||||
node.status({ fill: "red", shape: "ring", text: "error" });
|
||||
if (done) done(err);
|
||||
else node.error(err, msg);
|
||||
return;
|
||||
}
|
||||
msg.payload = result.body;
|
||||
msg.statusCode = result.statusCode;
|
||||
msg.zammad = result;
|
||||
node.status({ fill: "green", shape: "dot", text: result.statusCode });
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
const op = msg.operation || node.operation;
|
||||
|
||||
switch (op) {
|
||||
case "list": {
|
||||
const params = new URLSearchParams();
|
||||
if (msg.limit) params.append("per_page", msg.limit);
|
||||
if (msg.page) params.append("page", msg.page);
|
||||
const qs = params.toString();
|
||||
doRequest("GET", "/organizations" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "search": {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (msg.query) searchParams.append("query", msg.query);
|
||||
if (msg.limit) searchParams.append("per_page", msg.limit);
|
||||
const qs = searchParams.toString();
|
||||
doRequest("GET", "/organizations/search" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
const id = msg.organizationId || msg.payload;
|
||||
if (!id) { node.error("Missing organizationId"); if (done) done(new Error("Missing organizationId")); return; }
|
||||
doRequest("GET", "/organizations/" + encodeURIComponent(id), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "create": {
|
||||
const body = msg.payload && typeof msg.payload === "object" ? msg.payload : {
|
||||
name: msg.name || "",
|
||||
note: msg.note || "",
|
||||
street: msg.street || "",
|
||||
zip: msg.zip || "",
|
||||
city: msg.city || "",
|
||||
country: msg.country || ""
|
||||
};
|
||||
doRequest("POST", "/organizations", body, msg, done);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const err = new Error("Unknown operation: " + op);
|
||||
node.error(err);
|
||||
if (done) done(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("zammad-organization", ZammadOrganizationNode);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("zammad-ticket", {
|
||||
category: "Zammad",
|
||||
color: "#3e8e41",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
config: { type: "zammad-config", required: true },
|
||||
operation: { value: "list" },
|
||||
topic: { value: "" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-ticket",
|
||||
label: function () {
|
||||
return this.name || ("Zammad " + this.operation);
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
},
|
||||
oneditprepare: function () {
|
||||
$("#node-input-config").val(this.config ? this.config.id : "");
|
||||
var op = this.operation || "list";
|
||||
$("#node-select-operation").val(op);
|
||||
$("#node-input-topic").parent().toggle(op === "subscribe" || op === "publish");
|
||||
},
|
||||
oneditsave: function () {
|
||||
this.operation = $("#node-select-operation").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="zammad-ticket">
|
||||
<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="Zammad Ticket">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
|
||||
<input type="text" id="node-input-config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
|
||||
<select id="node-select-operation">
|
||||
<option value="list">List Tickets</option>
|
||||
<option value="search">Search Tickets</option>
|
||||
<option value="get">Get Ticket</option>
|
||||
<option value="create">Create Ticket</option>
|
||||
<option value="update">Update Ticket</option>
|
||||
<option value="articles">List Articles</option>
|
||||
<option value="addArticle">Add Article</option>
|
||||
<option value="tags">List Tags</option>
|
||||
<option value="addTag">Add Tag</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
|
||||
<b>Create</b>: Set msg.title, msg.customer, msg.body, msg.group, msg.state, msg.priority —
|
||||
or send a full JSON payload object with those fields.<br>
|
||||
<b>Search</b>: Zammad query syntax e.g. <code>state.name:open</code>,
|
||||
<code>customer.email:foo@example.com</code>, <code>title:order</code><br>
|
||||
<b>addArticle</b>: msg.articleBody, msg.sender ("Agent"/"Customer"), msg.from (email for customer articles)<br>
|
||||
<b>addTag</b>: msg.tag — e.g. "woocommerce-order"
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="zammad-ticket">
|
||||
<p>Zammad ticket operations: list, search, get, create, update, articles, tags.</p>
|
||||
<h3>Operations</h3>
|
||||
<ul>
|
||||
<li><b>list</b> – List all tickets (optional msg.query, msg.limit, msg.page)</li>
|
||||
<li><b>search</b</b> – Search via Zammad query syntax (msg.query)</li>
|
||||
<li><b>get</b> – Get single ticket by msg.ticketId</li>
|
||||
<li><b>create</b> – Create from individual properties or full JSON payload</li>
|
||||
<li><b>update</b> – Update existing ticket (msg.ticketId)</li>
|
||||
<li><b>articles</b> – List ticket articles (msg.ticketId)</li>
|
||||
<li><b>addArticle</b> – Add article to ticket (msg.ticketId, msg.articleBody, msg.sender)</li>
|
||||
<li><b>tags</b> – List ticket tags (msg.ticketId)</li>
|
||||
<li><b>addTag</b> – Add tag to ticket (msg.ticketId, msg.tag)</li>
|
||||
</ul>
|
||||
</script>
|
||||
@@ -0,0 +1,117 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
function ZammadTicketNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.configNode = RED.nodes.getNode(config.config);
|
||||
if (!node.configNode) { node.error("No zammad-config node configured"); return; }
|
||||
|
||||
node.operation = config.operation || "list";
|
||||
node.status({ fill: "blue", shape: "ring", text: "ready" });
|
||||
|
||||
function doRequest(method, path, body, msg, done) {
|
||||
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
|
||||
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
|
||||
if (err) { node.status({ fill: "red", shape: "ring", text: "error" }); done ? done(err) : node.error(err, msg); return; }
|
||||
msg.payload = result.body;
|
||||
msg.statusCode = result.statusCode;
|
||||
msg.zammad = result;
|
||||
node.status({ fill: "green", shape: "dot", text: result.statusCode });
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
function buildTicketPayload(msg) {
|
||||
if (msg.payload && typeof msg.payload === "object" && msg.payload.title) return msg.payload;
|
||||
const p = {};
|
||||
if (msg.title !== undefined) p.title = msg.title;
|
||||
if (msg.customer !== undefined) p.customer = msg.customer;
|
||||
if (msg.group !== undefined) p.group = msg.group;
|
||||
if (msg.state !== undefined) p.state = msg.state;
|
||||
if (msg.priority !== undefined) p.priority = msg.priority;
|
||||
if (msg.body !== undefined || msg.articleBody !== undefined) {
|
||||
p.article = { body: msg.body || msg.articleBody, from: msg.from || "System", to: msg.to || "", sender: msg.sender || "Agent", type: msg.articleType || "note" };
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
const op = msg.operation || node.operation;
|
||||
switch (op) {
|
||||
case "list": {
|
||||
const p = new URLSearchParams();
|
||||
if (msg.query) p.append("query", msg.query);
|
||||
if (msg.limit) p.append("per_page", msg.limit);
|
||||
if (msg.page) p.append("page", msg.page);
|
||||
const qs = p.toString();
|
||||
doRequest("GET", "/tickets" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "search": {
|
||||
const p = new URLSearchParams();
|
||||
if (msg.query) p.append("query", msg.query);
|
||||
if (msg.limit) p.append("per_page", msg.limit);
|
||||
const qs = p.toString();
|
||||
doRequest("GET", "/tickets/search" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
const id = msg.ticketId || msg.payload;
|
||||
if (!id) { node.error("Missing ticketId"); done && done(new Error("Missing ticketId")); return; }
|
||||
doRequest("GET", "/tickets/" + encodeURIComponent(id), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "create": {
|
||||
doRequest("POST", "/tickets", buildTicketPayload(msg), msg, done);
|
||||
break;
|
||||
}
|
||||
case "update": {
|
||||
const id = msg.ticketId || (msg.payload && msg.payload.id);
|
||||
if (!id) { node.error("Missing ticketId"); done && done(new Error("Missing ticketId")); return; }
|
||||
doRequest("PUT", "/tickets/" + encodeURIComponent(id),
|
||||
msg.payload && typeof msg.payload === "object" && msg.payload.title ? msg.payload : buildTicketPayload(msg), msg, done);
|
||||
break;
|
||||
}
|
||||
case "articles": {
|
||||
const id = msg.ticketId || (msg.payload && msg.payload.id);
|
||||
if (!id) { node.error("Missing ticketId"); done && done(new Error("Missing ticketId")); return; }
|
||||
doRequest("GET", "/ticket_articles/by_ticket/" + encodeURIComponent(id), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "addArticle": {
|
||||
const id = msg.ticketId || (msg.payload && msg.payload.id);
|
||||
if (!id) { node.error("Missing ticketId"); done && done(new Error("Missing ticketId")); return; }
|
||||
const sender = msg.sender || "Agent";
|
||||
doRequest("POST", "/ticket_articles", {
|
||||
body: msg.articleBody || msg.body || (msg.payload && msg.payload.body) || "",
|
||||
from: msg.from || sender, sender, type: msg.articleType || "note",
|
||||
internal: msg.internal !== undefined ? msg.internal : false, contentType: "text/plain"
|
||||
}, msg, done);
|
||||
break;
|
||||
}
|
||||
case "tags": {
|
||||
const id = msg.ticketId || (msg.payload && msg.payload.id);
|
||||
if (!id) { node.error("Missing ticketId"); done && done(new Error("Missing ticketId")); return; }
|
||||
doRequest("GET", "/tickets/" + encodeURIComponent(id) + "/tags", undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "addTag": {
|
||||
const id = msg.ticketId || (msg.payload && msg.payload.id);
|
||||
const tag = msg.tag || msg.tagName;
|
||||
if (!id || !tag) { node.error("Missing ticketId or tag"); done && done(new Error("Missing ticketId or tag")); return; }
|
||||
doRequest("POST", "/tags/add", { tag, object: "Ticket", o_id: id }, msg, done);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const err = new Error("Unknown operation: " + op);
|
||||
node.error(err);
|
||||
if (done) done(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("zammad-ticket", ZammadTicketNode);
|
||||
};
|
||||
@@ -0,0 +1,63 @@
|
||||
<script type="text/javascript">
|
||||
RED.nodes.registerType("zammad-user", {
|
||||
category: "Zammad",
|
||||
color: "#3e8e41",
|
||||
defaults: {
|
||||
name: { value: "" },
|
||||
config: { type: "zammad-config", required: true },
|
||||
operation: { value: "list" }
|
||||
},
|
||||
inputs: 1,
|
||||
outputs: 1,
|
||||
icon: "font-awesome/fa-user",
|
||||
label: function () {
|
||||
return this.name || ("Zammad User " + this.operation);
|
||||
},
|
||||
labelStyle: function () {
|
||||
return this.name ? "node_label_italic" : "";
|
||||
},
|
||||
oneditprepare: function () {
|
||||
$("#node-input-config").val(this.config ? this.config.id : "");
|
||||
},
|
||||
oneditsave: function () {
|
||||
this.operation = $("#node-select-operation").val();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-template-name="zammad-user">
|
||||
<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="Zammad User">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-input-config"><i class="fa fa-server"></i> Config</label>
|
||||
<input type="text" id="node-input-config">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="node-select-operation"><i class="fa fa-wrench"></i> Operation</label>
|
||||
<select id="node-select-operation">
|
||||
<option value="me">Get Current User (me)</option>
|
||||
<option value="list">List Users</option>
|
||||
<option value="search">Search Users</option>
|
||||
<option value="get">Get User by ID</option>
|
||||
<option value="create">Create User</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-tips" style="color:#888; font-size:11px; margin-top:10px;">
|
||||
<b>Search</b>: msg.query – e.g. <code>email:foo@example.com</code><br>
|
||||
<b>Create</b>: msg.firstname, msg.lastname, msg.email, msg.phone, msg.customer (login), msg.group
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-red" data-help-name="zammad-user">
|
||||
<p>Zammad user operations: me, list, search, get, create.</p>
|
||||
<h3>Operations</h3>
|
||||
<ul>
|
||||
<li><b>me</b> – Get currently authenticated user info</li>
|
||||
<li><b>list</b> – List all users (optional msg.limit, msg.page)</li>
|
||||
<li><b>search</b> – Search users by query string (msg.query)</li>
|
||||
<li><b>get</b> – Get user by msg.userId</li>
|
||||
<li><b>create</b> – Create user from individual properties or full JSON payload</li>
|
||||
</ul>
|
||||
</script>
|
||||
@@ -0,0 +1,88 @@
|
||||
module.exports = function (RED) {
|
||||
"use strict";
|
||||
|
||||
function ZammadUserNode(config) {
|
||||
RED.nodes.createNode(this, config);
|
||||
const node = this;
|
||||
|
||||
node.configNode = RED.nodes.getNode(config.config);
|
||||
if (!node.configNode) {
|
||||
node.error("No zammad-config node configured");
|
||||
return;
|
||||
}
|
||||
|
||||
node.operation = config.operation || "list";
|
||||
|
||||
node.status({ fill: "blue", shape: "ring", text: "ready" });
|
||||
|
||||
function doRequest(method, path, body, msg, done) {
|
||||
node.status({ fill: "yellow", shape: "dot", text: method + " " + path });
|
||||
node.configNode.sendRequest(method, path, body, {}, function (err, result) {
|
||||
if (err) {
|
||||
node.status({ fill: "red", shape: "ring", text: "error" });
|
||||
if (done) done(err);
|
||||
else node.error(err, msg);
|
||||
return;
|
||||
}
|
||||
msg.payload = result.body;
|
||||
msg.statusCode = result.statusCode;
|
||||
msg.zammad = result;
|
||||
node.status({ fill: "green", shape: "dot", text: result.statusCode });
|
||||
done();
|
||||
});
|
||||
}
|
||||
|
||||
node.on("input", function (msg, send, done) {
|
||||
const op = msg.operation || node.operation;
|
||||
|
||||
switch (op) {
|
||||
case "me": {
|
||||
doRequest("GET", "/users/me", undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "list": {
|
||||
const params = new URLSearchParams();
|
||||
if (msg.limit) params.append("per_page", msg.limit);
|
||||
if (msg.page) params.append("page", msg.page);
|
||||
const qs = params.toString();
|
||||
doRequest("GET", "/users" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "search": {
|
||||
const searchParams = new URLSearchParams();
|
||||
if (msg.query) searchParams.append("query", msg.query);
|
||||
if (msg.limit) searchParams.append("per_page", msg.limit);
|
||||
const qs = searchParams.toString();
|
||||
doRequest("GET", "/users/search" + (qs ? "?" + qs : ""), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "get": {
|
||||
const id = msg.userId || msg.payload;
|
||||
if (!id) { node.error("Missing userId"); if (done) done(new Error("Missing userId")); return; }
|
||||
doRequest("GET", "/users/" + encodeURIComponent(id), undefined, msg, done);
|
||||
break;
|
||||
}
|
||||
case "create": {
|
||||
const body = msg.payload && typeof msg.payload === "object" ? msg.payload : {
|
||||
firstname: msg.firstname || "",
|
||||
lastname: msg.lastname || "",
|
||||
email: msg.email || "",
|
||||
phone: msg.phone || "",
|
||||
customer: msg.customer || "",
|
||||
group: msg.group || "",
|
||||
note: msg.note || ""
|
||||
};
|
||||
doRequest("POST", "/users", body, msg, done);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
const err = new Error("Unknown operation: " + op);
|
||||
node.error(err);
|
||||
if (done) done(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
RED.nodes.registerType("zammad-user", ZammadUserNode);
|
||||
};
|
||||
Reference in New Issue
Block a user