Merge PR #48: feat: add Xiaohongshu source + Reddit public fallback
- Xiaohongshu search via local MCP service (opt-in, zero impact if service not running) - Reddit public JSON fallback (works with zero API keys) - Reddit priority: ScrapeCreators -> OpenAI -> public fallback - Updated env.py: Reddit always available via public fallback Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+91
@@ -0,0 +1,91 @@
|
||||
import { normalizeHandle } from '../lib/normalize-handle.js';
|
||||
import { TwitterClient } from '../lib/twitter-client.js';
|
||||
const ONLY_DIGITS_REGEX = /^\d+$/;
|
||||
async function resolveUserId(client, usernameOrId, ctx) {
|
||||
const raw = usernameOrId.trim();
|
||||
const isNumeric = ONLY_DIGITS_REGEX.test(raw);
|
||||
// Otherwise, treat as username and look up
|
||||
const handle = normalizeHandle(raw);
|
||||
if (handle) {
|
||||
const lookup = await client.getUserIdByUsername(handle);
|
||||
if (lookup.success && lookup.userId) {
|
||||
return { userId: lookup.userId, username: lookup.username };
|
||||
}
|
||||
if (!isNumeric) {
|
||||
console.error(`${ctx.p('err')}Failed to find user @${handle}: ${lookup.error ?? 'Unknown error'}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
if (isNumeric) {
|
||||
return { userId: raw };
|
||||
}
|
||||
console.error(`${ctx.p('err')}Invalid username: ${usernameOrId}`);
|
||||
return null;
|
||||
}
|
||||
export function registerFollowCommands(program, ctx) {
|
||||
program
|
||||
.command('follow')
|
||||
.description('Follow a user')
|
||||
.argument('<username-or-id>', 'Username (with or without @) or user ID to follow')
|
||||
.action(async (usernameOrId) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
const client = new TwitterClient({ cookies, timeoutMs });
|
||||
const resolved = await resolveUserId(client, usernameOrId, ctx);
|
||||
if (!resolved) {
|
||||
process.exit(1);
|
||||
}
|
||||
const { userId, username } = resolved;
|
||||
const displayName = username ? `@${username}` : userId;
|
||||
const result = await client.follow(userId);
|
||||
if (result.success) {
|
||||
const finalName = result.username ? `@${result.username}` : displayName;
|
||||
console.log(`${ctx.p('ok')}Now following ${finalName}`);
|
||||
}
|
||||
else {
|
||||
console.error(`${ctx.p('err')}Failed to follow ${displayName}: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
program
|
||||
.command('unfollow')
|
||||
.description('Unfollow a user')
|
||||
.argument('<username-or-id>', 'Username (with or without @) or user ID to unfollow')
|
||||
.action(async (usernameOrId) => {
|
||||
const opts = program.opts();
|
||||
const timeoutMs = ctx.resolveTimeoutFromOptions(opts);
|
||||
const { cookies, warnings } = await ctx.resolveCredentialsFromOptions(opts);
|
||||
for (const warning of warnings) {
|
||||
console.error(`${ctx.p('warn')}${warning}`);
|
||||
}
|
||||
if (!cookies.authToken || !cookies.ct0) {
|
||||
console.error(`${ctx.p('err')}Missing required credentials`);
|
||||
process.exit(1);
|
||||
}
|
||||
const client = new TwitterClient({ cookies, timeoutMs });
|
||||
const resolved = await resolveUserId(client, usernameOrId, ctx);
|
||||
if (!resolved) {
|
||||
process.exit(1);
|
||||
}
|
||||
const { userId, username } = resolved;
|
||||
const displayName = username ? `@${username}` : userId;
|
||||
const result = await client.unfollow(userId);
|
||||
if (result.success) {
|
||||
const finalName = result.username ? `@${result.username}` : displayName;
|
||||
console.log(`${ctx.p('ok')}Unfollowed ${finalName}`);
|
||||
}
|
||||
else {
|
||||
console.error(`${ctx.p('err')}Failed to unfollow ${displayName}: ${result.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
}
|
||||
//# sourceMappingURL=follow.js.map
|
||||
Reference in New Issue
Block a user