neoagent 3.1.1-beta.2 → 3.1.1-beta.4
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/extensions/chrome-browser/manifest.json +1 -1
- package/extensions/chrome-browser/protocol.mjs +36 -0
- package/flutter_app/lib/main_app_shell.dart +9 -3
- package/flutter_app/lib/main_controller.dart +49 -0
- package/flutter_app/lib/main_models.dart +55 -0
- package/flutter_app/lib/main_operations.dart +530 -2
- package/flutter_app/lib/main_settings.dart +301 -1
- package/flutter_app/lib/main_shared.dart +7 -2
- package/flutter_app/lib/main_timeline.dart +82 -11
- package/flutter_app/lib/src/backend_client.dart +50 -0
- package/landing/index.html +3 -0
- package/landing/status.html +178 -0
- package/lib/schema_migrations.js +23 -0
- package/package.json +1 -1
- package/server/db/database.js +6 -0
- package/server/http/routes.js +2 -0
- package/server/http/static.js +1 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +76806 -75964
- package/server/routes/public_status.js +12 -0
- package/server/routes/settings.js +14 -0
- package/server/routes/social_reach.js +88 -0
- package/server/routes/tasks.js +17 -0
- package/server/services/ai/tools.js +60 -1
- package/server/services/browser/extension/protocol.js +1 -0
- package/server/services/manager.js +14 -0
- package/server/services/memory/consolidation.js +2 -0
- package/server/services/memory/manager.js +70 -8
- package/server/services/memory/retention.js +66 -0
- package/server/services/memory/routing.js +68 -0
- package/server/services/messaging/access_policy.js +39 -2
- package/server/services/messaging/manager.js +9 -4
- package/server/services/public_status.js +103 -0
- package/server/services/runtime/settings.js +6 -0
- package/server/services/social_reach/channels/base.js +56 -0
- package/server/services/social_reach/channels/github.js +75 -0
- package/server/services/social_reach/channels/index.js +34 -0
- package/server/services/social_reach/channels/linkedin.js +34 -0
- package/server/services/social_reach/channels/rss.js +58 -0
- package/server/services/social_reach/channels/unsupported.js +49 -0
- package/server/services/social_reach/channels/v2ex.js +85 -0
- package/server/services/social_reach/channels/web.js +40 -0
- package/server/services/social_reach/channels/xueqiu.js +101 -0
- package/server/services/social_reach/channels/youtube.js +42 -0
- package/server/services/social_reach/index.js +7 -0
- package/server/services/social_reach/platforms.js +162 -0
- package/server/services/social_reach/service.js +163 -0
- package/server/services/social_reach/store.js +89 -0
- package/server/services/social_reach/utils.js +95 -0
- package/server/services/tasks/delivery_targets.js +260 -0
- package/server/utils/whatsapp.js +33 -1
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SocialReachChannel } = require('./base');
|
|
4
|
+
const { getPlatformDefinition } = require('../platforms');
|
|
5
|
+
const { cookieHeaderForPlatform, getCookieSummary } = require('../store');
|
|
6
|
+
const { DEFAULT_UA, fetchJson, normalizeLimit, parseMaybeJson } = require('../utils');
|
|
7
|
+
|
|
8
|
+
function stripHtml(value) {
|
|
9
|
+
return String(value || '').replace(/<[^>]+>/g, '').replace(/\s+/g, ' ').trim();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
class XueqiuChannel extends SocialReachChannel {
|
|
13
|
+
constructor() {
|
|
14
|
+
super(getPlatformDefinition('xueqiu'));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async check({ userId } = {}) {
|
|
18
|
+
const cookies = getCookieSummary(userId, this.id);
|
|
19
|
+
return {
|
|
20
|
+
...(await super.check()),
|
|
21
|
+
ready: cookies.configured,
|
|
22
|
+
status: cookies.configured ? 'ok' : 'warn',
|
|
23
|
+
activeBackend: cookies.configured ? 'xueqiu_http_cookies' : null,
|
|
24
|
+
cookie: cookies,
|
|
25
|
+
message: cookies.configured
|
|
26
|
+
? 'Cookie-backed Xueqiu APIs are configured.'
|
|
27
|
+
: 'Import Xueqiu cookies from the Chrome extension before using stock and community APIs.',
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
#headers(userId) {
|
|
32
|
+
const cookie = cookieHeaderForPlatform(userId, this.id);
|
|
33
|
+
return {
|
|
34
|
+
'user-agent': DEFAULT_UA,
|
|
35
|
+
referer: 'https://xueqiu.com/',
|
|
36
|
+
...(cookie ? { cookie } : {}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async search({ userId, query, limit }) {
|
|
41
|
+
const q = String(query || '').trim();
|
|
42
|
+
if (!q) {
|
|
43
|
+
const error = new Error('query is required.');
|
|
44
|
+
error.status = 400;
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
const url = `https://xueqiu.com/stock/search.json?code=${encodeURIComponent(q)}&size=${normalizeLimit(limit, 10, 50)}`;
|
|
48
|
+
const data = await fetchJson(url, { headers: this.#headers(userId) });
|
|
49
|
+
return {
|
|
50
|
+
platform: this.id,
|
|
51
|
+
query: q,
|
|
52
|
+
results: (data.stocks || []).map((item) => ({
|
|
53
|
+
symbol: item.code || '',
|
|
54
|
+
name: item.name || '',
|
|
55
|
+
exchange: item.exchange || '',
|
|
56
|
+
})),
|
|
57
|
+
source: 'xueqiu_http_cookies',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async read({ userId, symbol, limit }) {
|
|
62
|
+
const raw = String(symbol || '').trim().toUpperCase();
|
|
63
|
+
if (raw) {
|
|
64
|
+
const data = await fetchJson(
|
|
65
|
+
`https://stock.xueqiu.com/v5/stock/batch/quote.json?symbol=${encodeURIComponent(raw)}`,
|
|
66
|
+
{ headers: this.#headers(userId) },
|
|
67
|
+
);
|
|
68
|
+
const quote = data.data?.items?.[0]?.quote || {};
|
|
69
|
+
return {
|
|
70
|
+
platform: this.id,
|
|
71
|
+
symbol: quote.symbol || raw,
|
|
72
|
+
name: quote.name || '',
|
|
73
|
+
quote,
|
|
74
|
+
source: 'xueqiu_http_cookies',
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const data = await fetchJson(
|
|
78
|
+
'https://xueqiu.com/v4/statuses/public_timeline_by_category.json?since_id=-1&max_id=-1&count=20&category=-1',
|
|
79
|
+
{ headers: this.#headers(userId) },
|
|
80
|
+
);
|
|
81
|
+
return {
|
|
82
|
+
platform: this.id,
|
|
83
|
+
results: (data.list || []).slice(0, normalizeLimit(limit, 20, 50)).map((item) => {
|
|
84
|
+
const post = parseMaybeJson(item.data, {});
|
|
85
|
+
return {
|
|
86
|
+
id: post.id || item.id || null,
|
|
87
|
+
title: post.title || '',
|
|
88
|
+
text: stripHtml(post.text || post.description || ''),
|
|
89
|
+
author: post.user?.screen_name || '',
|
|
90
|
+
likes: post.like_count || null,
|
|
91
|
+
url: post.target ? `https://xueqiu.com${post.target}` : '',
|
|
92
|
+
};
|
|
93
|
+
}),
|
|
94
|
+
source: 'xueqiu_http_cookies',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
XueqiuChannel,
|
|
101
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { SocialReachChannel } = require('./base');
|
|
4
|
+
const { getPlatformDefinition } = require('../platforms');
|
|
5
|
+
const { assertHttpUrl, compactText, fetchJson, fetchText } = require('../utils');
|
|
6
|
+
|
|
7
|
+
class YoutubeChannel extends SocialReachChannel {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(getPlatformDefinition('youtube'));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async check() {
|
|
13
|
+
return {
|
|
14
|
+
...(await super.check()),
|
|
15
|
+
activeBackend: 'youtube_oembed_jina',
|
|
16
|
+
message: 'Node-only mode supports public metadata and readable page text. Caption extraction is not available without non-Node tooling.',
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async read({ url }) {
|
|
21
|
+
const parsed = assertHttpUrl(url);
|
|
22
|
+
const [oembed, page] = await Promise.all([
|
|
23
|
+
fetchJson(`https://www.youtube.com/oembed?url=${encodeURIComponent(parsed.toString())}&format=json`).catch(() => null),
|
|
24
|
+
fetchText(`https://r.jina.ai/${parsed.toString()}`, { headers: { accept: 'text/plain' } }).catch(() => ''),
|
|
25
|
+
]);
|
|
26
|
+
return {
|
|
27
|
+
platform: this.id,
|
|
28
|
+
url: parsed.toString(),
|
|
29
|
+
title: oembed?.title || null,
|
|
30
|
+
author: oembed?.author_name || null,
|
|
31
|
+
thumbnailUrl: oembed?.thumbnail_url || null,
|
|
32
|
+
content: compactText(page, 12000),
|
|
33
|
+
transcript: null,
|
|
34
|
+
source: 'youtube_oembed_jina',
|
|
35
|
+
limitation: 'Transcript extraction is unavailable in Node-only mode.',
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = {
|
|
41
|
+
YoutubeChannel,
|
|
42
|
+
};
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const PLATFORM_DEFINITIONS = Object.freeze({
|
|
4
|
+
web: {
|
|
5
|
+
id: 'web',
|
|
6
|
+
label: 'Web',
|
|
7
|
+
tier: 0,
|
|
8
|
+
setupKind: 'none',
|
|
9
|
+
hosts: [],
|
|
10
|
+
domains: [],
|
|
11
|
+
},
|
|
12
|
+
rss: {
|
|
13
|
+
id: 'rss',
|
|
14
|
+
label: 'RSS / Atom',
|
|
15
|
+
tier: 0,
|
|
16
|
+
setupKind: 'none',
|
|
17
|
+
hosts: [],
|
|
18
|
+
domains: [],
|
|
19
|
+
},
|
|
20
|
+
v2ex: {
|
|
21
|
+
id: 'v2ex',
|
|
22
|
+
label: 'V2EX',
|
|
23
|
+
tier: 0,
|
|
24
|
+
setupKind: 'none',
|
|
25
|
+
hosts: ['v2ex.com', 'www.v2ex.com'],
|
|
26
|
+
domains: ['v2ex.com'],
|
|
27
|
+
},
|
|
28
|
+
github: {
|
|
29
|
+
id: 'github',
|
|
30
|
+
label: 'GitHub',
|
|
31
|
+
tier: 0,
|
|
32
|
+
setupKind: 'optional_auth',
|
|
33
|
+
hosts: ['github.com', 'www.github.com'],
|
|
34
|
+
domains: ['github.com'],
|
|
35
|
+
},
|
|
36
|
+
youtube: {
|
|
37
|
+
id: 'youtube',
|
|
38
|
+
label: 'YouTube',
|
|
39
|
+
tier: 0,
|
|
40
|
+
setupKind: 'none',
|
|
41
|
+
hosts: ['youtube.com', 'www.youtube.com', 'youtu.be'],
|
|
42
|
+
domains: ['youtube.com', 'youtu.be'],
|
|
43
|
+
},
|
|
44
|
+
linkedin: {
|
|
45
|
+
id: 'linkedin',
|
|
46
|
+
label: 'LinkedIn',
|
|
47
|
+
tier: 2,
|
|
48
|
+
setupKind: 'public_read_only',
|
|
49
|
+
hosts: ['linkedin.com', 'www.linkedin.com'],
|
|
50
|
+
domains: ['linkedin.com'],
|
|
51
|
+
},
|
|
52
|
+
xueqiu: {
|
|
53
|
+
id: 'xueqiu',
|
|
54
|
+
label: 'Xueqiu',
|
|
55
|
+
tier: 1,
|
|
56
|
+
setupKind: 'cookies',
|
|
57
|
+
hosts: ['xueqiu.com', 'www.xueqiu.com', 'stock.xueqiu.com'],
|
|
58
|
+
domains: ['xueqiu.com'],
|
|
59
|
+
},
|
|
60
|
+
twitter: {
|
|
61
|
+
id: 'twitter',
|
|
62
|
+
label: 'X / Twitter',
|
|
63
|
+
tier: 2,
|
|
64
|
+
setupKind: 'unsupported_node_only',
|
|
65
|
+
hosts: ['x.com', 'twitter.com', 'www.x.com', 'www.twitter.com'],
|
|
66
|
+
domains: ['x.com', 'twitter.com'],
|
|
67
|
+
},
|
|
68
|
+
reddit: {
|
|
69
|
+
id: 'reddit',
|
|
70
|
+
label: 'Reddit',
|
|
71
|
+
tier: 2,
|
|
72
|
+
setupKind: 'unsupported_node_only',
|
|
73
|
+
hosts: ['reddit.com', 'www.reddit.com', 'old.reddit.com'],
|
|
74
|
+
domains: ['reddit.com'],
|
|
75
|
+
},
|
|
76
|
+
facebook: {
|
|
77
|
+
id: 'facebook',
|
|
78
|
+
label: 'Facebook',
|
|
79
|
+
tier: 2,
|
|
80
|
+
setupKind: 'unsupported_node_only',
|
|
81
|
+
hosts: ['facebook.com', 'www.facebook.com'],
|
|
82
|
+
domains: ['facebook.com'],
|
|
83
|
+
},
|
|
84
|
+
instagram: {
|
|
85
|
+
id: 'instagram',
|
|
86
|
+
label: 'Instagram',
|
|
87
|
+
tier: 2,
|
|
88
|
+
setupKind: 'unsupported_node_only',
|
|
89
|
+
hosts: ['instagram.com', 'www.instagram.com'],
|
|
90
|
+
domains: ['instagram.com'],
|
|
91
|
+
},
|
|
92
|
+
bilibili: {
|
|
93
|
+
id: 'bilibili',
|
|
94
|
+
label: 'Bilibili',
|
|
95
|
+
tier: 2,
|
|
96
|
+
setupKind: 'unsupported_node_only',
|
|
97
|
+
hosts: ['bilibili.com', 'www.bilibili.com', 'b23.tv'],
|
|
98
|
+
domains: ['bilibili.com', 'b23.tv'],
|
|
99
|
+
},
|
|
100
|
+
xiaohongshu: {
|
|
101
|
+
id: 'xiaohongshu',
|
|
102
|
+
label: 'Xiaohongshu',
|
|
103
|
+
tier: 2,
|
|
104
|
+
setupKind: 'unsupported_node_only',
|
|
105
|
+
hosts: ['xiaohongshu.com', 'www.xiaohongshu.com', 'xhslink.com'],
|
|
106
|
+
domains: ['xiaohongshu.com', 'xhslink.com'],
|
|
107
|
+
},
|
|
108
|
+
xiaoyuzhou: {
|
|
109
|
+
id: 'xiaoyuzhou',
|
|
110
|
+
label: 'Xiaoyuzhou',
|
|
111
|
+
tier: 2,
|
|
112
|
+
setupKind: 'unsupported_node_only',
|
|
113
|
+
hosts: ['xiaoyuzhoufm.com', 'www.xiaoyuzhoufm.com'],
|
|
114
|
+
domains: ['xiaoyuzhoufm.com'],
|
|
115
|
+
},
|
|
116
|
+
exa_search: {
|
|
117
|
+
id: 'exa_search',
|
|
118
|
+
label: 'Exa Search',
|
|
119
|
+
tier: 2,
|
|
120
|
+
setupKind: 'unsupported_node_only',
|
|
121
|
+
hosts: [],
|
|
122
|
+
domains: [],
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
function normalizePlatformId(value) {
|
|
127
|
+
const id = String(value || '').trim().toLowerCase().replace(/[^a-z0-9_]+/g, '_');
|
|
128
|
+
if (id === 'x') return 'twitter';
|
|
129
|
+
return id;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getPlatformDefinition(platform) {
|
|
133
|
+
return PLATFORM_DEFINITIONS[normalizePlatformId(platform)] || null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function listPlatformDefinitions() {
|
|
137
|
+
return Object.values(PLATFORM_DEFINITIONS);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function hostMatchesPlatform(hostname, platform) {
|
|
141
|
+
const definition = getPlatformDefinition(platform);
|
|
142
|
+
if (!definition) return false;
|
|
143
|
+
const host = String(hostname || '').toLowerCase().replace(/^www\./, '');
|
|
144
|
+
return definition.hosts.some((candidate) => {
|
|
145
|
+
const normalized = candidate.toLowerCase().replace(/^www\./, '');
|
|
146
|
+
return host === normalized || host.endsWith(`.${normalized}`);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function domainsForPlatform(platform) {
|
|
151
|
+
const definition = getPlatformDefinition(platform);
|
|
152
|
+
return definition ? [...definition.domains] : [];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = {
|
|
156
|
+
PLATFORM_DEFINITIONS,
|
|
157
|
+
domainsForPlatform,
|
|
158
|
+
getPlatformDefinition,
|
|
159
|
+
hostMatchesPlatform,
|
|
160
|
+
listPlatformDefinitions,
|
|
161
|
+
normalizePlatformId,
|
|
162
|
+
};
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { EXTENSION_COMMANDS } = require('../browser/extension/protocol');
|
|
4
|
+
const { createChannels } = require('./channels');
|
|
5
|
+
const { deleteCookieBundle, getCookieSummary, writeCookieBundle } = require('./store');
|
|
6
|
+
const { domainsForPlatform, getPlatformDefinition, normalizePlatformId } = require('./platforms');
|
|
7
|
+
const { assertHttpUrl } = require('./utils');
|
|
8
|
+
|
|
9
|
+
const COOKIE_IMPORT_PLATFORMS = new Set(['xueqiu']);
|
|
10
|
+
const MAX_IMPORTED_COOKIES = 80;
|
|
11
|
+
const MAX_COOKIE_NAME_CHARS = 256;
|
|
12
|
+
const MAX_COOKIE_VALUE_CHARS = 4096;
|
|
13
|
+
|
|
14
|
+
function shapeError(error) {
|
|
15
|
+
return {
|
|
16
|
+
error: error?.message || String(error),
|
|
17
|
+
status: error?.status || 500,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sanitizeCookies(cookies = [], allowedDomains = []) {
|
|
22
|
+
const allowed = allowedDomains.map((domain) => String(domain || '').toLowerCase()).filter(Boolean);
|
|
23
|
+
return (Array.isArray(cookies) ? cookies : [])
|
|
24
|
+
.filter((cookie) => cookie && cookie.name && cookie.value != null)
|
|
25
|
+
.filter((cookie) => String(cookie.name).length <= MAX_COOKIE_NAME_CHARS)
|
|
26
|
+
.filter((cookie) => String(cookie.value).length <= MAX_COOKIE_VALUE_CHARS)
|
|
27
|
+
.filter((cookie) => {
|
|
28
|
+
const domain = String(cookie.domain || '').replace(/^\./, '').toLowerCase();
|
|
29
|
+
return allowed.some((item) => domain === item || domain.endsWith(`.${item}`));
|
|
30
|
+
})
|
|
31
|
+
.slice(0, MAX_IMPORTED_COOKIES)
|
|
32
|
+
.map((cookie) => ({
|
|
33
|
+
name: String(cookie.name),
|
|
34
|
+
value: String(cookie.value),
|
|
35
|
+
domain: String(cookie.domain || ''),
|
|
36
|
+
path: String(cookie.path || '/'),
|
|
37
|
+
secure: cookie.secure === true,
|
|
38
|
+
httpOnly: cookie.httpOnly === true,
|
|
39
|
+
sameSite: cookie.sameSite || null,
|
|
40
|
+
expirationDate: cookie.expirationDate || cookie.expires || null,
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
class SocialReachService {
|
|
45
|
+
constructor(options = {}) {
|
|
46
|
+
this.browserExtensionRegistry = options.browserExtensionRegistry || null;
|
|
47
|
+
this.channels = createChannels();
|
|
48
|
+
this.channelById = new Map(this.channels.map((channel) => [channel.id, channel]));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getChannel(platform) {
|
|
52
|
+
const id = normalizePlatformId(platform);
|
|
53
|
+
return this.channelById.get(id) || null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
detectChannelForUrl(url) {
|
|
57
|
+
const parsed = assertHttpUrl(url);
|
|
58
|
+
return this.channels.find((channel) => channel.id !== 'web' && channel.canHandleUrl(parsed.toString()))
|
|
59
|
+
|| this.getChannel('web');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async getStatus(userId) {
|
|
63
|
+
const statuses = [];
|
|
64
|
+
for (const channel of this.channels) {
|
|
65
|
+
try {
|
|
66
|
+
const status = await channel.check({ userId });
|
|
67
|
+
if (!status.cookie && channel.setupKind === 'cookies') {
|
|
68
|
+
status.cookie = getCookieSummary(userId, channel.id);
|
|
69
|
+
}
|
|
70
|
+
statuses.push(status);
|
|
71
|
+
} catch (error) {
|
|
72
|
+
statuses.push({
|
|
73
|
+
platform: channel.id,
|
|
74
|
+
label: channel.label,
|
|
75
|
+
ready: false,
|
|
76
|
+
status: 'error',
|
|
77
|
+
activeBackend: null,
|
|
78
|
+
tier: channel.tier,
|
|
79
|
+
setupKind: channel.setupKind,
|
|
80
|
+
message: error?.message || String(error),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
platforms: statuses,
|
|
86
|
+
generatedAt: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async read(userId, args = {}) {
|
|
91
|
+
const platform = normalizePlatformId(args.platform || '');
|
|
92
|
+
const channel = platform ? this.getChannel(platform) : this.detectChannelForUrl(args.url);
|
|
93
|
+
if (!channel) {
|
|
94
|
+
const error = new Error(`Unsupported social reach platform: ${platform || 'unknown'}`);
|
|
95
|
+
error.status = 400;
|
|
96
|
+
throw error;
|
|
97
|
+
}
|
|
98
|
+
return channel.read({ ...args, userId });
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async search(userId, args = {}) {
|
|
102
|
+
const platform = normalizePlatformId(args.platform || '');
|
|
103
|
+
const channel = this.getChannel(platform);
|
|
104
|
+
if (!channel) {
|
|
105
|
+
const error = new Error('platform is required and must be supported.');
|
|
106
|
+
error.status = 400;
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return channel.search({ ...args, userId });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async importCookiesFromExtension(userId, platform, options = {}) {
|
|
113
|
+
const id = normalizePlatformId(platform);
|
|
114
|
+
const definition = getPlatformDefinition(id);
|
|
115
|
+
if (!definition || !COOKIE_IMPORT_PLATFORMS.has(id)) {
|
|
116
|
+
const error = new Error(`${definition?.label || id || 'Platform'} does not support cookie import yet.`);
|
|
117
|
+
error.status = 400;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
if (!this.browserExtensionRegistry || typeof this.browserExtensionRegistry.dispatch !== 'function') {
|
|
121
|
+
const error = new Error('Chrome extension registry is unavailable.');
|
|
122
|
+
error.status = 503;
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
const domains = domainsForPlatform(id);
|
|
126
|
+
const response = await this.browserExtensionRegistry.dispatch(userId, EXTENSION_COMMANDS.GET_COOKIES, {
|
|
127
|
+
platform: id,
|
|
128
|
+
domains,
|
|
129
|
+
tokenId: options.tokenId || null,
|
|
130
|
+
}, {
|
|
131
|
+
tokenId: options.tokenId || null,
|
|
132
|
+
timeoutMs: 15000,
|
|
133
|
+
});
|
|
134
|
+
const cookies = sanitizeCookies(response?.cookies || [], domains);
|
|
135
|
+
if (cookies.length === 0) {
|
|
136
|
+
const error = new Error(`No ${definition.label} cookies were found in the connected Chrome profile. Log in first, then import again.`);
|
|
137
|
+
error.status = 422;
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
const bundle = writeCookieBundle(userId, id, { cookies });
|
|
141
|
+
return {
|
|
142
|
+
platform: id,
|
|
143
|
+
configured: true,
|
|
144
|
+
count: cookies.length,
|
|
145
|
+
importedAt: bundle.importedAt,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
clearCookies(userId, platform) {
|
|
150
|
+
const id = normalizePlatformId(platform);
|
|
151
|
+
deleteCookieBundle(userId, id);
|
|
152
|
+
return { platform: id, configured: false, count: 0 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
shapeError(error) {
|
|
156
|
+
return shapeError(error);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = {
|
|
161
|
+
SocialReachService,
|
|
162
|
+
sanitizeCookies,
|
|
163
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../db/database');
|
|
4
|
+
const { decryptValue, encryptValue } = require('../integrations/secrets');
|
|
5
|
+
const { normalizePlatformId } = require('./platforms');
|
|
6
|
+
|
|
7
|
+
function cookieSettingKey(platform) {
|
|
8
|
+
return `social_reach_cookies_${normalizePlatformId(platform)}`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function readRawSetting(userId, key) {
|
|
12
|
+
if (!userId || !key) return null;
|
|
13
|
+
return db.prepare('SELECT value FROM user_settings WHERE user_id = ? AND key = ?').get(userId, key)?.value || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function writeRawSetting(userId, key, value) {
|
|
17
|
+
db.prepare(
|
|
18
|
+
`INSERT INTO user_settings (user_id, key, value)
|
|
19
|
+
VALUES (?, ?, ?)
|
|
20
|
+
ON CONFLICT(user_id, key) DO UPDATE SET value = excluded.value`,
|
|
21
|
+
).run(userId, key, value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function deleteRawSetting(userId, key) {
|
|
25
|
+
db.prepare('DELETE FROM user_settings WHERE user_id = ? AND key = ?').run(userId, key);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readJsonSetting(userId, key, fallback = null) {
|
|
29
|
+
const raw = readRawSetting(userId, key);
|
|
30
|
+
if (!raw) return fallback;
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(raw);
|
|
33
|
+
} catch {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readCookieBundle(userId, platform) {
|
|
39
|
+
const raw = readRawSetting(userId, cookieSettingKey(platform));
|
|
40
|
+
if (!raw) return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(decryptValue(raw));
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function writeCookieBundle(userId, platform, bundle) {
|
|
49
|
+
const payload = {
|
|
50
|
+
platform: normalizePlatformId(platform),
|
|
51
|
+
importedAt: new Date().toISOString(),
|
|
52
|
+
cookies: Array.isArray(bundle?.cookies) ? bundle.cookies : [],
|
|
53
|
+
};
|
|
54
|
+
writeRawSetting(userId, cookieSettingKey(platform), encryptValue(JSON.stringify(payload)));
|
|
55
|
+
return payload;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function deleteCookieBundle(userId, platform) {
|
|
59
|
+
deleteRawSetting(userId, cookieSettingKey(platform));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getCookieSummary(userId, platform) {
|
|
63
|
+
const bundle = readCookieBundle(userId, platform);
|
|
64
|
+
const cookies = Array.isArray(bundle?.cookies) ? bundle.cookies : [];
|
|
65
|
+
return {
|
|
66
|
+
configured: cookies.length > 0,
|
|
67
|
+
count: cookies.length,
|
|
68
|
+
importedAt: bundle?.importedAt || null,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function cookieHeaderForPlatform(userId, platform) {
|
|
73
|
+
const bundle = readCookieBundle(userId, platform);
|
|
74
|
+
const cookies = Array.isArray(bundle?.cookies) ? bundle.cookies : [];
|
|
75
|
+
return cookies
|
|
76
|
+
.filter((cookie) => cookie && cookie.name && cookie.value != null)
|
|
77
|
+
.map((cookie) => `${String(cookie.name).trim()}=${String(cookie.value)}`)
|
|
78
|
+
.join('; ');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
cookieHeaderForPlatform,
|
|
83
|
+
cookieSettingKey,
|
|
84
|
+
deleteCookieBundle,
|
|
85
|
+
getCookieSummary,
|
|
86
|
+
readCookieBundle,
|
|
87
|
+
readJsonSetting,
|
|
88
|
+
writeCookieBundle,
|
|
89
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cheerio = require('cheerio');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_UA = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 '
|
|
6
|
+
+ '(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36';
|
|
7
|
+
|
|
8
|
+
function normalizeLimit(value, fallback = 10, max = 50) {
|
|
9
|
+
const n = Number(value);
|
|
10
|
+
if (!Number.isFinite(n) || n <= 0) return fallback;
|
|
11
|
+
return Math.max(1, Math.min(Math.floor(n), max));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function assertHttpUrl(value) {
|
|
15
|
+
const raw = String(value || '').trim();
|
|
16
|
+
const withScheme = /^https?:\/\//i.test(raw) ? raw : `https://${raw}`;
|
|
17
|
+
let parsed;
|
|
18
|
+
try {
|
|
19
|
+
parsed = new URL(withScheme);
|
|
20
|
+
} catch {
|
|
21
|
+
const error = new Error('A valid http or https URL is required.');
|
|
22
|
+
error.status = 400;
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
|
26
|
+
const error = new Error('Only http and https URLs are supported.');
|
|
27
|
+
error.status = 400;
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
return parsed;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function fetchText(url, options = {}) {
|
|
34
|
+
const response = await fetch(url, {
|
|
35
|
+
redirect: 'follow',
|
|
36
|
+
...options,
|
|
37
|
+
headers: {
|
|
38
|
+
'user-agent': DEFAULT_UA,
|
|
39
|
+
accept: 'text/plain,text/html,application/xml,application/rss+xml,application/atom+xml,*/*',
|
|
40
|
+
...(options.headers || {}),
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const text = await response.text();
|
|
44
|
+
if (!response.ok) {
|
|
45
|
+
const error = new Error(`Request failed with HTTP ${response.status}`);
|
|
46
|
+
error.status = response.status;
|
|
47
|
+
error.body = text.slice(0, 500);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
return text;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function fetchJson(url, options = {}) {
|
|
54
|
+
const text = await fetchText(url, {
|
|
55
|
+
...options,
|
|
56
|
+
headers: {
|
|
57
|
+
accept: 'application/json,*/*',
|
|
58
|
+
...(options.headers || {}),
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
return JSON.parse(text);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function htmlToText(html, maxChars = 20000) {
|
|
65
|
+
const $ = cheerio.load(String(html || ''));
|
|
66
|
+
$('script, style, noscript, svg').remove();
|
|
67
|
+
return $('body').text().replace(/\s+/g, ' ').trim().slice(0, maxChars);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseMaybeJson(value, fallback = null) {
|
|
71
|
+
if (value == null) return fallback;
|
|
72
|
+
if (typeof value === 'object') return value;
|
|
73
|
+
try {
|
|
74
|
+
return JSON.parse(String(value));
|
|
75
|
+
} catch {
|
|
76
|
+
return fallback;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function compactText(value, maxChars = 4000) {
|
|
81
|
+
const text = String(value || '').replace(/\s+/g, ' ').trim();
|
|
82
|
+
if (text.length <= maxChars) return text;
|
|
83
|
+
return `${text.slice(0, maxChars).trim()}...`;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = {
|
|
87
|
+
DEFAULT_UA,
|
|
88
|
+
assertHttpUrl,
|
|
89
|
+
compactText,
|
|
90
|
+
fetchJson,
|
|
91
|
+
fetchText,
|
|
92
|
+
htmlToText,
|
|
93
|
+
normalizeLimit,
|
|
94
|
+
parseMaybeJson,
|
|
95
|
+
};
|