Initial commit: Nextcloud Node-RED Docker image and custom nodes

This commit is contained in:
newkle3r
2026-05-15 14:50:48 +02:00
commit fd7cc695f7
44 changed files with 3936 additions and 0 deletions
+477
View File
@@ -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="&mdash; Collectives &mdash;">
<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="&mdash; Trash &mdash;">
<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="&mdash; Shares &mdash;">
<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="&mdash; Search &mdash;">
<option value="search:recentPages">Search Recent Pages</option>
<option value="search:content">Search Page Content</option>
</optgroup>
<optgroup label="&mdash; Pages &mdash;">
<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="&mdash; Page Trash &mdash;">
<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="&mdash; Templates &mdash;">
<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="&mdash; Tags &mdash;">
<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="&mdash; User Settings &mdash;">
<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="&mdash; Sessions &mdash;">
<option value="session:create">Create Session</option>
<option value="session:sync">Sync Session</option>
<option value="session:close">Close Session</option>
</optgroup>
<optgroup label="&mdash; Settings &mdash;">
<option value="settings:get">Get User Setting</option>
<option value="settings:set">Set User Setting</option>
</optgroup>
<optgroup label="&mdash; Public (Share Token) &mdash;">
<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>
+303
View File
@@ -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);
};
+34
View File
@@ -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 &amp; 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 &amp; 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>
+167
View File
@@ -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);
};
+29
View File
@@ -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>
+81
View File
@@ -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);
};
+37
View File
@@ -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>
+85
View File
@@ -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);
};
+142
View File
@@ -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>
+170
View File
@@ -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);
};
+20
View File
@@ -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 &amp; 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>
+91
View File
@@ -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);
};
+67
View File
@@ -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>
+116
View File
@@ -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);
};
+37
View File
@@ -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 &amp; sessions.</p>
</div>
</script>
+15
View File
@@ -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" }
}
});
}
+14
View File
@@ -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>
+62
View File
@@ -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);
};
+44
View File
@@ -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>
+84
View File
@@ -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);
}
+26
View File
@@ -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"
}
}
}
+24
View File
@@ -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 &amp; 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>
+116
View File
@@ -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);
};
+15
View File
@@ -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>
+58
View File
@@ -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);
};
+48
View File
@@ -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>
+162
View File
@@ -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);
};
+106
View File
@@ -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 &amp; 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>
+234
View File
@@ -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);
};
+20
View File
@@ -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>
+73
View File
@@ -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);
};
+34
View File
@@ -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>
+91
View File
@@ -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);
};