swift-skills 0.0.1
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 +410 -0
- package/build/cli/auth.d.ts +3 -0
- package/build/cli/auth.d.ts.map +1 -0
- package/build/cli/auth.js +44 -0
- package/build/cli/auth.js.map +1 -0
- package/build/cli/setup.d.ts +2 -0
- package/build/cli/setup.d.ts.map +1 -0
- package/build/cli/setup.js +150 -0
- package/build/cli/setup.js.map +1 -0
- package/build/cli/source-manager.d.ts +3 -0
- package/build/cli/source-manager.d.ts.map +1 -0
- package/build/cli/source-manager.js +117 -0
- package/build/cli/source-manager.js.map +1 -0
- package/build/config/creators.d.ts +11 -0
- package/build/config/creators.d.ts.map +1 -0
- package/build/config/creators.js +32 -0
- package/build/config/creators.js.map +1 -0
- package/build/config/sources.d.ts +91 -0
- package/build/config/sources.d.ts.map +1 -0
- package/build/config/sources.js +231 -0
- package/build/config/sources.js.map +1 -0
- package/build/config/sources.test.d.ts +2 -0
- package/build/config/sources.test.d.ts.map +1 -0
- package/build/config/sources.test.js +199 -0
- package/build/config/sources.test.js.map +1 -0
- package/build/config/swift-keywords.d.ts +29 -0
- package/build/config/swift-keywords.d.ts.map +1 -0
- package/build/config/swift-keywords.js +77 -0
- package/build/config/swift-keywords.js.map +1 -0
- package/build/index.d.ts +3 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +181 -0
- package/build/index.js.map +1 -0
- package/build/integration/cache-behavior.test.d.ts +2 -0
- package/build/integration/cache-behavior.test.d.ts.map +1 -0
- package/build/integration/cache-behavior.test.js +521 -0
- package/build/integration/cache-behavior.test.js.map +1 -0
- package/build/integration/mcp-client.test.d.ts +2 -0
- package/build/integration/mcp-client.test.d.ts.map +1 -0
- package/build/integration/mcp-client.test.js +82 -0
- package/build/integration/mcp-client.test.js.map +1 -0
- package/build/integration/response-quality.test.d.ts +2 -0
- package/build/integration/response-quality.test.d.ts.map +1 -0
- package/build/integration/response-quality.test.js +230 -0
- package/build/integration/response-quality.test.js.map +1 -0
- package/build/integration/test-client.d.ts +25 -0
- package/build/integration/test-client.d.ts.map +1 -0
- package/build/integration/test-client.js +93 -0
- package/build/integration/test-client.js.map +1 -0
- package/build/sources/free/nilcoalescing.d.ts +8 -0
- package/build/sources/free/nilcoalescing.d.ts.map +1 -0
- package/build/sources/free/nilcoalescing.js +26 -0
- package/build/sources/free/nilcoalescing.js.map +1 -0
- package/build/sources/free/nilcoalescing.test.d.ts +2 -0
- package/build/sources/free/nilcoalescing.test.d.ts.map +1 -0
- package/build/sources/free/nilcoalescing.test.js +63 -0
- package/build/sources/free/nilcoalescing.test.js.map +1 -0
- package/build/sources/free/pointfree.d.ts +15 -0
- package/build/sources/free/pointfree.d.ts.map +1 -0
- package/build/sources/free/pointfree.js +175 -0
- package/build/sources/free/pointfree.js.map +1 -0
- package/build/sources/free/pointfree.test.d.ts +2 -0
- package/build/sources/free/pointfree.test.d.ts.map +1 -0
- package/build/sources/free/pointfree.test.js +86 -0
- package/build/sources/free/pointfree.test.js.map +1 -0
- package/build/sources/free/rssPatternSource.d.ts +42 -0
- package/build/sources/free/rssPatternSource.d.ts.map +1 -0
- package/build/sources/free/rssPatternSource.js +109 -0
- package/build/sources/free/rssPatternSource.js.map +1 -0
- package/build/sources/free/rssPatternSource.test.d.ts +2 -0
- package/build/sources/free/rssPatternSource.test.d.ts.map +1 -0
- package/build/sources/free/rssPatternSource.test.js +89 -0
- package/build/sources/free/rssPatternSource.test.js.map +1 -0
- package/build/sources/free/sundell.d.ts +8 -0
- package/build/sources/free/sundell.d.ts.map +1 -0
- package/build/sources/free/sundell.js +17 -0
- package/build/sources/free/sundell.js.map +1 -0
- package/build/sources/free/sundell.test.d.ts +2 -0
- package/build/sources/free/sundell.test.d.ts.map +1 -0
- package/build/sources/free/sundell.test.js +63 -0
- package/build/sources/free/sundell.test.js.map +1 -0
- package/build/sources/free/vanderlee.d.ts +8 -0
- package/build/sources/free/vanderlee.d.ts.map +1 -0
- package/build/sources/free/vanderlee.js +63 -0
- package/build/sources/free/vanderlee.js.map +1 -0
- package/build/sources/free/vanderlee.test.d.ts +2 -0
- package/build/sources/free/vanderlee.test.d.ts.map +1 -0
- package/build/sources/free/vanderlee.test.js +77 -0
- package/build/sources/free/vanderlee.test.js.map +1 -0
- package/build/sources/premium/patreon-dl.d.ts +45 -0
- package/build/sources/premium/patreon-dl.d.ts.map +1 -0
- package/build/sources/premium/patreon-dl.js +189 -0
- package/build/sources/premium/patreon-dl.js.map +1 -0
- package/build/sources/premium/patreon-fetch.d.ts +3 -0
- package/build/sources/premium/patreon-fetch.d.ts.map +1 -0
- package/build/sources/premium/patreon-fetch.js +18 -0
- package/build/sources/premium/patreon-fetch.js.map +1 -0
- package/build/sources/premium/patreon-oauth.d.ts +24 -0
- package/build/sources/premium/patreon-oauth.d.ts.map +1 -0
- package/build/sources/premium/patreon-oauth.js +208 -0
- package/build/sources/premium/patreon-oauth.js.map +1 -0
- package/build/sources/premium/patreon-zip.d.ts +17 -0
- package/build/sources/premium/patreon-zip.d.ts.map +1 -0
- package/build/sources/premium/patreon-zip.js +127 -0
- package/build/sources/premium/patreon-zip.js.map +1 -0
- package/build/sources/premium/patreon.d.ts +48 -0
- package/build/sources/premium/patreon.d.ts.map +1 -0
- package/build/sources/premium/patreon.js +221 -0
- package/build/sources/premium/patreon.js.map +1 -0
- package/build/sources/premium/youtube.d.ts +14 -0
- package/build/sources/premium/youtube.d.ts.map +1 -0
- package/build/sources/premium/youtube.js +92 -0
- package/build/sources/premium/youtube.js.map +1 -0
- package/build/tools/extract-cookie.d.ts +2 -0
- package/build/tools/extract-cookie.d.ts.map +1 -0
- package/build/tools/extract-cookie.js +40 -0
- package/build/tools/extract-cookie.js.map +1 -0
- package/build/tools/handlers/enableSource.d.ts +3 -0
- package/build/tools/handlers/enableSource.d.ts.map +1 -0
- package/build/tools/handlers/enableSource.js +25 -0
- package/build/tools/handlers/enableSource.js.map +1 -0
- package/build/tools/handlers/getPatreonPatterns.d.ts +3 -0
- package/build/tools/handlers/getPatreonPatterns.d.ts.map +1 -0
- package/build/tools/handlers/getPatreonPatterns.js +43 -0
- package/build/tools/handlers/getPatreonPatterns.js.map +1 -0
- package/build/tools/handlers/getSwiftPattern.d.ts +3 -0
- package/build/tools/handlers/getSwiftPattern.d.ts.map +1 -0
- package/build/tools/handlers/getSwiftPattern.js +72 -0
- package/build/tools/handlers/getSwiftPattern.js.map +1 -0
- package/build/tools/handlers/handlers.test.d.ts +2 -0
- package/build/tools/handlers/handlers.test.d.ts.map +1 -0
- package/build/tools/handlers/handlers.test.js +359 -0
- package/build/tools/handlers/handlers.test.js.map +1 -0
- package/build/tools/handlers/listContentSources.d.ts +3 -0
- package/build/tools/handlers/listContentSources.d.ts.map +1 -0
- package/build/tools/handlers/listContentSources.js +34 -0
- package/build/tools/handlers/listContentSources.js.map +1 -0
- package/build/tools/handlers/searchSwiftContent.d.ts +3 -0
- package/build/tools/handlers/searchSwiftContent.d.ts.map +1 -0
- package/build/tools/handlers/searchSwiftContent.js +121 -0
- package/build/tools/handlers/searchSwiftContent.js.map +1 -0
- package/build/tools/handlers/setupPatreon.d.ts +3 -0
- package/build/tools/handlers/setupPatreon.d.ts.map +1 -0
- package/build/tools/handlers/setupPatreon.js +40 -0
- package/build/tools/handlers/setupPatreon.js.map +1 -0
- package/build/tools/index.d.ts +3 -0
- package/build/tools/index.d.ts.map +1 -0
- package/build/tools/index.js +18 -0
- package/build/tools/index.js.map +1 -0
- package/build/tools/registry.d.ts +14 -0
- package/build/tools/registry.d.ts.map +1 -0
- package/build/tools/registry.js +21 -0
- package/build/tools/registry.js.map +1 -0
- package/build/tools/registry.test.d.ts +2 -0
- package/build/tools/registry.test.d.ts.map +1 -0
- package/build/tools/registry.test.js +54 -0
- package/build/tools/registry.test.js.map +1 -0
- package/build/tools/types.d.ts +67 -0
- package/build/tools/types.d.ts.map +1 -0
- package/build/tools/types.js +3 -0
- package/build/tools/types.js.map +1 -0
- package/build/utils/cache.d.ts +20 -0
- package/build/utils/cache.d.ts.map +1 -0
- package/build/utils/cache.js +186 -0
- package/build/utils/cache.js.map +1 -0
- package/build/utils/concurrency.d.ts +13 -0
- package/build/utils/concurrency.d.ts.map +1 -0
- package/build/utils/concurrency.js +33 -0
- package/build/utils/concurrency.js.map +1 -0
- package/build/utils/errors.d.ts +19 -0
- package/build/utils/errors.d.ts.map +1 -0
- package/build/utils/errors.js +35 -0
- package/build/utils/errors.js.map +1 -0
- package/build/utils/fetch.d.ts +6 -0
- package/build/utils/fetch.d.ts.map +1 -0
- package/build/utils/fetch.js +6 -0
- package/build/utils/fetch.js.map +1 -0
- package/build/utils/http.d.ts +21 -0
- package/build/utils/http.d.ts.map +1 -0
- package/build/utils/http.js +53 -0
- package/build/utils/http.js.map +1 -0
- package/build/utils/intent-cache.d.ts +94 -0
- package/build/utils/intent-cache.d.ts.map +1 -0
- package/build/utils/intent-cache.js +164 -0
- package/build/utils/intent-cache.js.map +1 -0
- package/build/utils/intent-cache.test.d.ts +2 -0
- package/build/utils/intent-cache.test.d.ts.map +1 -0
- package/build/utils/intent-cache.test.js +290 -0
- package/build/utils/intent-cache.test.js.map +1 -0
- package/build/utils/logger.d.ts +4 -0
- package/build/utils/logger.d.ts.map +1 -0
- package/build/utils/logger.js +9 -0
- package/build/utils/logger.js.map +1 -0
- package/build/utils/paths.d.ts +27 -0
- package/build/utils/paths.d.ts.map +1 -0
- package/build/utils/paths.js +43 -0
- package/build/utils/paths.js.map +1 -0
- package/build/utils/pattern-formatter.d.ts +40 -0
- package/build/utils/pattern-formatter.d.ts.map +1 -0
- package/build/utils/pattern-formatter.js +124 -0
- package/build/utils/pattern-formatter.js.map +1 -0
- package/build/utils/response-helpers.d.ts +17 -0
- package/build/utils/response-helpers.d.ts.map +1 -0
- package/build/utils/response-helpers.js +34 -0
- package/build/utils/response-helpers.js.map +1 -0
- package/build/utils/search-terms.d.ts +17 -0
- package/build/utils/search-terms.d.ts.map +1 -0
- package/build/utils/search-terms.js +71 -0
- package/build/utils/search-terms.js.map +1 -0
- package/build/utils/search-terms.test.d.ts +2 -0
- package/build/utils/search-terms.test.d.ts.map +1 -0
- package/build/utils/search-terms.test.js +107 -0
- package/build/utils/search-terms.test.js.map +1 -0
- package/build/utils/search.d.ts +48 -0
- package/build/utils/search.d.ts.map +1 -0
- package/build/utils/search.js +158 -0
- package/build/utils/search.js.map +1 -0
- package/build/utils/search.test.d.ts +2 -0
- package/build/utils/search.test.d.ts.map +1 -0
- package/build/utils/search.test.js +199 -0
- package/build/utils/search.test.js.map +1 -0
- package/build/utils/semantic-recall.d.ts +38 -0
- package/build/utils/semantic-recall.d.ts.map +1 -0
- package/build/utils/semantic-recall.js +134 -0
- package/build/utils/semantic-recall.js.map +1 -0
- package/build/utils/semantic-recall.test.d.ts +2 -0
- package/build/utils/semantic-recall.test.d.ts.map +1 -0
- package/build/utils/semantic-recall.test.js +326 -0
- package/build/utils/semantic-recall.test.js.map +1 -0
- package/build/utils/source-registry.d.ts +45 -0
- package/build/utils/source-registry.d.ts.map +1 -0
- package/build/utils/source-registry.js +113 -0
- package/build/utils/source-registry.js.map +1 -0
- package/build/utils/source-registry.test.d.ts +2 -0
- package/build/utils/source-registry.test.d.ts.map +1 -0
- package/build/utils/source-registry.test.js +206 -0
- package/build/utils/source-registry.test.js.map +1 -0
- package/build/utils/swift-analysis.d.ts +61 -0
- package/build/utils/swift-analysis.d.ts.map +1 -0
- package/build/utils/swift-analysis.js +339 -0
- package/build/utils/swift-analysis.js.map +1 -0
- package/build/utils/swift-analysis.test.d.ts +2 -0
- package/build/utils/swift-analysis.test.d.ts.map +1 -0
- package/build/utils/swift-analysis.test.js +473 -0
- package/build/utils/swift-analysis.test.js.map +1 -0
- package/package.json +85 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// src/sources/premium/patreon-oauth.ts
|
|
2
|
+
import http from 'http';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { URL } from 'url';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import { homedir } from 'os';
|
|
7
|
+
import fs from 'fs/promises';
|
|
8
|
+
import keytar from 'keytar';
|
|
9
|
+
import { fetch } from '../../utils/fetch.js';
|
|
10
|
+
const SERVICE_NAME = 'swift-patterns-mcp';
|
|
11
|
+
const ACCOUNT_NAME = 'patreon-tokens';
|
|
12
|
+
const CALLBACK_PORT = 9876;
|
|
13
|
+
const PATREON_AUTH_URL = 'https://www.patreon.com/oauth2/authorize';
|
|
14
|
+
const PATREON_TOKEN_URL = 'https://www.patreon.com/api/oauth2/token';
|
|
15
|
+
// Scopes explained:
|
|
16
|
+
// - identity: Basic user info
|
|
17
|
+
// - identity.memberships: REQUIRED - access to patron memberships via /identity endpoint
|
|
18
|
+
// - campaigns: Access campaigns you manage (creator-only, optional)
|
|
19
|
+
// - campaigns.members: Access members of your own campaign (creator-only, optional)
|
|
20
|
+
export const PATREON_SCOPES = [
|
|
21
|
+
'identity',
|
|
22
|
+
'identity.memberships', // REQUIRED: patron memberships live here
|
|
23
|
+
'campaigns',
|
|
24
|
+
'campaigns.members',
|
|
25
|
+
].join(' ');
|
|
26
|
+
export async function saveTokens(tokens) {
|
|
27
|
+
const encrypted = JSON.stringify(tokens);
|
|
28
|
+
await keytar.setPassword(SERVICE_NAME, ACCOUNT_NAME, encrypted);
|
|
29
|
+
}
|
|
30
|
+
export async function loadTokens() {
|
|
31
|
+
try {
|
|
32
|
+
const encrypted = await keytar.getPassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
33
|
+
if (!encrypted)
|
|
34
|
+
return null;
|
|
35
|
+
return JSON.parse(encrypted);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function clearTokens() {
|
|
42
|
+
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
43
|
+
}
|
|
44
|
+
export function isTokenExpired(tokens) {
|
|
45
|
+
// Refresh 5 minutes before actual expiry
|
|
46
|
+
return Date.now() >= (tokens.expires_at - 5 * 60 * 1000);
|
|
47
|
+
}
|
|
48
|
+
export async function refreshAccessToken(clientId, clientSecret, refreshToken) {
|
|
49
|
+
const response = await fetch(PATREON_TOKEN_URL, {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
52
|
+
body: new URLSearchParams({
|
|
53
|
+
grant_type: 'refresh_token',
|
|
54
|
+
refresh_token: refreshToken,
|
|
55
|
+
client_id: clientId,
|
|
56
|
+
client_secret: clientSecret,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
throw new Error(`Token refresh failed: ${response.status}`);
|
|
61
|
+
}
|
|
62
|
+
const data = await response.json();
|
|
63
|
+
const tokens = {
|
|
64
|
+
access_token: data.access_token,
|
|
65
|
+
refresh_token: data.refresh_token,
|
|
66
|
+
expires_at: Date.now() + data.expires_in * 1000,
|
|
67
|
+
scope: data.scope,
|
|
68
|
+
};
|
|
69
|
+
await saveTokens(tokens);
|
|
70
|
+
return tokens;
|
|
71
|
+
}
|
|
72
|
+
export async function startOAuthFlow(clientId, clientSecret) {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
const redirectUri = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
75
|
+
const authUrl = new URL(PATREON_AUTH_URL);
|
|
76
|
+
authUrl.searchParams.set('client_id', clientId);
|
|
77
|
+
authUrl.searchParams.set('redirect_uri', redirectUri);
|
|
78
|
+
authUrl.searchParams.set('response_type', 'code');
|
|
79
|
+
authUrl.searchParams.set('scope', PATREON_SCOPES);
|
|
80
|
+
let serverClosed = false;
|
|
81
|
+
const server = http.createServer(async (req, res) => {
|
|
82
|
+
if (!req.url?.startsWith('/callback')) {
|
|
83
|
+
res.writeHead(404);
|
|
84
|
+
res.end('Not found');
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
88
|
+
const code = url.searchParams.get('code');
|
|
89
|
+
const error = url.searchParams.get('error');
|
|
90
|
+
if (error) {
|
|
91
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
92
|
+
res.end('<h1>Authorization Denied</h1><p>You can close this window.</p>');
|
|
93
|
+
if (!serverClosed) {
|
|
94
|
+
serverClosed = true;
|
|
95
|
+
server.close();
|
|
96
|
+
resolve({ success: false, error });
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!code) {
|
|
101
|
+
res.writeHead(400, { 'Content-Type': 'text/html' });
|
|
102
|
+
res.end('<h1>Error</h1><p>No authorization code received.</p>');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
// Exchange code for tokens
|
|
107
|
+
const tokenResponse = await fetch(PATREON_TOKEN_URL, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
110
|
+
body: new URLSearchParams({
|
|
111
|
+
code,
|
|
112
|
+
grant_type: 'authorization_code',
|
|
113
|
+
client_id: clientId,
|
|
114
|
+
client_secret: clientSecret,
|
|
115
|
+
redirect_uri: redirectUri,
|
|
116
|
+
}),
|
|
117
|
+
});
|
|
118
|
+
if (!tokenResponse.ok) {
|
|
119
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status}`);
|
|
120
|
+
}
|
|
121
|
+
const data = await tokenResponse.json();
|
|
122
|
+
const tokens = {
|
|
123
|
+
access_token: data.access_token,
|
|
124
|
+
refresh_token: data.refresh_token,
|
|
125
|
+
expires_at: Date.now() + data.expires_in * 1000,
|
|
126
|
+
scope: data.scope,
|
|
127
|
+
};
|
|
128
|
+
await saveTokens(tokens);
|
|
129
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
130
|
+
res.end(`
|
|
131
|
+
<html>
|
|
132
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
133
|
+
<h1>Authorization Successful!</h1>
|
|
134
|
+
<p>You can close this window and return to the terminal.</p>
|
|
135
|
+
</body>
|
|
136
|
+
</html>
|
|
137
|
+
`);
|
|
138
|
+
if (!serverClosed) {
|
|
139
|
+
serverClosed = true;
|
|
140
|
+
server.close();
|
|
141
|
+
resolve({ success: true, tokens });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
res.writeHead(500, { 'Content-Type': 'text/html' });
|
|
146
|
+
res.end('<h1>Error</h1><p>Failed to complete authorization.</p>');
|
|
147
|
+
if (!serverClosed) {
|
|
148
|
+
serverClosed = true;
|
|
149
|
+
server.close();
|
|
150
|
+
resolve({ success: false, error: String(err) });
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
server.listen(CALLBACK_PORT, '127.0.0.1', () => {
|
|
155
|
+
console.log(`\nOpening browser for Patreon authorization...`);
|
|
156
|
+
console.log(`If browser doesn't open, visit: ${authUrl.toString()}\n`);
|
|
157
|
+
// Open browser
|
|
158
|
+
const cmd = process.platform === 'darwin' ? 'open' :
|
|
159
|
+
process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
160
|
+
exec(`${cmd} "${authUrl.toString()}"`);
|
|
161
|
+
});
|
|
162
|
+
// Timeout after 60 seconds
|
|
163
|
+
setTimeout(() => {
|
|
164
|
+
if (!serverClosed) {
|
|
165
|
+
serverClosed = true;
|
|
166
|
+
server.close();
|
|
167
|
+
resolve({ success: false, error: 'Authorization timed out after 60 seconds' });
|
|
168
|
+
}
|
|
169
|
+
}, 60000);
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
export async function getValidAccessToken(clientId, clientSecret) {
|
|
173
|
+
const tokens = await loadTokens();
|
|
174
|
+
if (!tokens)
|
|
175
|
+
return null;
|
|
176
|
+
if (isTokenExpired(tokens)) {
|
|
177
|
+
try {
|
|
178
|
+
const refreshed = await refreshAccessToken(clientId, clientSecret, tokens.refresh_token);
|
|
179
|
+
return refreshed.access_token;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// Refresh failed, need to re-authenticate
|
|
183
|
+
await clearTokens();
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return tokens.access_token;
|
|
188
|
+
}
|
|
189
|
+
// =============================================================================
|
|
190
|
+
// Cleanup & Reset
|
|
191
|
+
// =============================================================================
|
|
192
|
+
/**
|
|
193
|
+
* Clear all Patreon authentication data (keytar + legacy tokens.json)
|
|
194
|
+
*/
|
|
195
|
+
export async function clearPatreonAuth() {
|
|
196
|
+
// Clear from keytar
|
|
197
|
+
await keytar.deletePassword(SERVICE_NAME, "patreon");
|
|
198
|
+
await keytar.deletePassword(SERVICE_NAME, ACCOUNT_NAME);
|
|
199
|
+
// Clear legacy tokens.json file
|
|
200
|
+
const tokensPath = join(homedir(), ".swift-patterns-mcp", "tokens.json");
|
|
201
|
+
try {
|
|
202
|
+
await fs.rm(tokensPath, { force: true });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Ignore if file doesn't exist
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=patreon-oauth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patreon-oauth.js","sourceRoot":"","sources":["../../../src/sources/premium/patreon-oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ExtractedPattern {
|
|
2
|
+
filename: string;
|
|
3
|
+
content: string;
|
|
4
|
+
type: 'swift' | 'markdown' | 'playground' | 'other';
|
|
5
|
+
hasCode: boolean;
|
|
6
|
+
topics: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface ZipExtractionResult {
|
|
9
|
+
success: boolean;
|
|
10
|
+
patterns: ExtractedPattern[];
|
|
11
|
+
warnings: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function downloadZip(url: string, postId: string, accessToken: string): Promise<string | null>;
|
|
14
|
+
export declare function extractZip(zipPath: string, postId: string): ZipExtractionResult;
|
|
15
|
+
export declare function extractFromAttachment(attachmentUrl: string, postId: string, accessToken: string): Promise<ZipExtractionResult>;
|
|
16
|
+
export declare function clearZipCache(): void;
|
|
17
|
+
//# sourceMappingURL=patreon-zip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patreon-zip.d.ts","sourceRoot":"","sources":["../../../src/sources/premium/patreon-zip.ts"],"names":[],"mappings":"AAcA,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,OAAO,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CAAC;IACpD,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAuBD,wBAAsB,WAAW,CAC/B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAmCxB;AAED,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,mBAAmB,CAwD/E;AAED,wBAAsB,qBAAqB,CACzC,aAAa,EAAE,MAAM,EACrB,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,mBAAmB,CAAC,CAY9B;AAED,wBAAgB,aAAa,IAAI,IAAI,CAKpC"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// src/sources/premium/patreon-zip.ts
|
|
2
|
+
import AdmZip from 'adm-zip';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { getCacheDir } from '../../utils/paths.js';
|
|
6
|
+
import { detectTopics, hasCodeContent } from '../../utils/swift-analysis.js';
|
|
7
|
+
import { createSourceConfig } from '../../config/swift-keywords.js';
|
|
8
|
+
import { logError } from '../../utils/errors.js';
|
|
9
|
+
import { fetch } from '../../utils/fetch.js';
|
|
10
|
+
const MAX_ZIP_SIZE = 50 * 1024 * 1024; // 50MB
|
|
11
|
+
const MAX_FILES = 100;
|
|
12
|
+
const { topicKeywords: zipTopicKeywords } = createSourceConfig({
|
|
13
|
+
'concurrency': ['sendable'],
|
|
14
|
+
'networking': ['request'],
|
|
15
|
+
'testing': ['stub'],
|
|
16
|
+
'architecture': ['repository', 'usecase'],
|
|
17
|
+
'uikit': ['uitableview', 'uicollectionview'],
|
|
18
|
+
}, {});
|
|
19
|
+
function detectFileType(filename) {
|
|
20
|
+
const ext = path.extname(filename).toLowerCase();
|
|
21
|
+
switch (ext) {
|
|
22
|
+
case '.swift': return 'swift';
|
|
23
|
+
case '.md':
|
|
24
|
+
case '.markdown': return 'markdown';
|
|
25
|
+
case '.playground': return 'playground';
|
|
26
|
+
default: return 'other';
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function downloadZip(url, postId, accessToken) {
|
|
30
|
+
const cacheDir = getCacheDir('zips');
|
|
31
|
+
const zipPath = path.join(cacheDir, `${postId}.zip`);
|
|
32
|
+
// Check if already cached
|
|
33
|
+
if (fs.existsSync(zipPath)) {
|
|
34
|
+
return zipPath;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const response = await fetch(url, {
|
|
38
|
+
headers: { 'Authorization': `Bearer ${accessToken}` },
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
logError('Patreon Zip', `Download failed: ${response.status}`, { postId });
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const contentLength = response.headers.get('content-length');
|
|
45
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_ZIP_SIZE) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
// Ensure cache directory exists
|
|
49
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
50
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
51
|
+
fs.writeFileSync(zipPath, buffer);
|
|
52
|
+
return zipPath;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
logError('Patreon Zip', error, { postId });
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function extractZip(zipPath, postId) {
|
|
60
|
+
const warnings = [];
|
|
61
|
+
const patterns = [];
|
|
62
|
+
const destDir = path.join(getCacheDir('zips'), postId);
|
|
63
|
+
try {
|
|
64
|
+
const zip = new AdmZip(zipPath);
|
|
65
|
+
const entries = zip.getEntries();
|
|
66
|
+
if (entries.length > MAX_FILES) {
|
|
67
|
+
warnings.push(`Zip contains ${entries.length} files, extracting first ${MAX_FILES}`);
|
|
68
|
+
}
|
|
69
|
+
// Ensure destination exists
|
|
70
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
71
|
+
let count = 0;
|
|
72
|
+
for (const entry of entries) {
|
|
73
|
+
if (count >= MAX_FILES)
|
|
74
|
+
break;
|
|
75
|
+
if (entry.isDirectory)
|
|
76
|
+
continue;
|
|
77
|
+
const filename = entry.entryName;
|
|
78
|
+
const type = detectFileType(filename);
|
|
79
|
+
// Skip non-relevant files
|
|
80
|
+
if (type === 'other')
|
|
81
|
+
continue;
|
|
82
|
+
try {
|
|
83
|
+
const content = entry.getData().toString('utf8');
|
|
84
|
+
const text = `${filename} ${content}`;
|
|
85
|
+
const topics = detectTopics(text, zipTopicKeywords);
|
|
86
|
+
const hasCode = hasCodeContent(content);
|
|
87
|
+
patterns.push({
|
|
88
|
+
filename,
|
|
89
|
+
content,
|
|
90
|
+
type,
|
|
91
|
+
hasCode,
|
|
92
|
+
topics,
|
|
93
|
+
});
|
|
94
|
+
count++;
|
|
95
|
+
}
|
|
96
|
+
catch (err) {
|
|
97
|
+
warnings.push(`Failed to read ${filename}: ${err}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { success: true, patterns, warnings };
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
patterns: [],
|
|
106
|
+
warnings: [`Extraction failed: ${error}`],
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export async function extractFromAttachment(attachmentUrl, postId, accessToken) {
|
|
111
|
+
const zipPath = await downloadZip(attachmentUrl, postId, accessToken);
|
|
112
|
+
if (!zipPath) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
patterns: [],
|
|
116
|
+
warnings: ['Failed to download zip attachment'],
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return extractZip(zipPath, postId);
|
|
120
|
+
}
|
|
121
|
+
export function clearZipCache() {
|
|
122
|
+
const cacheDir = getCacheDir('zips');
|
|
123
|
+
if (fs.existsSync(cacheDir)) {
|
|
124
|
+
fs.rmSync(cacheDir, { recursive: true });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=patreon-zip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patreon-zip.js","sourceRoot":"","sources":["../../../src/sources/premium/patreon-zip.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface PatreonPattern {
|
|
2
|
+
id: string;
|
|
3
|
+
title: string;
|
|
4
|
+
url: string;
|
|
5
|
+
publishDate: string;
|
|
6
|
+
excerpt: string;
|
|
7
|
+
content: string;
|
|
8
|
+
creator: string;
|
|
9
|
+
topics: string[];
|
|
10
|
+
relevanceScore: number;
|
|
11
|
+
hasCode: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface CreatorInfo {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
url: string;
|
|
17
|
+
isSwiftRelated: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare class PatreonSource {
|
|
20
|
+
private clientId;
|
|
21
|
+
private clientSecret;
|
|
22
|
+
private enabledCreators;
|
|
23
|
+
constructor();
|
|
24
|
+
private loadEnabledCreators;
|
|
25
|
+
saveEnabledCreators(creatorIds: string[]): void;
|
|
26
|
+
isConfigured(): Promise<boolean>;
|
|
27
|
+
/**
|
|
28
|
+
* Returns creators the user PAYS (patron memberships).
|
|
29
|
+
*
|
|
30
|
+
* IMPORTANT: This uses the identity endpoint with memberships.campaign include,
|
|
31
|
+
* NOT the /campaigns endpoint. The /campaigns endpoint only returns campaigns
|
|
32
|
+
* you OWN as a creator, not campaigns you subscribe to as a patron.
|
|
33
|
+
*
|
|
34
|
+
* Correct API: GET /identity?include=memberships.campaign
|
|
35
|
+
*/
|
|
36
|
+
getSubscribedCreators(): Promise<CreatorInfo[]>;
|
|
37
|
+
detectSwiftCreators(): Promise<CreatorInfo[]>;
|
|
38
|
+
fetchPatterns(creatorId?: string): Promise<PatreonPattern[]>;
|
|
39
|
+
/**
|
|
40
|
+
* Convert downloaded post to patterns
|
|
41
|
+
*/
|
|
42
|
+
private downloadedPostToPatterns;
|
|
43
|
+
private videoToPattern;
|
|
44
|
+
searchPatterns(query: string): Promise<PatreonPattern[]>;
|
|
45
|
+
isAvailable(): boolean;
|
|
46
|
+
}
|
|
47
|
+
export default PatreonSource;
|
|
48
|
+
//# sourceMappingURL=patreon.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patreon.d.ts","sourceRoot":"","sources":["../../../src/sources/premium/patreon.ts"],"names":[],"mappings":"AAgBA,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,cAAc,EAAE,OAAO,CAAC;CACzB;AA+CD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAAgB;;IAQvC,OAAO,CAAC,mBAAmB;IAY3B,mBAAmB,CAAC,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI;IAOzC,YAAY,IAAI,OAAO,CAAC,OAAO,CAAC;IAMtC;;;;;;;;OAQG;IACG,qBAAqB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IA0D/C,mBAAmB,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;IAK7C,aAAa,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAoDlE;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA8BhC,OAAO,CAAC,cAAc;IAoBhB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAqB9D,WAAW,IAAI,OAAO;CAGvB;AAED,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// src/sources/premium/patreon.ts
|
|
2
|
+
import { loadTokens, getValidAccessToken } from './patreon-oauth.js';
|
|
3
|
+
import { getChannelVideos, searchVideos } from './youtube.js';
|
|
4
|
+
import { scanDownloadedContent } from './patreon-dl.js';
|
|
5
|
+
import { getByPatreonId } from '../../config/creators.js';
|
|
6
|
+
import { getPatreonCreatorsPath } from '../../utils/paths.js';
|
|
7
|
+
import { detectTopics, hasCodeContent, calculateRelevance } from '../../utils/swift-analysis.js';
|
|
8
|
+
import { createSourceConfig } from '../../config/swift-keywords.js';
|
|
9
|
+
import { logError } from '../../utils/errors.js';
|
|
10
|
+
import { fetch } from '../../utils/fetch.js';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
const PATREON_API = 'https://www.patreon.com/api/oauth2/v2';
|
|
14
|
+
const { topicKeywords: patreonTopicKeywords, qualitySignals: patreonQualitySignals } = createSourceConfig({ 'swiftui': ['@observable'], 'architecture': ['clean architecture'] }, { 'swift': 10, 'ios': 8, 'pattern': 6, 'best practice': 8 });
|
|
15
|
+
// Patreon-specific scoring constants
|
|
16
|
+
const PATREON_CODE_BONUS = 15; // Higher bonus for code-heavy Patreon content
|
|
17
|
+
const PATREON_BASE_SCORE = 0; // Start at 0 for Patreon to rely on quality signals
|
|
18
|
+
function isSwiftRelated(name, summary) {
|
|
19
|
+
const text = `${name} ${summary || ''}`.toLowerCase();
|
|
20
|
+
const keywords = ['swift', 'swiftui', 'ios', 'apple', 'xcode', 'uikit', 'iphone', 'ipad'];
|
|
21
|
+
return keywords.some(k => text.includes(k));
|
|
22
|
+
}
|
|
23
|
+
export class PatreonSource {
|
|
24
|
+
clientId;
|
|
25
|
+
clientSecret;
|
|
26
|
+
enabledCreators = [];
|
|
27
|
+
constructor() {
|
|
28
|
+
this.clientId = process.env.PATREON_CLIENT_ID || '';
|
|
29
|
+
this.clientSecret = process.env.PATREON_CLIENT_SECRET || '';
|
|
30
|
+
this.loadEnabledCreators();
|
|
31
|
+
}
|
|
32
|
+
loadEnabledCreators() {
|
|
33
|
+
try {
|
|
34
|
+
const configPath = getPatreonCreatorsPath();
|
|
35
|
+
if (fs.existsSync(configPath)) {
|
|
36
|
+
const data = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
37
|
+
this.enabledCreators = data.enabledCreators || [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
this.enabledCreators = [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
saveEnabledCreators(creatorIds) {
|
|
45
|
+
this.enabledCreators = creatorIds;
|
|
46
|
+
const configPath = getPatreonCreatorsPath();
|
|
47
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
48
|
+
fs.writeFileSync(configPath, JSON.stringify({ enabledCreators: creatorIds }, null, 2));
|
|
49
|
+
}
|
|
50
|
+
async isConfigured() {
|
|
51
|
+
if (!this.clientId || !this.clientSecret)
|
|
52
|
+
return false;
|
|
53
|
+
const tokens = await loadTokens();
|
|
54
|
+
return tokens !== null;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Returns creators the user PAYS (patron memberships).
|
|
58
|
+
*
|
|
59
|
+
* IMPORTANT: This uses the identity endpoint with memberships.campaign include,
|
|
60
|
+
* NOT the /campaigns endpoint. The /campaigns endpoint only returns campaigns
|
|
61
|
+
* you OWN as a creator, not campaigns you subscribe to as a patron.
|
|
62
|
+
*
|
|
63
|
+
* Correct API: GET /identity?include=memberships.campaign
|
|
64
|
+
*/
|
|
65
|
+
async getSubscribedCreators() {
|
|
66
|
+
const accessToken = await getValidAccessToken(this.clientId, this.clientSecret);
|
|
67
|
+
if (!accessToken)
|
|
68
|
+
return [];
|
|
69
|
+
try {
|
|
70
|
+
const url = `${PATREON_API}/identity?include=memberships.campaign&fields[user]=full_name,email&fields[member]=patron_status&fields[campaign]=creation_name,url,summary`;
|
|
71
|
+
const response = await fetch(url, {
|
|
72
|
+
headers: { 'Authorization': `Bearer ${accessToken}` }
|
|
73
|
+
});
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
logError('Patreon', `Failed to fetch memberships: ${response.status}`);
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
const data = await response.json();
|
|
79
|
+
if (!data.included) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
// Extract active memberships and their campaigns
|
|
83
|
+
const members = data.included.filter((item) => item.type === 'member' &&
|
|
84
|
+
item.attributes?.patron_status === 'active_patron');
|
|
85
|
+
const campaigns = data.included.filter((item) => item.type === 'campaign');
|
|
86
|
+
// Get campaign IDs from active memberships
|
|
87
|
+
const activeCampaignIds = new Set(members
|
|
88
|
+
.map(m => m.relationships?.campaign?.data?.id)
|
|
89
|
+
.filter((id) => !!id));
|
|
90
|
+
// Return campaigns user is actively subscribed to
|
|
91
|
+
return campaigns
|
|
92
|
+
.filter(c => activeCampaignIds.has(c.id))
|
|
93
|
+
.map(campaign => ({
|
|
94
|
+
id: campaign.id,
|
|
95
|
+
name: campaign.attributes.creation_name,
|
|
96
|
+
url: campaign.attributes.url,
|
|
97
|
+
isSwiftRelated: isSwiftRelated(campaign.attributes.creation_name, campaign.attributes.summary),
|
|
98
|
+
}));
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
logError('Patreon', error);
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async detectSwiftCreators() {
|
|
106
|
+
const creators = await this.getSubscribedCreators();
|
|
107
|
+
return creators.filter(c => c.isSwiftRelated);
|
|
108
|
+
}
|
|
109
|
+
async fetchPatterns(creatorId) {
|
|
110
|
+
const patterns = [];
|
|
111
|
+
const creatorsToFetch = creatorId
|
|
112
|
+
? [creatorId]
|
|
113
|
+
: this.enabledCreators;
|
|
114
|
+
// 1. Scan locally downloaded content (from patreon-dl)
|
|
115
|
+
const downloadedPosts = scanDownloadedContent();
|
|
116
|
+
for (const post of downloadedPosts) {
|
|
117
|
+
// Filter by creator if specified
|
|
118
|
+
const creator = creatorsToFetch.length > 0
|
|
119
|
+
? getByPatreonId(creatorsToFetch.find(id => {
|
|
120
|
+
const c = getByPatreonId(id);
|
|
121
|
+
return c?.name === post.creator;
|
|
122
|
+
}) || '')
|
|
123
|
+
: null;
|
|
124
|
+
if (creatorsToFetch.length > 0 && !creator)
|
|
125
|
+
continue;
|
|
126
|
+
patterns.push(...this.downloadedPostToPatterns(post));
|
|
127
|
+
}
|
|
128
|
+
// 2. Fetch YouTube videos for additional metadata
|
|
129
|
+
for (const patreonId of creatorsToFetch) {
|
|
130
|
+
const creator = getByPatreonId(patreonId);
|
|
131
|
+
if (!creator?.youtubeChannelId)
|
|
132
|
+
continue;
|
|
133
|
+
try {
|
|
134
|
+
const videos = await getChannelVideos(creator.youtubeChannelId, 50);
|
|
135
|
+
for (const video of videos) {
|
|
136
|
+
// Add video as pattern (skip if we already have downloaded content for this)
|
|
137
|
+
const hasDownloaded = patterns.some(p => p.url === video.patreonLink || p.title.includes(video.title));
|
|
138
|
+
if (!hasDownloaded) {
|
|
139
|
+
patterns.push(this.videoToPattern(video, creator.name));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
logError('Patreon', error, { creator: creator.name });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
// Sort by date (newest first)
|
|
148
|
+
patterns.sort((a, b) => new Date(b.publishDate).getTime() - new Date(a.publishDate).getTime());
|
|
149
|
+
return patterns;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Convert downloaded post to patterns
|
|
153
|
+
*/
|
|
154
|
+
downloadedPostToPatterns(post) {
|
|
155
|
+
const patterns = [];
|
|
156
|
+
for (const file of post.files) {
|
|
157
|
+
if (file.type === 'other')
|
|
158
|
+
continue;
|
|
159
|
+
const content = file.content || '';
|
|
160
|
+
const text = `${file.filename} ${content}`;
|
|
161
|
+
const topics = detectTopics(text, patreonTopicKeywords);
|
|
162
|
+
const hasCode = file.type === 'swift' || hasCodeContent(content);
|
|
163
|
+
const relevanceScore = calculateRelevance(text, hasCode, patreonQualitySignals, PATREON_BASE_SCORE, PATREON_CODE_BONUS);
|
|
164
|
+
patterns.push({
|
|
165
|
+
id: `dl-${post.postId}-${file.filename}`,
|
|
166
|
+
title: `${post.title} - ${file.filename}`,
|
|
167
|
+
url: `file://${file.filepath}`,
|
|
168
|
+
publishDate: post.publishDate || new Date().toISOString(),
|
|
169
|
+
excerpt: content.substring(0, 300),
|
|
170
|
+
content,
|
|
171
|
+
creator: post.creator,
|
|
172
|
+
topics,
|
|
173
|
+
relevanceScore,
|
|
174
|
+
hasCode,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return patterns;
|
|
178
|
+
}
|
|
179
|
+
videoToPattern(video, creatorName) {
|
|
180
|
+
const text = `${video.title} ${video.description}`;
|
|
181
|
+
const topics = detectTopics(text, patreonTopicKeywords);
|
|
182
|
+
const hasCode = hasCodeContent(video.description) || (video.codeLinks?.length ?? 0) > 0;
|
|
183
|
+
const relevanceScore = calculateRelevance(text, hasCode, patreonQualitySignals, PATREON_BASE_SCORE, PATREON_CODE_BONUS);
|
|
184
|
+
return {
|
|
185
|
+
id: `yt-${video.id}`,
|
|
186
|
+
title: video.title,
|
|
187
|
+
url: video.patreonLink || `https://youtube.com/watch?v=${video.id}`,
|
|
188
|
+
publishDate: video.publishedAt,
|
|
189
|
+
excerpt: video.description.substring(0, 300),
|
|
190
|
+
content: video.description,
|
|
191
|
+
creator: creatorName,
|
|
192
|
+
topics,
|
|
193
|
+
relevanceScore,
|
|
194
|
+
hasCode,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
async searchPatterns(query) {
|
|
198
|
+
const patterns = [];
|
|
199
|
+
// Search YouTube for each enabled creator
|
|
200
|
+
for (const patreonId of this.enabledCreators) {
|
|
201
|
+
const creator = getByPatreonId(patreonId);
|
|
202
|
+
if (!creator?.youtubeChannelId)
|
|
203
|
+
continue;
|
|
204
|
+
try {
|
|
205
|
+
const videos = await searchVideos(query, creator.youtubeChannelId, 25);
|
|
206
|
+
for (const video of videos) {
|
|
207
|
+
patterns.push(this.videoToPattern(video, creator.name));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch (error) {
|
|
211
|
+
logError('Patreon', error, { creator: creator.name, query });
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return patterns;
|
|
215
|
+
}
|
|
216
|
+
isAvailable() {
|
|
217
|
+
return !!(this.clientId && this.clientSecret);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export default PatreonSource;
|
|
221
|
+
//# sourceMappingURL=patreon.js.map
|