n8n-nodes-recentreborn 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +49 -0
- package/dist/src/credentials/RecentRebornApi.credentials.d.ts +10 -0
- package/dist/src/credentials/RecentRebornApi.credentials.js +42 -0
- package/dist/src/credentials/recentreborn.svg +15 -0
- package/dist/src/nodes/RecentReborn/RecentReborn.node.d.ts +5 -0
- package/dist/src/nodes/RecentReborn/RecentReborn.node.js +267 -0
- package/dist/src/nodes/RecentReborn/recentreborn.svg +15 -0
- package/dist/src/nodes/RecentRebornTrigger/RecentRebornTrigger.node.d.ts +5 -0
- package/dist/src/nodes/RecentRebornTrigger/RecentRebornTrigger.node.js +167 -0
- package/dist/src/nodes/RecentRebornTrigger/recentreborn.svg +15 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 RecentReborn
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# n8n-nodes-recentreborn
|
|
2
|
+
|
|
3
|
+
Community node for [RecentReborn](https://recentreborn.com), a chronological search engine for Instagram, TikTok, and YouTube. Search recent posts by hashtag, query, or location, or trigger a workflow the moment a new one appears.
|
|
4
|
+
|
|
5
|
+
This package includes two nodes:
|
|
6
|
+
|
|
7
|
+
- **RecentReborn** — an action node with four operations: Search Hashtag (Instagram), Search Query (TikTok/YouTube), Search Location (Instagram), and Look Up Locations (Instagram, to find a `location_id`).
|
|
8
|
+
- **RecentReborn Trigger** — a polling trigger that watches a hashtag, query, or location and emits each new post once, in the order it happened.
|
|
9
|
+
|
|
10
|
+
## Setup
|
|
11
|
+
|
|
12
|
+
1. Install dependencies:
|
|
13
|
+
```bash
|
|
14
|
+
npm install
|
|
15
|
+
```
|
|
16
|
+
2. Run locally with hot reload (this uses the built-in n8n instance from `@n8n/node-cli`, no global n8n install needed):
|
|
17
|
+
```bash
|
|
18
|
+
npm run dev
|
|
19
|
+
```
|
|
20
|
+
3. In the n8n editor, add a **RecentReborn API** credential. Get your API key from **API Key** in the RecentReborn dashboard, and paste it in (it's sent as `Authorization: Bearer <key>`, so no need to type "Bearer" yourself).
|
|
21
|
+
4. Add the **RecentReborn** or **RecentReborn Trigger** node to a workflow and pick your credential.
|
|
22
|
+
|
|
23
|
+
## Notes on the API this wraps
|
|
24
|
+
|
|
25
|
+
- **Base URL**: `https://app.recentreborn.com/api/v1`
|
|
26
|
+
- **Auth**: bearer token, one live key per account, no separate sandbox key. Calls through this node count against your normal daily search quota, same as dashboard searches.
|
|
27
|
+
- **Rate limit**: 20 requests/minute per key, on top of your daily quota. If you're running the trigger on a tight interval across several watched hashtags, keep an eye on `X-RateLimit-Remaining` (returned on every response) so you don't get a `429`.
|
|
28
|
+
- **Pagination**: the action node's "Return All" option follows `next_pagination_token` until the API stops returning one. Leave it off and set a "Limit" if you only want the first page or two.
|
|
29
|
+
- **Location search is Instagram-only.** Use "Look Up Locations" first to turn a place name into a numeric `location_id`, then feed that into "Search Location" or the trigger.
|
|
30
|
+
- **`upload_date` only applies to YouTube** queries, and the API defaults it to `hour` if you don't set it, which is usually too narrow for a scheduled job. Both nodes default it to `today` instead.
|
|
31
|
+
|
|
32
|
+
## How the trigger avoids duplicates
|
|
33
|
+
|
|
34
|
+
RecentReborn returns posts newest-first. On each poll, the trigger node remembers the `posted_at` of the newest post it has already emitted (stored in the node's static workflow data) and only emits posts newer than that, oldest-first. On the very first poll it doesn't emit anything (there's no "last seen" yet, so everything would look new) except when you're manually testing the node in the editor, so you can see a real result shape immediately.
|
|
35
|
+
|
|
36
|
+
This means: don't run two separate workflows watching the *same* hashtag/query/location with the same credential unless you're fine with each one independently tracking its own "last seen" post.
|
|
37
|
+
|
|
38
|
+
## Ideas for what to build with this
|
|
39
|
+
|
|
40
|
+
- Poll a niche hashtag with the trigger, and drop each new post into a Google Sheet, Airtable base, or CRM as a lead/prospect list.
|
|
41
|
+
- Chain **Look Up Locations → Search Location** to monitor a city or neighborhood for real-time posts (events, local business mentions, etc.).
|
|
42
|
+
- Pair the trigger with an AI node (e.g. an LLM step) to auto-draft a comment or DM for each new post, then hold for manual approval before sending.
|
|
43
|
+
- Use the action node's `hashtags`, `caption`, and `author` fields as input to a scoring step, then only route "good fit" authors onward.
|
|
44
|
+
|
|
45
|
+
## Limitations to flag to users
|
|
46
|
+
|
|
47
|
+
- This node calls the public RecentReborn API as documented; it does not expose Discovered Profiles (fetch/export), since those aren't part of the public API today.
|
|
48
|
+
- All error handling assumes the standard RecentReborn error shape (`{ "error": { "code", "message" } }`) for `400`, `401`, `403`, `429`, `500`, and `503`.
|
|
49
|
+
# RecentReborn-n8n
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
export declare class RecentRebornApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
icon: "file:recentreborn.svg";
|
|
6
|
+
documentationUrl: string;
|
|
7
|
+
properties: INodeProperties[];
|
|
8
|
+
authenticate: IAuthenticateGeneric;
|
|
9
|
+
test: ICredentialTestRequest;
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RecentRebornApi = void 0;
|
|
4
|
+
class RecentRebornApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'recentRebornApi';
|
|
7
|
+
this.displayName = 'RecentReborn API';
|
|
8
|
+
this.icon = 'file:recentreborn.svg';
|
|
9
|
+
this.documentationUrl = 'https://recentreborn.com/api-docs';
|
|
10
|
+
this.properties = [
|
|
11
|
+
{
|
|
12
|
+
displayName: 'API Key',
|
|
13
|
+
name: 'apiKey',
|
|
14
|
+
type: 'string',
|
|
15
|
+
typeOptions: { password: true },
|
|
16
|
+
default: '',
|
|
17
|
+
required: true,
|
|
18
|
+
description: 'Your RecentReborn API key (starts with "rr_live_"). Generate one from the API Key section of your RecentReborn dashboard. Sent as a bearer token, and calls made with it count against your normal daily search quota.',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
// RecentReborn expects "Authorization: Bearer <key>" on every request.
|
|
22
|
+
this.authenticate = {
|
|
23
|
+
type: 'generic',
|
|
24
|
+
properties: {
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: '=Bearer {{$credentials.apiKey}}',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
// A cheap, always-available call used by the "Test" button in the credential UI.
|
|
31
|
+
this.test = {
|
|
32
|
+
request: {
|
|
33
|
+
baseURL: 'https://app.recentreborn.com/api/v1',
|
|
34
|
+
url: '/locations',
|
|
35
|
+
qs: {
|
|
36
|
+
query: 'Berlin, Germany',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.RecentRebornApi = RecentRebornApi;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="linear-gradient" y1="1" x2="1" gradientUnits="objectBoundingBox">
|
|
4
|
+
<stop offset="0" stop-color="#c6c6f1"/>
|
|
5
|
+
<stop offset="1" stop-color="#f0d7d6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<g id="Group_14" data-name="Group 14" transform="translate(-33 -17)">
|
|
9
|
+
<rect id="Rectangle_15" data-name="Rectangle 15" width="1024" height="1024" rx="512" transform="translate(33 17)" fill="url(#linear-gradient)"/>
|
|
10
|
+
<g id="Group_13" data-name="Group 13" transform="translate(241.174 277.218)">
|
|
11
|
+
<path id="Path_14" data-name="Path 14" d="M644.262,648c-.1,80.906-.161,160.778-.247,240.651-.019,16.193-1.854,17.879-17.91,16.815-30.435-2.017-60.9-.8-91.352-.681-8.375.034-11.943-1.438-11.9-10.8.384-88.046-.731-176.114.791-264.137,1.121-64.842,28.842-118.8,75.282-163.53a263.707,263.707,0,0,1,54.369-40.425c28.521-15.967,59.219-23.481,91.714-23.8,26.745-.263,53.5.173,80.245-.087,8.372-.082,11.439,2.182,11.283,11.473-.658,39.369-.286,78.755-.355,118.135-.023,13.655-.642,14.1-14.553,14.283-25.96.334-52.078-.921-77.844,1.516C693.816,552.135,648.906,598.209,644.262,648Z" transform="translate(-522.811 -401.999)"/>
|
|
12
|
+
<path id="Path_15" data-name="Path 15" d="M644.262,648c-.1,80.906-.161,160.778-.247,240.651-.019,16.193-1.854,17.879-17.91,16.815-30.435-2.017-60.9-.8-91.352-.681-8.375.034-11.943-1.438-11.9-10.8.384-88.046-.731-176.114.791-264.137,1.121-64.842,28.842-118.8,75.282-163.53a263.707,263.707,0,0,1,54.369-40.425c28.521-15.967,59.219-23.481,91.714-23.8,26.745-.263,53.5.173,80.245-.087,8.372-.082,11.439,2.182,11.283,11.473-.658,39.369-.286,78.755-.355,118.135-.023,13.655-.642,14.1-14.553,14.283-25.96.334-52.078-.921-77.844,1.516C693.816,552.135,648.906,598.209,644.262,648Z" transform="translate(-209.082 -401.999)"/>
|
|
13
|
+
</g>
|
|
14
|
+
</g>
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class RecentReborn implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RecentReborn = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const BASE_URL = 'https://app.recentreborn.com/api/v1';
|
|
6
|
+
/**
|
|
7
|
+
* Calls a RecentReborn GET endpoint, following next_pagination_token until
|
|
8
|
+
* either there are no more pages or the requested item limit is reached.
|
|
9
|
+
*/
|
|
10
|
+
async function fetchAllPages(ctx, path, qs, returnAll, limit) {
|
|
11
|
+
const results = [];
|
|
12
|
+
let paginationToken;
|
|
13
|
+
do {
|
|
14
|
+
const query = {};
|
|
15
|
+
for (const [key, value] of Object.entries(qs)) {
|
|
16
|
+
if (value !== undefined && value !== '')
|
|
17
|
+
query[key] = value;
|
|
18
|
+
}
|
|
19
|
+
if (paginationToken)
|
|
20
|
+
query.pagination_token = paginationToken;
|
|
21
|
+
const options = {
|
|
22
|
+
method: 'GET',
|
|
23
|
+
url: `${BASE_URL}${path}`,
|
|
24
|
+
qs: query,
|
|
25
|
+
json: true,
|
|
26
|
+
};
|
|
27
|
+
const response = (await ctx.helpers.httpRequestWithAuthentication.call(ctx, 'recentRebornApi', options));
|
|
28
|
+
results.push(...(response.results ?? []));
|
|
29
|
+
paginationToken = response.next_pagination_token;
|
|
30
|
+
if (!returnAll && results.length >= limit) {
|
|
31
|
+
return results.slice(0, limit);
|
|
32
|
+
}
|
|
33
|
+
} while (paginationToken && (returnAll || results.length < limit));
|
|
34
|
+
return results;
|
|
35
|
+
}
|
|
36
|
+
class RecentReborn {
|
|
37
|
+
constructor() {
|
|
38
|
+
this.description = {
|
|
39
|
+
displayName: 'RecentReborn',
|
|
40
|
+
name: 'recentReborn',
|
|
41
|
+
icon: 'file:recentreborn.svg',
|
|
42
|
+
group: ['transform'],
|
|
43
|
+
version: 1,
|
|
44
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
45
|
+
description: 'Chronological, real-time search across Instagram, TikTok, and YouTube. Search recent posts by hashtag, query, or location.',
|
|
46
|
+
defaults: {
|
|
47
|
+
name: 'RecentReborn',
|
|
48
|
+
},
|
|
49
|
+
usableAsTool: true,
|
|
50
|
+
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
51
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
52
|
+
credentials: [
|
|
53
|
+
{
|
|
54
|
+
name: 'recentRebornApi',
|
|
55
|
+
required: true,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
properties: [
|
|
59
|
+
{
|
|
60
|
+
displayName: 'Operation',
|
|
61
|
+
name: 'operation',
|
|
62
|
+
type: 'options',
|
|
63
|
+
noDataExpression: true,
|
|
64
|
+
default: 'searchHashtag',
|
|
65
|
+
options: [
|
|
66
|
+
{
|
|
67
|
+
name: 'Search Hashtag',
|
|
68
|
+
value: 'searchHashtag',
|
|
69
|
+
description: 'Search recent Instagram posts using a hashtag',
|
|
70
|
+
action: 'Search recent instagram posts by hashtag',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Search Query',
|
|
74
|
+
value: 'searchQuery',
|
|
75
|
+
description: 'Search recent TikTok or YouTube posts matching a free-text query',
|
|
76
|
+
action: 'Search recent tiktok or youtube posts by query',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: 'Search Location',
|
|
80
|
+
value: 'searchLocation',
|
|
81
|
+
description: 'Search recent Instagram posts tagged at a location',
|
|
82
|
+
action: 'Search recent instagram posts by location',
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'Look Up Locations',
|
|
86
|
+
value: 'lookupLocations',
|
|
87
|
+
description: 'Find an Instagram location_id by name, for use with Search Location',
|
|
88
|
+
action: 'Look up instagram locations by name',
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
// --- Search Hashtag ---
|
|
93
|
+
{
|
|
94
|
+
displayName: 'Hashtag',
|
|
95
|
+
name: 'tag',
|
|
96
|
+
type: 'string',
|
|
97
|
+
default: '',
|
|
98
|
+
required: true,
|
|
99
|
+
placeholder: 'coffee',
|
|
100
|
+
description: 'Hashtag to search, without the # symbol',
|
|
101
|
+
displayOptions: { show: { operation: ['searchHashtag'] } },
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
displayName: 'Feed Type',
|
|
105
|
+
name: 'feedType',
|
|
106
|
+
type: 'options',
|
|
107
|
+
default: 'posts',
|
|
108
|
+
options: [
|
|
109
|
+
{ name: 'Posts', value: 'posts' },
|
|
110
|
+
{ name: 'Reels', value: 'reels' },
|
|
111
|
+
],
|
|
112
|
+
displayOptions: { show: { operation: ['searchHashtag'] } },
|
|
113
|
+
},
|
|
114
|
+
// --- Search Query ---
|
|
115
|
+
{
|
|
116
|
+
displayName: 'Query',
|
|
117
|
+
name: 'q',
|
|
118
|
+
type: 'string',
|
|
119
|
+
default: '',
|
|
120
|
+
required: true,
|
|
121
|
+
placeholder: 'matcha latte',
|
|
122
|
+
description: 'Search term to match against captions and titles',
|
|
123
|
+
displayOptions: { show: { operation: ['searchQuery'] } },
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
displayName: 'Platform',
|
|
127
|
+
name: 'platform',
|
|
128
|
+
type: 'options',
|
|
129
|
+
default: 'tiktok',
|
|
130
|
+
options: [
|
|
131
|
+
{ name: 'TikTok', value: 'tiktok' },
|
|
132
|
+
{ name: 'YouTube', value: 'youtube' },
|
|
133
|
+
],
|
|
134
|
+
displayOptions: { show: { operation: ['searchQuery'] } },
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
displayName: 'Upload Date',
|
|
138
|
+
name: 'uploadDate',
|
|
139
|
+
type: 'options',
|
|
140
|
+
default: 'today',
|
|
141
|
+
description: 'How far back to look. YouTube only. The API defaults to "hour" if omitted, which is usually too narrow for a scheduled workflow.',
|
|
142
|
+
options: [
|
|
143
|
+
{ name: 'Past Hour', value: 'hour' },
|
|
144
|
+
{ name: 'Today', value: 'today' },
|
|
145
|
+
{ name: 'Past Week', value: 'week' },
|
|
146
|
+
{ name: 'Past Month', value: 'month' },
|
|
147
|
+
{ name: 'Past Year', value: 'year' },
|
|
148
|
+
],
|
|
149
|
+
displayOptions: { show: { operation: ['searchQuery'], platform: ['youtube'] } },
|
|
150
|
+
},
|
|
151
|
+
// --- Look Up Locations ---
|
|
152
|
+
{
|
|
153
|
+
displayName: 'Location Name',
|
|
154
|
+
name: 'locationQuery',
|
|
155
|
+
type: 'string',
|
|
156
|
+
default: '',
|
|
157
|
+
required: true,
|
|
158
|
+
placeholder: 'Berlin, Germany',
|
|
159
|
+
description: 'Location name to search for, to find its location_id',
|
|
160
|
+
displayOptions: { show: { operation: ['lookupLocations'] } },
|
|
161
|
+
},
|
|
162
|
+
// --- Search Location ---
|
|
163
|
+
{
|
|
164
|
+
displayName: 'Location ID',
|
|
165
|
+
name: 'locationId',
|
|
166
|
+
type: 'number',
|
|
167
|
+
default: 0,
|
|
168
|
+
required: true,
|
|
169
|
+
description: 'Instagram location ID. Get this from the "Look Up Locations" operation first.',
|
|
170
|
+
displayOptions: { show: { operation: ['searchLocation'] } },
|
|
171
|
+
},
|
|
172
|
+
// --- Shared pagination controls (search operations only) ---
|
|
173
|
+
{
|
|
174
|
+
displayName: 'Return All',
|
|
175
|
+
name: 'returnAll',
|
|
176
|
+
type: 'boolean',
|
|
177
|
+
default: false,
|
|
178
|
+
description: 'Whether to follow next_pagination_token and return all results, or stop at a limit',
|
|
179
|
+
displayOptions: {
|
|
180
|
+
show: { operation: ['searchHashtag', 'searchQuery', 'searchLocation'] },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
displayName: 'Limit',
|
|
185
|
+
name: 'limit',
|
|
186
|
+
type: 'number',
|
|
187
|
+
default: 50,
|
|
188
|
+
typeOptions: { minValue: 1 },
|
|
189
|
+
description: 'Max number of posts to return',
|
|
190
|
+
displayOptions: {
|
|
191
|
+
show: {
|
|
192
|
+
operation: ['searchHashtag', 'searchQuery', 'searchLocation'],
|
|
193
|
+
returnAll: [false],
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
async execute() {
|
|
201
|
+
const items = this.getInputData();
|
|
202
|
+
const returnData = [];
|
|
203
|
+
const operation = this.getNodeParameter('operation', 0);
|
|
204
|
+
for (let i = 0; i < items.length; i++) {
|
|
205
|
+
try {
|
|
206
|
+
if (operation === 'lookupLocations') {
|
|
207
|
+
const query = this.getNodeParameter('locationQuery', i);
|
|
208
|
+
const options = {
|
|
209
|
+
method: 'GET',
|
|
210
|
+
url: `${BASE_URL}/locations`,
|
|
211
|
+
qs: { query },
|
|
212
|
+
json: true,
|
|
213
|
+
};
|
|
214
|
+
const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'recentRebornApi', options));
|
|
215
|
+
for (const location of response.locations ?? []) {
|
|
216
|
+
returnData.push({ json: location, pairedItem: i });
|
|
217
|
+
}
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
const returnAll = this.getNodeParameter('returnAll', i, false);
|
|
221
|
+
const limit = this.getNodeParameter('limit', i, 50);
|
|
222
|
+
let path;
|
|
223
|
+
let qs;
|
|
224
|
+
if (operation === 'searchHashtag') {
|
|
225
|
+
path = '/search/hashtag';
|
|
226
|
+
qs = {
|
|
227
|
+
tag: this.getNodeParameter('tag', i),
|
|
228
|
+
feed_type: this.getNodeParameter('feedType', i),
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
else if (operation === 'searchQuery') {
|
|
232
|
+
const platform = this.getNodeParameter('platform', i);
|
|
233
|
+
path = '/search/query';
|
|
234
|
+
qs = {
|
|
235
|
+
q: this.getNodeParameter('q', i),
|
|
236
|
+
platform,
|
|
237
|
+
upload_date: platform === 'youtube' ? this.getNodeParameter('uploadDate', i) : undefined,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
else if (operation === 'searchLocation') {
|
|
241
|
+
path = '/search/location';
|
|
242
|
+
qs = {
|
|
243
|
+
location_id: this.getNodeParameter('locationId', i),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
248
|
+
message: `Unknown operation: ${operation}`,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const posts = await fetchAllPages(this, path, qs, returnAll, limit);
|
|
252
|
+
for (const post of posts) {
|
|
253
|
+
returnData.push({ json: post, pairedItem: i });
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (this.continueOnFail()) {
|
|
258
|
+
returnData.push({ json: { error: error.message }, pairedItem: i });
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return [returnData];
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
exports.RecentReborn = RecentReborn;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="linear-gradient" y1="1" x2="1" gradientUnits="objectBoundingBox">
|
|
4
|
+
<stop offset="0" stop-color="#c6c6f1"/>
|
|
5
|
+
<stop offset="1" stop-color="#f0d7d6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<g id="Group_14" data-name="Group 14" transform="translate(-33 -17)">
|
|
9
|
+
<rect id="Rectangle_15" data-name="Rectangle 15" width="1024" height="1024" rx="512" transform="translate(33 17)" fill="url(#linear-gradient)"/>
|
|
10
|
+
<g id="Group_13" data-name="Group 13" transform="translate(241.174 277.218)">
|
|
11
|
+
<path id="Path_14" data-name="Path 14" d="M644.262,648c-.1,80.906-.161,160.778-.247,240.651-.019,16.193-1.854,17.879-17.91,16.815-30.435-2.017-60.9-.8-91.352-.681-8.375.034-11.943-1.438-11.9-10.8.384-88.046-.731-176.114.791-264.137,1.121-64.842,28.842-118.8,75.282-163.53a263.707,263.707,0,0,1,54.369-40.425c28.521-15.967,59.219-23.481,91.714-23.8,26.745-.263,53.5.173,80.245-.087,8.372-.082,11.439,2.182,11.283,11.473-.658,39.369-.286,78.755-.355,118.135-.023,13.655-.642,14.1-14.553,14.283-25.96.334-52.078-.921-77.844,1.516C693.816,552.135,648.906,598.209,644.262,648Z" transform="translate(-522.811 -401.999)"/>
|
|
12
|
+
<path id="Path_15" data-name="Path 15" d="M644.262,648c-.1,80.906-.161,160.778-.247,240.651-.019,16.193-1.854,17.879-17.91,16.815-30.435-2.017-60.9-.8-91.352-.681-8.375.034-11.943-1.438-11.9-10.8.384-88.046-.731-176.114.791-264.137,1.121-64.842,28.842-118.8,75.282-163.53a263.707,263.707,0,0,1,54.369-40.425c28.521-15.967,59.219-23.481,91.714-23.8,26.745-.263,53.5.173,80.245-.087,8.372-.082,11.439,2.182,11.283,11.473-.658,39.369-.286,78.755-.355,118.135-.023,13.655-.642,14.1-14.553,14.283-25.96.334-52.078-.921-77.844,1.516C693.816,552.135,648.906,598.209,644.262,648Z" transform="translate(-209.082 -401.999)"/>
|
|
13
|
+
</g>
|
|
14
|
+
</g>
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { INodeExecutionData, INodeType, INodeTypeDescription, IPollFunctions } from 'n8n-workflow';
|
|
2
|
+
export declare class RecentRebornTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null>;
|
|
5
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.RecentRebornTrigger = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const BASE_URL = 'https://app.recentreborn.com/api/v1';
|
|
6
|
+
class RecentRebornTrigger {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: 'RecentReborn Trigger',
|
|
10
|
+
name: 'recentRebornTrigger',
|
|
11
|
+
icon: 'file:recentreborn.svg',
|
|
12
|
+
group: ['trigger'],
|
|
13
|
+
version: 1,
|
|
14
|
+
subtitle: '={{$parameter["searchType"]}}',
|
|
15
|
+
description: 'Polls RecentReborn on a schedule and starts a workflow the moment a new hashtag, query, or location post appears',
|
|
16
|
+
defaults: {
|
|
17
|
+
name: 'RecentReborn Trigger',
|
|
18
|
+
},
|
|
19
|
+
polling: true,
|
|
20
|
+
inputs: [],
|
|
21
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
22
|
+
credentials: [
|
|
23
|
+
{
|
|
24
|
+
name: 'recentRebornApi',
|
|
25
|
+
required: true,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
properties: [
|
|
29
|
+
{
|
|
30
|
+
displayName: 'Search Type',
|
|
31
|
+
name: 'searchType',
|
|
32
|
+
type: 'options',
|
|
33
|
+
noDataExpression: true,
|
|
34
|
+
default: 'hashtag',
|
|
35
|
+
options: [
|
|
36
|
+
{ name: 'Hashtag (Instagram)', value: 'hashtag' },
|
|
37
|
+
{ name: 'Query (TikTok / YouTube)', value: 'query' },
|
|
38
|
+
{ name: 'Location (Instagram)', value: 'location' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
displayName: 'Hashtag',
|
|
43
|
+
name: 'tag',
|
|
44
|
+
type: 'string',
|
|
45
|
+
default: '',
|
|
46
|
+
required: true,
|
|
47
|
+
placeholder: 'coffee',
|
|
48
|
+
description: 'Hashtag to watch, without the # symbol',
|
|
49
|
+
displayOptions: { show: { searchType: ['hashtag'] } },
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
displayName: 'Feed Type',
|
|
53
|
+
name: 'feedType',
|
|
54
|
+
type: 'options',
|
|
55
|
+
default: 'posts',
|
|
56
|
+
options: [
|
|
57
|
+
{ name: 'Posts', value: 'posts' },
|
|
58
|
+
{ name: 'Reels', value: 'reels' },
|
|
59
|
+
],
|
|
60
|
+
displayOptions: { show: { searchType: ['hashtag'] } },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
displayName: 'Query',
|
|
64
|
+
name: 'q',
|
|
65
|
+
type: 'string',
|
|
66
|
+
default: '',
|
|
67
|
+
required: true,
|
|
68
|
+
placeholder: 'matcha latte',
|
|
69
|
+
description: 'Search term to watch, matched against captions and titles',
|
|
70
|
+
displayOptions: { show: { searchType: ['query'] } },
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
displayName: 'Platform',
|
|
74
|
+
name: 'platform',
|
|
75
|
+
type: 'options',
|
|
76
|
+
default: 'tiktok',
|
|
77
|
+
options: [
|
|
78
|
+
{ name: 'TikTok', value: 'tiktok' },
|
|
79
|
+
{ name: 'YouTube', value: 'youtube' },
|
|
80
|
+
],
|
|
81
|
+
displayOptions: { show: { searchType: ['query'] } },
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
displayName: 'Upload Date',
|
|
85
|
+
name: 'uploadDate',
|
|
86
|
+
type: 'options',
|
|
87
|
+
default: 'today',
|
|
88
|
+
description: 'How far back each poll looks on YouTube. Keep this wider than your polling interval so no post is missed between polls; the node still filters out anything it has already emitted.',
|
|
89
|
+
options: [
|
|
90
|
+
{ name: 'Past Hour', value: 'hour' },
|
|
91
|
+
{ name: 'Today', value: 'today' },
|
|
92
|
+
{ name: 'Past Week', value: 'week' },
|
|
93
|
+
],
|
|
94
|
+
displayOptions: { show: { searchType: ['query'], platform: ['youtube'] } },
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
displayName: 'Location ID',
|
|
98
|
+
name: 'locationId',
|
|
99
|
+
type: 'number',
|
|
100
|
+
default: 0,
|
|
101
|
+
required: true,
|
|
102
|
+
description: 'Instagram location ID to watch. Use the RecentReborn node\'s "Look Up Locations" operation to find it.',
|
|
103
|
+
displayOptions: { show: { searchType: ['location'] } },
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async poll() {
|
|
109
|
+
const searchType = this.getNodeParameter('searchType');
|
|
110
|
+
let path;
|
|
111
|
+
const qs = {};
|
|
112
|
+
if (searchType === 'hashtag') {
|
|
113
|
+
path = '/search/hashtag';
|
|
114
|
+
qs.tag = this.getNodeParameter('tag');
|
|
115
|
+
qs.feed_type = this.getNodeParameter('feedType');
|
|
116
|
+
}
|
|
117
|
+
else if (searchType === 'query') {
|
|
118
|
+
const platform = this.getNodeParameter('platform');
|
|
119
|
+
path = '/search/query';
|
|
120
|
+
qs.q = this.getNodeParameter('q');
|
|
121
|
+
qs.platform = platform;
|
|
122
|
+
if (platform === 'youtube') {
|
|
123
|
+
qs.upload_date = this.getNodeParameter('uploadDate');
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
path = '/search/location';
|
|
128
|
+
qs.location_id = this.getNodeParameter('locationId');
|
|
129
|
+
}
|
|
130
|
+
const options = {
|
|
131
|
+
method: 'GET',
|
|
132
|
+
url: `${BASE_URL}${path}`,
|
|
133
|
+
qs,
|
|
134
|
+
json: true,
|
|
135
|
+
};
|
|
136
|
+
const response = (await this.helpers.httpRequestWithAuthentication.call(this, 'recentRebornApi', options));
|
|
137
|
+
// RecentReborn returns newest-first. Keep only posts newer than the last
|
|
138
|
+
// one we've already emitted, then flip to chronological (oldest-first)
|
|
139
|
+
// order so downstream nodes process them in the order they happened.
|
|
140
|
+
const posts = response.results ?? [];
|
|
141
|
+
const staticData = this.getWorkflowStaticData('node');
|
|
142
|
+
const lastPostedAt = staticData.lastPostedAt;
|
|
143
|
+
const newPosts = lastPostedAt
|
|
144
|
+
? posts.filter((post) => post.posted_at > lastPostedAt)
|
|
145
|
+
: posts;
|
|
146
|
+
if (posts.length > 0) {
|
|
147
|
+
// posts[0] is the newest, since results come back newest-first.
|
|
148
|
+
staticData.lastPostedAt = posts[0].posted_at;
|
|
149
|
+
}
|
|
150
|
+
// On the very first poll there's nothing to compare against yet, so
|
|
151
|
+
// don't flood the workflow with everything that already exists. Emit
|
|
152
|
+
// data only when the user is manually testing the node, so they can
|
|
153
|
+
// see the shape of a real result.
|
|
154
|
+
if (!lastPostedAt && this.getMode() !== 'manual') {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (newPosts.length === 0) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const ordered = [...newPosts].reverse();
|
|
161
|
+
const returnData = ordered.map((post) => ({
|
|
162
|
+
json: post,
|
|
163
|
+
}));
|
|
164
|
+
return [returnData];
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.RecentRebornTrigger = RecentRebornTrigger;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="linear-gradient" y1="1" x2="1" gradientUnits="objectBoundingBox">
|
|
4
|
+
<stop offset="0" stop-color="#c6c6f1"/>
|
|
5
|
+
<stop offset="1" stop-color="#f0d7d6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<g id="Group_14" data-name="Group 14" transform="translate(-33 -17)">
|
|
9
|
+
<rect id="Rectangle_15" data-name="Rectangle 15" width="1024" height="1024" rx="512" transform="translate(33 17)" fill="url(#linear-gradient)"/>
|
|
10
|
+
<g id="Group_13" data-name="Group 13" transform="translate(241.174 277.218)">
|
|
11
|
+
<path id="Path_14" data-name="Path 14" d="M644.262,648c-.1,80.906-.161,160.778-.247,240.651-.019,16.193-1.854,17.879-17.91,16.815-30.435-2.017-60.9-.8-91.352-.681-8.375.034-11.943-1.438-11.9-10.8.384-88.046-.731-176.114.791-264.137,1.121-64.842,28.842-118.8,75.282-163.53a263.707,263.707,0,0,1,54.369-40.425c28.521-15.967,59.219-23.481,91.714-23.8,26.745-.263,53.5.173,80.245-.087,8.372-.082,11.439,2.182,11.283,11.473-.658,39.369-.286,78.755-.355,118.135-.023,13.655-.642,14.1-14.553,14.283-25.96.334-52.078-.921-77.844,1.516C693.816,552.135,648.906,598.209,644.262,648Z" transform="translate(-522.811 -401.999)"/>
|
|
12
|
+
<path id="Path_15" data-name="Path 15" d="M644.262,648c-.1,80.906-.161,160.778-.247,240.651-.019,16.193-1.854,17.879-17.91,16.815-30.435-2.017-60.9-.8-91.352-.681-8.375.034-11.943-1.438-11.9-10.8.384-88.046-.731-176.114.791-264.137,1.121-64.842,28.842-118.8,75.282-163.53a263.707,263.707,0,0,1,54.369-40.425c28.521-15.967,59.219-23.481,91.714-23.8,26.745-.263,53.5.173,80.245-.087,8.372-.082,11.439,2.182,11.283,11.473-.658,39.369-.286,78.755-.355,118.135-.023,13.655-.642,14.1-14.553,14.283-25.96.334-52.078-.921-77.844,1.516C693.816,552.135,648.906,598.209,644.262,648Z" transform="translate(-209.082 -401.999)"/>
|
|
13
|
+
</g>
|
|
14
|
+
</g>
|
|
15
|
+
</svg>
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-recentreborn",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community node for RecentReborn, chronological search for Instagram, TikTok, and YouTube. Search hashtags, queries, and locations, and trigger workflows the moment new posts appear.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"recentreborn",
|
|
8
|
+
"instagram",
|
|
9
|
+
"tiktok",
|
|
10
|
+
"youtube",
|
|
11
|
+
"hashtag search"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"homepage": "https://recentreborn.com",
|
|
15
|
+
"author": {
|
|
16
|
+
"name": "RecentReborn",
|
|
17
|
+
"email": "felix@recentreborn.com"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/takkbbl/RecentReborn-n8n.git"
|
|
22
|
+
},
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/takkbbl/RecentReborn-n8n/issues"
|
|
25
|
+
},
|
|
26
|
+
"main": "index.js",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"dev": "n8n-node dev",
|
|
32
|
+
"build": "n8n-node build",
|
|
33
|
+
"build:watch": "tsc --watch",
|
|
34
|
+
"lint": "n8n-node lint",
|
|
35
|
+
"lint:fix": "n8n-node lint --fix",
|
|
36
|
+
"release": "n8n-node release",
|
|
37
|
+
"prepublishOnly": "n8n-node prerelease",
|
|
38
|
+
"test": "vitest run"
|
|
39
|
+
},
|
|
40
|
+
"files": [
|
|
41
|
+
"dist"
|
|
42
|
+
],
|
|
43
|
+
"n8n": {
|
|
44
|
+
"n8nNodesApiVersion": 1,
|
|
45
|
+
"strict": true,
|
|
46
|
+
"credentials": [
|
|
47
|
+
"dist/src/credentials/RecentRebornApi.credentials.js"
|
|
48
|
+
],
|
|
49
|
+
"nodes": [
|
|
50
|
+
"dist/src/nodes/RecentReborn/RecentReborn.node.js",
|
|
51
|
+
"dist/src/nodes/RecentRebornTrigger/RecentRebornTrigger.node.js"
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@n8n/node-cli": "^0.47.1",
|
|
56
|
+
"typescript": "^5.9.2",
|
|
57
|
+
"vitest": "^3.0.0"
|
|
58
|
+
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"n8n-workflow": "*"
|
|
61
|
+
}
|
|
62
|
+
}
|