@ewanc26/supporters 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/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@ewanc26/supporters",
3
+ "version": "0.1.0",
4
+ "description": "SvelteKit component library for displaying Ko-fi supporters, backed by an ATProto PDS.",
5
+ "author": "Ewan Croft",
6
+ "license": "AGPL-3.0-only",
7
+ "files": [
8
+ "dist",
9
+ "lexicons",
10
+ "scripts",
11
+ "!dist/**/*.test.*",
12
+ "!dist/**/*.spec.*"
13
+ ],
14
+ "sideEffects": [
15
+ "**/*.css"
16
+ ],
17
+ "svelte": "./dist/index.js",
18
+ "types": "./dist/index.d.ts",
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "svelte": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "dependencies": {
28
+ "@ewanc26/tid": "^1.1.1"
29
+ },
30
+ "peerDependencies": {
31
+ "@atproto/api": ">=0.13.0",
32
+ "svelte": "^5.0.0"
33
+ },
34
+ "devDependencies": {
35
+ "@atproto/api": "^0.18.1",
36
+ "@sveltejs/adapter-auto": "^7.0.0",
37
+ "@sveltejs/kit": "^2.50.2",
38
+ "@sveltejs/package": "^2.5.7",
39
+ "@sveltejs/vite-plugin-svelte": "^6.2.4",
40
+ "@tailwindcss/typography": "^0.5.19",
41
+ "@tailwindcss/vite": "^4.1.18",
42
+ "prettier": "^3.8.1",
43
+ "prettier-plugin-svelte": "^3.4.1",
44
+ "prettier-plugin-tailwindcss": "^0.7.2",
45
+ "publint": "^0.3.17",
46
+ "svelte": "^5.51.0",
47
+ "svelte-check": "^4.4.2",
48
+ "tailwindcss": "^4.1.18",
49
+ "typescript": "^5.9.3",
50
+ "vite": "^7.3.1"
51
+ },
52
+ "keywords": [
53
+ "svelte",
54
+ "atproto",
55
+ "ko-fi",
56
+ "supporters",
57
+ "webhook"
58
+ ],
59
+ "publishConfig": {
60
+ "access": "public"
61
+ },
62
+ "scripts": {
63
+ "dev": "vite dev",
64
+ "build": "vite build && npm run prepack",
65
+ "preview": "vite preview",
66
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
67
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
68
+ "lint": "prettier --check .",
69
+ "format": "prettier --write ."
70
+ }
71
+ }
@@ -0,0 +1,189 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Import historical Ko-fi transaction data into your PDS.
4
+ *
5
+ * Export your transactions from: ko-fi.com/manage/transactions → Export CSV
6
+ *
7
+ * Usage:
8
+ * ATPROTO_DID=... ATPROTO_PDS_URL=... ATPROTO_APP_PASSWORD=... \
9
+ * node scripts/import-history.mjs /path/to/kofi-transactions.csv
10
+ *
11
+ * Flags:
12
+ * --dry-run Print what would be upserted without writing to the PDS.
13
+ * --skip N Skip the first N data rows (resume after a partial import).
14
+ *
15
+ * The script is idempotent — re-running it will merge new event types and
16
+ * tiers into existing records rather than creating duplicates.
17
+ */
18
+
19
+ import { createReadStream } from 'node:fs';
20
+ import { createInterface } from 'node:readline';
21
+ import { resolve } from 'node:path';
22
+
23
+ // ── Args ─────────────────────────────────────────────────────────────────────
24
+
25
+ const args = process.argv.slice(2);
26
+ const csvPath = args.find((a) => !a.startsWith('--'));
27
+ const dryRun = args.includes('--dry-run');
28
+ const skipArg = args.find((a) => a.startsWith('--skip='));
29
+ const skip = skipArg ? parseInt(skipArg.split('=')[1], 10) : 0;
30
+
31
+ if (!csvPath) {
32
+ console.error('Usage: node scripts/import-history.mjs <path-to-csv> [--dry-run] [--skip=N]');
33
+ process.exit(1);
34
+ }
35
+
36
+ // ── Ko-fi CSV column mapping ──────────────────────────────────────────────────
37
+ // Actual export headers (from ko-fi.com/manage/transactions):
38
+ // DateTime (UTC), From, Message, Item, Received, Given, Currency,
39
+ // TransactionType, TransactionId, Reference, SalesTax, ...
40
+ //
41
+ // TransactionType values seen in exports → our KofiEventType:
42
+ // "Tip" → "Donation"
43
+ // "Monthly Tip" → "Subscription"
44
+ // "Commission" → "Commission"
45
+ // "Shop Order" → "Shop Order"
46
+
47
+ const TYPE_MAP = {
48
+ 'tip': 'Donation',
49
+ 'monthly tip': 'Subscription',
50
+ 'commission': 'Commission',
51
+ 'shop order': 'Shop Order',
52
+ };
53
+
54
+ function normaliseType(raw) {
55
+ const key = raw.trim().toLowerCase();
56
+ return TYPE_MAP[key] ?? raw.trim();
57
+ }
58
+
59
+ // Ko-fi's CSV header names, lowercased + spaces collapsed to underscores.
60
+ // Used to map row fields by name rather than position.
61
+ const COL_TIMESTAMP = 'datetime_(utc)';
62
+ const COL_NAME = 'from';
63
+ const COL_TYPE = 'transactiontype';
64
+ const COL_TIER = 'item'; // only meaningful for subscriptions
65
+
66
+ // ── ATProto write ────────────────────────────────────────────────────────────
67
+
68
+ const COLLECTION = 'uk.ewancroft.kofi.supporter';
69
+
70
+ function requireEnv(key) {
71
+ const val = process.env[key];
72
+ if (!val) { console.error(`Missing env var: ${key}`); process.exit(1); }
73
+ return val;
74
+ }
75
+
76
+ const { generateTID } = await import('@ewanc26/tid');
77
+
78
+ async function appendEvent(agent, did, name, type, tier, timestamp) {
79
+ const rkey = generateTID(timestamp);
80
+ const record = { name, type, ...(tier ? { tier } : {}) };
81
+ await agent.com.atproto.repo.putRecord({ repo: did, collection: COLLECTION, rkey, record });
82
+ return rkey;
83
+ }
84
+
85
+ // ── CSV parser (no dependencies) ─────────────────────────────────────────────
86
+
87
+ function parseCSVLine(line) {
88
+ const fields = [];
89
+ let current = '';
90
+ let inQuotes = false;
91
+
92
+ for (let i = 0; i < line.length; i++) {
93
+ const ch = line[i];
94
+ if (ch === '"') {
95
+ if (inQuotes && line[i + 1] === '"') { current += '"'; i++; }
96
+ else { inQuotes = !inQuotes; }
97
+ } else if (ch === ',' && !inQuotes) {
98
+ fields.push(current.trim());
99
+ current = '';
100
+ } else {
101
+ current += ch;
102
+ }
103
+ }
104
+ fields.push(current.trim());
105
+ return fields;
106
+ }
107
+
108
+ // ── Main ─────────────────────────────────────────────────────────────────────
109
+
110
+ const { AtpAgent } = await import('@atproto/api');
111
+
112
+ let agent, did;
113
+
114
+ if (!dryRun) {
115
+ did = requireEnv('ATPROTO_DID');
116
+ const pdsUrl = requireEnv('ATPROTO_PDS_URL');
117
+ const password = requireEnv('ATPROTO_APP_PASSWORD');
118
+
119
+ agent = new AtpAgent({ service: pdsUrl });
120
+ await agent.login({ identifier: did, password });
121
+ console.log(`✓ Logged in as ${did}\n`);
122
+ } else {
123
+ console.log('DRY RUN — nothing will be written to the PDS.\n');
124
+ }
125
+
126
+ const rl = createInterface({ input: createReadStream(resolve(csvPath)), crlfDelay: Infinity });
127
+
128
+ let headers = null;
129
+ let rowIndex = 0;
130
+ let processed = 0;
131
+ let skipped = 0;
132
+ let errors = 0;
133
+
134
+ for await (const line of rl) {
135
+ if (!line.trim()) continue;
136
+
137
+ if (!headers) {
138
+ headers = parseCSVLine(line).map((h) => h.toLowerCase().replace(/\s+/g, '_'));
139
+ console.log(`Headers: ${headers.join(', ')}\n`);
140
+ continue;
141
+ }
142
+
143
+ rowIndex++;
144
+
145
+ if (rowIndex <= skip) {
146
+ skipped++;
147
+ continue;
148
+ }
149
+
150
+ const fields = parseCSVLine(line);
151
+ const row = Object.fromEntries(headers.map((h, i) => [h, fields[i] ?? '']));
152
+
153
+ const name = row[COL_NAME]?.trim();
154
+ const type = normaliseType(row[COL_TYPE] ?? '');
155
+ const rawTier = row[COL_TIER]?.trim();
156
+ // Only treat "Item" as a tier if it's a Subscription AND not the generic "Ko-fi Support" label.
157
+ const tier = type === 'Subscription' && rawTier && rawTier.toLowerCase() !== 'ko-fi support' ? rawTier : null;
158
+ // Ko-fi exports timestamps as "MM/DD/YYYY HH:MM" UTC — convert to ISO 8601.
159
+ const rawTs = row[COL_TIMESTAMP]?.trim();
160
+ const timestamp = rawTs ? new Date(rawTs + ' UTC').toISOString() : new Date().toISOString();
161
+
162
+ if (!name) {
163
+ console.warn(`Row ${rowIndex}: empty name, skipping.`);
164
+ errors++;
165
+ continue;
166
+ }
167
+
168
+ if (dryRun) {
169
+ const rkey = generateTID(timestamp);
170
+ console.log(`[${rowIndex}] ${rkey} ${name} · ${type}${tier ? ` · ${tier}` : ''}`);
171
+ processed++;
172
+ continue;
173
+ }
174
+
175
+ try {
176
+ const rkey = await appendEvent(agent, did, name, type, tier, timestamp);
177
+ console.log(`[${rowIndex}] ${rkey} ${name} · ${type}${tier ? ` · ${tier}` : ''}`);
178
+ processed++;
179
+ } catch (err) {
180
+ console.error(`[${rowIndex}] ERROR for "${name}": ${err.message}`);
181
+ errors++;
182
+ }
183
+ }
184
+
185
+ console.log(`\n─────────────────────────────`);
186
+ console.log(`Processed : ${processed}`);
187
+ if (skipped) console.log(`Skipped : ${skipped} (--skip=${skip})`);
188
+ if (errors) console.log(`Errors : ${errors}`);
189
+ console.log(`─────────────────────────────`);
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Probe the ko-fi.tools API for a given Ko-fi page ID.
4
+ *
5
+ * Usage:
6
+ * node scripts/probe-api.mjs YOUR_KOFI_PAGE_ID
7
+ *
8
+ * If the endpoint path is wrong, update API_BASE / path in src/lib/api.ts
9
+ * once the ko-fi.tools V2 docs are published.
10
+ */
11
+
12
+ const pageId = process.argv[2];
13
+
14
+ if (!pageId) {
15
+ console.error('Usage: node scripts/probe-api.mjs YOUR_KOFI_PAGE_ID');
16
+ process.exit(1);
17
+ }
18
+
19
+ const API_BASE = 'https://api.ko-fi.tools/v2';
20
+ const url = `${API_BASE}/${encodeURIComponent(pageId)}/supporters`;
21
+
22
+ console.log(`→ GET ${url}\n`);
23
+
24
+ try {
25
+ const res = await fetch(url, { headers: { Accept: 'application/json' } });
26
+ console.log(`Status: ${res.status} ${res.statusText}`);
27
+ console.log('Headers:', Object.fromEntries(res.headers.entries()));
28
+
29
+ const body = await res.text();
30
+ try {
31
+ console.log('\nBody (parsed):', JSON.stringify(JSON.parse(body), null, 2));
32
+ } catch {
33
+ console.log('\nBody (raw):', body);
34
+ }
35
+ } catch (err) {
36
+ console.error('Network error:', err.message);
37
+ }
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Probe multiple candidate ko-fi.tools API endpoint patterns.
4
+ * Usage: node scripts/probe-endpoints.mjs YOUR_KOFI_PAGE_ID
5
+ */
6
+
7
+ const pageId = process.argv[2] ?? 'ewancroft';
8
+
9
+ const candidates = [
10
+ `https://api.ko-fi.tools/v2/${pageId}/supporters`,
11
+ `https://api.ko-fi.tools/v2/${pageId}/top-supporters`,
12
+ `https://api.ko-fi.tools/v2/supporters/${pageId}`,
13
+ `https://api.ko-fi.tools/${pageId}/supporters`,
14
+ `https://api.ko-fi.tools/${pageId}`,
15
+ `https://api.ko-fi.tools/v2/${pageId}`,
16
+ `https://api.ko-fi.tools/v1/${pageId}/supporters`,
17
+ `https://api.ko-fi.tools/v1/${pageId}/top`,
18
+ `https://ko-fi.tools/api/${pageId}/supporters`,
19
+ `https://ko-fi.tools/api/v2/${pageId}/supporters`,
20
+ ];
21
+
22
+ for (const url of candidates) {
23
+ try {
24
+ const res = await fetch(url, { headers: { Accept: 'application/json' } });
25
+ const body = await res.text();
26
+ let parsed;
27
+ try { parsed = JSON.parse(body); } catch { parsed = body.slice(0, 120); }
28
+ console.log(`${res.status} ${url}`);
29
+ if (res.status !== 404) console.log(' →', JSON.stringify(parsed).slice(0, 200));
30
+ } catch (e) {
31
+ console.log(`ERR ${url} — ${e.message}`);
32
+ }
33
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Simulate Ko-fi webhook POSTs against your local dev server.
4
+ *
5
+ * Usage:
6
+ * KOFI_VERIFICATION_TOKEN=your-token node scripts/simulate-webhook.mjs [type] [name] [tier]
7
+ *
8
+ * Types: Donation | Subscription | Commission | "Shop Order"
9
+ *
10
+ * Examples:
11
+ * node scripts/simulate-webhook.mjs Donation "Jo Example"
12
+ * node scripts/simulate-webhook.mjs Subscription "Alice" "Lunar Contributors"
13
+ * node scripts/simulate-webhook.mjs Commission "Bob"
14
+ * node scripts/simulate-webhook.mjs "Shop Order" "Carol"
15
+ */
16
+
17
+ const token = process.env.KOFI_VERIFICATION_TOKEN ?? 'test-token';
18
+ const type = process.argv[2] ?? 'Donation';
19
+ const name = process.argv[3] ?? 'Jo Example';
20
+ const tier = process.argv[4] ?? (type === 'Subscription' ? 'Lunar Contributors' : null);
21
+ const url = process.argv[5] ?? 'http://localhost:5173/webhook';
22
+
23
+ const isSubscription = type === 'Subscription';
24
+
25
+ const payload = {
26
+ verification_token: token,
27
+ message_id: crypto.randomUUID(),
28
+ timestamp: new Date().toISOString(),
29
+ type,
30
+ is_public: true,
31
+ from_name: name,
32
+ message: 'Simulated webhook event',
33
+ amount: '3.00',
34
+ url: 'https://ko-fi.com/Home/CoffeeShop?txid=test',
35
+ email: 'test@example.com',
36
+ currency: 'GBP',
37
+ is_subscription_payment: isSubscription,
38
+ is_first_subscription_payment: isSubscription,
39
+ kofi_transaction_id: crypto.randomUUID(),
40
+ shop_items: type === 'Shop Order' ? [{ id: 'item-1', name: 'Test Item' }] : null,
41
+ tier_name: tier ?? null,
42
+ shipping: null
43
+ };
44
+
45
+ const body = new URLSearchParams({ data: JSON.stringify(payload) });
46
+
47
+ console.log(`→ POST ${url}`);
48
+ console.log(` type: ${type}`);
49
+ console.log(` from_name: ${name}`);
50
+ if (tier) console.log(` tier_name: ${tier}`);
51
+ console.log();
52
+
53
+ const res = await fetch(url, {
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
56
+ body
57
+ });
58
+
59
+ console.log(`Status: ${res.status} ${res.statusText}`);
60
+ if (res.status !== 200) {
61
+ console.log('Body:', await res.text());
62
+ }