···1010class DiscordWebhook {
1111 constructor() {
1212 this.webhookUrl = config.discord?.webhookUrl;
1313+ this.enabled = config.discord?.enabled;
1414+1315 if (!this.webhookUrl) {
1416 console.warn('Discord webhook URL not configured. Discord integration disabled.');
1717+ } else if (this.enabled === false) {
1818+ console.log('Discord webhook URL is configured but integration is disabled in settings.');
1519 } else {
1620 console.log('Discord webhook integration initialized.');
1721 }
···2226 * @returns {boolean} - Whether Discord integration is enabled
2327 */
2428 isEnabled() {
2525- return !!this.webhookUrl;
2929+ // Only return true if both the webhook URL is set AND explicitly enabled in config
3030+ return !!this.webhookUrl && this.enabled !== false;
2631 }
27322833 /**
+138-21
scrapers/blueskyScraper.js
···1212 // Initialize with service endpoint only - no authentication needed for public posts
1313 this.serviceEndpoint = config.bluesky.service || 'https://bsky.social';
1414 // Update regex to support bsky.app, deer.social, and sky.thebull.app
1515- this.matcher = new RegExp('(?:https?://)?(?:bsky\\.app|deer\\.social|sky\\.thebull\\.app)/profile/(?<repo>\\S+)/post/(?<rkey>\\S+)');
1515+ // Also properly handle DIDs in the URL path
1616+ this.matcher = new RegExp('(?:https?://)?(?:bsky\\.app|deer\\.social|sky\\.thebull\\.app)/profile/(?<repo>[^/]+(?:/[^/]+)?)/post/(?<rkey>[^/]+)');
16171718 // Initialize the agent
1819 this.agent = new BskyAgent({ service: this.serviceEndpoint });
···2930 }
30313132 /**
3232- * Parse a Bluesky URL to extract handle and rkey (post ID)
3333+ * Parse a Bluesky URL to extract handle/DID and rkey (post ID)
3334 * @param {string} url - Bluesky URL
3434- * @returns {{repo: string, rkey: string}} - Extracted repo (handle) and rkey
3535+ * @returns {{repo: string, rkey: string}} - Extracted repo (handle/DID) and rkey
3536 */
3637 parseBlueskyUrl(url) {
3738 try {
···4041 throw new Error('Invalid Bluesky URL format');
4142 }
42434444+ let repo = matches.groups.repo;
4545+4646+ // Special handling for DIDs in the URL
4747+ // Example: did:plc:pmrzhfxlsflj5vb63h27jmax
4848+ if (repo.includes('/')) {
4949+ // This might be a URL-encoded DID, so extract just the DID part
5050+ const didMatches = repo.match(/^(did:[^/]+)/) || repo.match(/^([^/]+\/[^/]+)$/);
5151+ if (didMatches && didMatches[1]) {
5252+ repo = didMatches[1];
5353+ }
5454+ }
5555+4356 return {
4444- repo: matches.groups.repo,
5757+ repo: repo,
4558 rkey: matches.groups.rkey
4659 };
4760 } catch (error) {
···5063 }
51645265 /**
5353- * Get user's display name from their handle
5454- * @param {string} handle - The user's Bluesky handle
5555- * @returns {Promise<string>} - User's display name or handle if not found
6666+ * Get user's display name from their handle or DID
6767+ * @param {string} identifier - The user's Bluesky handle or DID
6868+ * @returns {Promise<string>} - User's display name or original identifier if not found
5669 */
5757- async getUserDisplayName(handle) {
7070+ async getUserDisplayName(identifier) {
5871 try {
5959- // Try to get user info from API
6060- const response = await axios.get(`${this.serviceEndpoint}/xrpc/app.bsky.actor.getProfile`, {
6161- params: { actor: handle },
6262- headers: { 'User-Agent': 'Stagehand/1.1.0' }
6363- });
7272+ let did = identifier;
64736565- if (response.data && response.data.displayName) {
6666- return response.data.displayName;
7474+ // Check if the identifier is a handle (contains a dot) rather than a DID
7575+ if (identifier.includes('.') && !identifier.startsWith('did:')) {
7676+ try {
7777+ // Convert handle to DID
7878+ const didResponse = await axios.get(`${this.serviceEndpoint}/xrpc/com.atproto.identity.resolveHandle`, {
7979+ params: { handle: identifier },
8080+ headers: { 'User-Agent': 'Stagehand/1.1.0' }
8181+ });
8282+8383+ if (didResponse.data && didResponse.data.did) {
8484+ did = didResponse.data.did;
8585+ } else {
8686+ console.log(`Failed to resolve handle to DID: ${identifier}`);
8787+ return identifier;
8888+ }
8989+ } catch (handleError) {
9090+ console.log(`Error resolving handle ${identifier}: ${handleError.message}`);
9191+ return identifier;
9292+ }
6793 }
68946969- // Return handle if display name not found
7070- return handle;
9595+ // Now we have a DID (either provided or resolved from handle)
9696+ try {
9797+ // Access the profile directly using repo API - this is the most reliable method
9898+ const profileUrl = new URL(`${this.serviceEndpoint}/xrpc/com.atproto.repo.getRecord`);
9999+ profileUrl.searchParams.append('repo', did);
100100+ profileUrl.searchParams.append('collection', 'app.bsky.actor.profile');
101101+ profileUrl.searchParams.append('rkey', 'self');
102102+103103+ const profileResponse = await axios.get(profileUrl.toString(), {
104104+ headers: { 'User-Agent': 'Stagehand/1.1.0' }
105105+ });
106106+107107+ if (profileResponse.data &&
108108+ profileResponse.data.value &&
109109+ profileResponse.data.value.displayName) {
110110+ return profileResponse.data.value.displayName;
111111+ }
112112+113113+ // If we got a profile but no displayName, try to get the handle
114114+ if (profileResponse.data &&
115115+ profileResponse.data.value &&
116116+ profileResponse.data.value.handle) {
117117+ return profileResponse.data.value.handle;
118118+ }
119119+ } catch (profileError) {
120120+ console.log(`Error getting profile for ${did}: ${profileError.message}`);
121121+122122+ // If that fails, try the actor getProfile API as a fallback
123123+ try {
124124+ const actorResponse = await axios.get(`${this.serviceEndpoint}/xrpc/app.bsky.actor.getProfile`, {
125125+ params: { actor: did },
126126+ headers: { 'User-Agent': 'Stagehand/1.1.0' }
127127+ });
128128+129129+ if (actorResponse.data && actorResponse.data.displayName) {
130130+ return actorResponse.data.displayName;
131131+ }
132132+133133+ if (actorResponse.data && actorResponse.data.handle) {
134134+ return actorResponse.data.handle;
135135+ }
136136+ } catch (actorError) {
137137+ console.log(`Error with actor getProfile API for ${did}: ${actorError.message}`);
138138+ }
139139+ }
140140+141141+ // Return the original identifier if all methods fail
142142+ return identifier;
71143 } catch (error) {
7272- console.log(`Couldn't get display name for ${handle}: ${error.message}`);
7373- return handle; // Fallback to handle on error
144144+ console.log(`Couldn't get display name for ${identifier}: ${error.message}`);
145145+ return identifier; // Fallback to identifier on error
74146 }
75147 }
76148···283355 }
284356285357 // 5. Get user's display name
286286- const displayName = await this.getUserDisplayName(repo);
287287- console.log(`Using display name: ${displayName} for handle: ${repo}`);
358358+ // First try to get the author's profile from the record itself if present
359359+ let displayName = repo; // Default to repo ID if we can't get a handle/display name
360360+ let handle = repo;
361361+ try {
362362+ // Try to get profile information directly from the repo
363363+ const profileUrl = new URL(`${this.serviceEndpoint}/xrpc/com.atproto.repo.getRecord`);
364364+ profileUrl.searchParams.append('repo', did);
365365+ profileUrl.searchParams.append('collection', 'app.bsky.actor.profile');
366366+ profileUrl.searchParams.append('rkey', 'self');
367367+368368+ const profileResponse = await axios.get(profileUrl.toString(), {
369369+ headers: { 'User-Agent': 'Stagehand/1.1.0' }
370370+ });
371371+372372+ if (profileResponse.data && profileResponse.data.value) {
373373+ // Extract display name and handle from the profile
374374+ if (profileResponse.data.value.displayName) {
375375+ displayName = profileResponse.data.value.displayName;
376376+ }
377377+ if (profileResponse.data.value.handle) {
378378+ handle = profileResponse.data.value.handle;
379379+ }
380380+ }
381381+ } catch (profileError) {
382382+ console.log(`Couldn't get profile directly: ${profileError.message}`);
383383+ // Fall back to resolving DID to handle
384384+ try {
385385+ const didToHandleUrl = new URL(`${this.serviceEndpoint}/xrpc/com.atproto.identity.resolveHandle`);
386386+ didToHandleUrl.searchParams.append('handle', did);
387387+388388+ const handleResponse = await axios.get(didToHandleUrl.toString(), {
389389+ headers: { 'User-Agent': 'Stagehand/1.1.0' }
390390+ });
391391+392392+ if (handleResponse.data && handleResponse.data.did) {
393393+ // We have a valid DID, now try to get the profile
394394+ const resolvedDisplayName = await this.getUserDisplayName(handleResponse.data.did);
395395+ if (resolvedDisplayName && resolvedDisplayName !== handleResponse.data.did) {
396396+ displayName = resolvedDisplayName;
397397+ }
398398+ }
399399+ } catch (didError) {
400400+ console.log(`Couldn't resolve DID to handle: ${didError.message}`);
401401+ }
402402+ }
403403+404404+ console.log(`Using display name: ${displayName} for handle: ${handle}`);
288405289406 // 6. Process based on embed type
290407 const embedType = record?.value?.embed?.$type;