A simple tool which lets you scrape twitter accounts and crosspost them to bluesky accounts! Comes with a CLI and a webapp for managing profiles! Works with images/videos/link embeds/threads.
13
fork

Configure Feed

Select the types of activity you want to include in your feed.

Default multi-source profile sync to first username

j4ckxyz c2e10731 1ef90d7c

+14 -88
+2 -6
src/cli.ts
··· 492 492 const sourceTwitterUsername = 493 493 requestedSource || 494 494 (storedSource && availableSources.includes(storedSource) ? storedSource : '') || 495 - (availableSources.length === 1 ? availableSources[0] : ''); 496 - 497 - if (!sourceTwitterUsername && availableSources.length > 1) { 498 - console.log('This mapping has multiple Twitter sources. Set profileSyncSourceUsername first with edit-mapping.'); 499 - return; 500 - } 495 + availableSources[0] || 496 + ''; 501 497 502 498 if (!sourceTwitterUsername) { 503 499 console.log('Mapping has no Twitter source usernames.');
+1 -3
src/config-manager.ts
··· 293 293 const resolvedProfileSyncSource = 294 294 profileSyncSourceUsername && usernames.includes(profileSyncSourceUsername) 295 295 ? profileSyncSourceUsername 296 - : usernames.length === 1 297 - ? usernames[0] 298 - : undefined; 296 + : usernames[0]; 299 297 const explicitCreator = normalizeString(record.createdByUserId) ?? normalizeString(record.ownerUserId); 300 298 const explicitCreatorExists = explicitCreator && users.some((user) => user.id === explicitCreator); 301 299
+1 -32
src/server.ts
··· 1242 1242 return resolved; 1243 1243 } 1244 1244 1245 - if (twitterUsernames.length === 1) { 1246 - return twitterUsernames[0]; 1247 - } 1248 - 1249 - return undefined; 1245 + return twitterUsernames[0]; 1250 1246 }; 1251 1247 1252 1248 const getMappingMirrorSyncState = (mapping: AccountMapping) => ({ ··· 2132 2128 requestedSource: req.body?.profileSyncSourceUsername, 2133 2129 }); 2134 2130 2135 - if (twitterUsernames.length > 1 && !profileSyncSourceUsername) { 2136 - res.status(400).json({ 2137 - error: 'Select which Twitter source should drive Bluesky profile sync for multi-source mappings.', 2138 - }); 2139 - return; 2140 - } 2141 - 2142 2131 const newMapping: AccountMapping = { 2143 2132 id: randomUUID(), 2144 2133 twitterUsernames, ··· 2233 2222 requestedSource: req.body?.profileSyncSourceUsername, 2234 2223 fallbackSource: existingMapping.profileSyncSourceUsername, 2235 2224 }); 2236 - const sourceWasExplicitlyProvided = req.body?.profileSyncSourceUsername !== undefined; 2237 - const usernamesWereUpdated = req.body?.twitterUsernames !== undefined; 2238 - 2239 - if ( 2240 - twitterUsernames.length > 1 && 2241 - !profileSyncSourceUsername && 2242 - (sourceWasExplicitlyProvided || usernamesWereUpdated) 2243 - ) { 2244 - res.status(400).json({ 2245 - error: 'Select which Twitter source should drive Bluesky profile sync for multi-source mappings.', 2246 - }); 2247 - return; 2248 - } 2249 2225 2250 2226 const updatedMapping: AccountMapping = { 2251 2227 ...existingMapping, ··· 2289 2265 }); 2290 2266 2291 2267 if (!sourceTwitterUsername) { 2292 - if (mapping.twitterUsernames.length > 1) { 2293 - res.status(400).json({ 2294 - error: 'Select a profile sync source username before syncing this multi-source mapping.', 2295 - }); 2296 - return; 2297 - } 2298 - 2299 2268 res.status(400).json({ error: 'Mapping has no Twitter source usernames.' }); 2300 2269 return; 2301 2270 }
+10 -47
web/src/App.tsx
··· 1493 1493 return; 1494 1494 } 1495 1495 1496 - const next = candidates.length === 1 ? candidates[0] || '' : ''; 1496 + const next = candidates[0] || ''; 1497 1497 if (editForm.profileSyncSourceUsername !== next) { 1498 1498 setEditForm((previous) => ({ ...previous, profileSyncSourceUsername: next })); 1499 1499 } ··· 2236 2236 return null; 2237 2237 } 2238 2238 2239 - if (candidates.length === 1) { 2240 - return candidates[0] || null; 2239 + const selected = normalizeTwitterUsername(mapping.profileSyncSourceUsername || ''); 2240 + if (selected && candidates.includes(selected)) { 2241 + return selected; 2241 2242 } 2242 2243 2243 - const selected = normalizeTwitterUsername(mapping.profileSyncSourceUsername || ''); 2244 - if (!selected || !candidates.includes(selected)) { 2245 - if (!silent) { 2246 - showNotice('error', 'Select a profile sync source for this multi-source mapping first.'); 2247 - } 2248 - return null; 2244 + if (!silent && candidates.length > 1) { 2245 + showNotice('info', `Using @${candidates[0] || ''} as the default profile sync source.`); 2249 2246 } 2250 2247 2251 - return selected; 2248 + return candidates[0] || null; 2252 2249 }; 2253 2250 2254 2251 const syncProfileFromTwitterForMapping = async ( ··· 2350 2347 return; 2351 2348 } 2352 2349 2353 - const missingSource = candidates.filter( 2354 - (mapping) => mapping.twitterUsernames.length > 1 && !resolveProfileSyncSource(mapping, true), 2355 - ); 2356 - if (missingSource.length > 0) { 2357 - const labels = missingSource 2358 - .slice(0, 3) 2359 - .map((mapping) => mapping.bskyIdentifier) 2360 - .join(', '); 2361 - const suffix = missingSource.length > 3 ? ` and ${missingSource.length - 3} more` : ''; 2362 - showNotice( 2363 - 'error', 2364 - `Sync all failed: choose a profile sync source for ${labels}${suffix} before running bulk sync.`, 2365 - ); 2366 - return; 2367 - } 2368 - 2369 2350 const confirmed = window.confirm(`Sync Twitter profile data for ${candidates.length} account(s), one at a time?`); 2370 2351 if (!confirmed) { 2371 2352 return; ··· 2418 2399 const sourceExists = mapping.twitterUsernames.some( 2419 2400 (username) => normalizeTwitterUsername(username) === normalizedSource, 2420 2401 ); 2421 - const nextSource = sourceExists 2422 - ? normalizedSource 2423 - : mapping.twitterUsernames.length === 1 2424 - ? normalizeTwitterUsername(mapping.twitterUsernames[0] || '') 2425 - : ''; 2426 - 2427 - if (mapping.twitterUsernames.length > 1 && !nextSource) { 2428 - showNotice('error', 'Select one of the mapped Twitter usernames as the profile sync source.'); 2429 - return; 2430 - } 2402 + const nextSource = sourceExists ? normalizedSource : normalizeTwitterUsername(mapping.twitterUsernames[0] || ''); 2431 2403 2432 2404 try { 2433 2405 await axios.put( ··· 3143 3115 bskyServiceUrl: mapping.bskyServiceUrl || 'https://bsky.social', 3144 3116 groupName: mapping.groupName || '', 3145 3117 groupEmoji: mapping.groupEmoji || '📁', 3146 - profileSyncSourceUsername: 3147 - mapping.profileSyncSourceUsername || 3148 - (mapping.twitterUsernames.length === 1 ? mapping.twitterUsernames[0] || '' : ''), 3118 + profileSyncSourceUsername: mapping.profileSyncSourceUsername || mapping.twitterUsernames[0] || '', 3149 3119 }); 3150 3120 setEditTwitterUsers(mapping.twitterUsernames); 3151 3121 setEditTwitterInput(''); ··· 3172 3142 ); 3173 3143 const profileSyncSourceUsername = hasSourceInMapping 3174 3144 ? normalizedProfileSyncSource 3175 - : editTwitterUsers.length === 1 3176 - ? normalizeTwitterUsername(editTwitterUsers[0] || '') 3177 - : ''; 3178 - 3179 - if (editTwitterUsers.length > 1 && !profileSyncSourceUsername) { 3180 - showNotice('error', 'Select which Twitter source should sync this Bluesky profile.'); 3181 - return; 3182 - } 3145 + : normalizeTwitterUsername(editTwitterUsers[0] || ''); 3183 3146 3184 3147 setIsBusy(true); 3185 3148