Skip to content

Commit

Permalink
attempt to bluesky auth
Browse files Browse the repository at this point in the history
  • Loading branch information
jerdog committed Dec 5, 2024
1 parent 5f510f2 commit d31ecf9
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 37 deletions.
46 changes: 25 additions & 21 deletions bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -487,12 +487,16 @@ async function fetchRecentPosts() {

async function getBlueskyAuth() {
try {
debug(`Authenticating with Bluesky using: ${CONFIG.bluesky.identifier}`, 'verbose');

// Validate credentials
if (!CONFIG.bluesky.identifier || !CONFIG.bluesky.password) {
throw new Error('Missing Bluesky credentials');
// Ensure config is loaded first
if (!CONFIG) {
CONFIG = await loadConfig();
}

if (!CONFIG?.bluesky?.identifier || !CONFIG?.bluesky?.password || !CONFIG?.bluesky?.service) {
throw new Error('Missing Bluesky configuration');
}

debug(`Authenticating with Bluesky using: ${CONFIG.bluesky.identifier}`, 'verbose');

if (!validateBlueskyUsername(CONFIG.bluesky.identifier)) {
throw new Error('Invalid Bluesky username format. Should be handle.bsky.social or handle.domain.tld');
Expand All @@ -503,7 +507,7 @@ async function getBlueskyAuth() {
'password': CONFIG.bluesky.password
};

debug('Sending Bluesky auth request...', 'verbose', authData);
debug('Sending Bluesky auth request...', 'verbose');

const response = await fetch(`${CONFIG.bluesky.service}/xrpc/com.atproto.server.createSession`, {
method: 'POST',
Expand Down Expand Up @@ -610,26 +614,26 @@ async function postToMastodon(content) {

async function postToBluesky(content) {
try {
// Get existing auth if available
let auth = blueskyAuth;

// If no auth or expired, create new session
if (!auth) {
auth = await getBlueskyAuth();
if (!auth) {
throw new Error('Failed to authenticate with Bluesky');
}
blueskyAuth = auth;
// Get auth token
const authToken = await getBlueskyAuth();
if (!authToken) {
throw new Error('Failed to authenticate with Bluesky');
}

// Get user DID
const did = await getBlueskyDid();
if (!did) {
throw new Error('Failed to get Bluesky DID');
}

const response = await fetch(`${process.env.BLUESKY_API_URL}/xrpc/com.atproto.repo.createRecord`, {
const response = await fetch(`${CONFIG.bluesky.service}/xrpc/com.atproto.repo.createRecord`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${auth.accessJwt}`,
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
repo: auth.did,
repo: did,
collection: 'app.bsky.feed.post',
record: {
text: content,
Expand All @@ -639,7 +643,8 @@ async function postToBluesky(content) {
});

if (!response.ok) {
throw new Error(`Failed to post to Bluesky: ${response.statusText}`);
const errorData = await response.text();
throw new Error(`Failed to post to Bluesky: ${errorData}`);
}

const data = await response.json();
Expand All @@ -648,7 +653,6 @@ async function postToBluesky(content) {
return true;
} catch (error) {
debug('Error posting to Bluesky:', 'error', error);
blueskyAuth = null; // Clear auth on error
return false;
}
}
Expand Down
97 changes: 91 additions & 6 deletions replies.js
Original file line number Diff line number Diff line change
Expand Up @@ -123,9 +123,53 @@ export function getOriginalPost(platform, postId) {
return recentPosts.get(`${platform}:${postId}`);
}

// Track rate limit state
const rateLimitState = {
lastError: null,
backoffMinutes: 5,
maxBackoffMinutes: 60
};

// Fallback responses when rate limited
const fallbackResponses = [
"My AI brain needs a quick nap! 😴 I'll be back with witty responses soon!",
"Taking a brief creative break! Check back in a bit for more banter! 🎭",

Check failure on line 136 in replies.js

View workflow job for this annotation

GitHub Actions / test (20.x)

Strings must use singlequote
"Currently recharging my joke batteries! 🔋 Will be back with fresh material soon!",

Check failure on line 137 in replies.js

View workflow job for this annotation

GitHub Actions / test (20.x)

Strings must use singlequote
"In a brief meditation session to expand my wit! 🧘‍♂️ Back shortly!",

Check failure on line 138 in replies.js

View workflow job for this annotation

GitHub Actions / test (20.x)

Strings must use singlequote
"Temporarily out of clever responses - but I'll return with double the humor! ✨",
"My pun generator is cooling down! Will be back with more wordplay soon! 🎮",

Check failure on line 140 in replies.js

View workflow job for this annotation

GitHub Actions / test (20.x)

Strings must use singlequote
"Taking a quick comedy workshop! Back soon with fresh material! 🎭",

Check failure on line 141 in replies.js

View workflow job for this annotation

GitHub Actions / test (20.x)

Strings must use singlequote
"Briefly offline doing stand-up practice! 🎤 Return in a bit for the good stuff!"

Check failure on line 142 in replies.js

View workflow job for this annotation

GitHub Actions / test (20.x)

Strings must use singlequote
];

function getFallbackResponse() {
const randomIndex = Math.floor(Math.random() * fallbackResponses.length);
return fallbackResponses[randomIndex];
}

// Generate a reply using ChatGPT
export async function generateReply(originalPost, replyContent) {
try {
// Check if we're currently rate limited
if (rateLimitState.lastError) {
const timeSinceError = Date.now() - rateLimitState.lastError;
const backoffMs = rateLimitState.backoffMinutes * 60 * 1000;

if (timeSinceError < backoffMs) {
const waitMinutes = Math.ceil((backoffMs - timeSinceError) / (60 * 1000));
debug('Still rate limited, using fallback response', 'info', {
waitMinutes,
backoffMinutes: rateLimitState.backoffMinutes
});
return getFallbackResponse();
} else {
// Reset rate limit state
rateLimitState.lastError = null;
rateLimitState.backoffMinutes = 5;
debug('Rate limit period expired, retrying', 'info');
}
}

debug('Generating reply with ChatGPT...', 'verbose', { originalPost, replyContent });
debug('Using API key:', 'verbose', process.env.OPENAI_API_KEY ? 'Present' : 'Missing');

Expand Down Expand Up @@ -155,6 +199,21 @@ export async function generateReply(originalPost, replyContent) {
if (!response.ok) {
const errorText = await response.text();
debug('ChatGPT API error:', 'error', errorText);

// Check for rate limit error
if (errorText.includes('rate limit') || response.status === 429) {
rateLimitState.lastError = Date.now();
// Double the backoff time for next attempt, up to max
rateLimitState.backoffMinutes = Math.min(
rateLimitState.backoffMinutes * 2,
rateLimitState.maxBackoffMinutes
);
debug('Rate limit hit, using fallback response', 'warn', {
nextBackoffMinutes: rateLimitState.backoffMinutes
});
return getFallbackResponse();
}

throw new Error(`ChatGPT API error: ${response.status} ${response.statusText}`);
}

Expand All @@ -167,7 +226,8 @@ export async function generateReply(originalPost, replyContent) {
throw new Error('Invalid response from ChatGPT');
} catch (error) {
debug('Error generating reply with ChatGPT:', 'error', error);
return null;
// Use fallback response for any errors
return getFallbackResponse();
}
}

Expand Down Expand Up @@ -212,41 +272,58 @@ export async function handleMastodonReply(notification) {
// Handle replies on Bluesky
export async function handleBlueskyReply(notification) {
try {
debug('Processing Bluesky notification', 'verbose', notification);
debug('Processing Bluesky notification for reply', 'info', {
author: notification.author.handle,
text: notification.record?.text?.substring(0, 50) + '...',
uri: notification.uri,
replyParent: notification.reply?.parent?.uri
});

// Get auth session
const auth = await getBlueskyAuth();
if (!auth) {
debug('Failed to authenticate with Bluesky', 'error');
throw new Error('Failed to authenticate with Bluesky');
}

// Check if this is a reply to our post
const replyToUri = notification.reply?.parent?.uri;
if (!replyToUri) {
debug('Not a reply, skipping', 'verbose');
debug('Not a reply, skipping', 'info', { notification });
return;
}

debug('Checking if reply is to our post', 'info', { replyToUri });
const originalPost = getOriginalPost('bluesky', replyToUri);
if (!originalPost) {
debug('Not a reply to our post, skipping', 'verbose');
debug('Not a reply to our post, skipping', 'info', { replyToUri });
return;
}

// Get the reply content
const replyContent = notification.record?.text;
if (!replyContent) {
debug('No reply content found', 'verbose');
debug('No reply content found', 'info', { notification });
return;
}

debug('Generating reply', 'info', {
originalPost: originalPost.content.substring(0, 50) + '...',
replyContent: replyContent.substring(0, 50) + '...'
});

// Generate a witty reply
const reply = await generateReply(originalPost.content, replyContent);
if (!reply) {
debug('Failed to generate reply', 'error');
return;
}

debug('Posting reply to Bluesky', 'info', {
reply: reply.substring(0, 50) + '...',
replyTo: notification.uri
});

// Post the reply
const response = await fetch(`${process.env.BLUESKY_API_URL}/xrpc/com.atproto.repo.createRecord`, {
method: 'POST',
Expand All @@ -273,10 +350,18 @@ export async function handleBlueskyReply(notification) {

if (!response.ok) {
const errorText = await response.text();
debug('Failed to post Bluesky reply', 'error', {
status: response.status,
statusText: response.statusText,
error: errorText
});
throw new Error(`Failed to post Bluesky reply: ${errorText}`);
}

debug('Successfully posted reply to Bluesky', 'info', { reply });
debug('Successfully posted reply to Bluesky', 'info', {
reply: reply.substring(0, 50) + '...',
replyTo: notification.uri
});
} catch (error) {
debug('Error handling Bluesky reply:', 'error', error);
}
Expand Down
72 changes: 62 additions & 10 deletions worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ async function checkNotifications(env) {
// Check Bluesky notifications
const auth = await getBlueskyAuth();
if (auth) {
debug('Fetching Bluesky notifications...', 'info');
const blueskyResponse = await fetch(`${process.env.BLUESKY_API_URL}/xrpc/app.bsky.notification.listNotifications?limit=50`, {
headers: {
'Authorization': `Bearer ${auth.accessJwt}`
Expand All @@ -61,17 +62,40 @@ async function checkNotifications(env) {
if (blueskyResponse.ok) {
const data = await blueskyResponse.json();
const notifications = data.notifications || [];
debug('Retrieved Bluesky notifications', 'info', {
totalCount: notifications.length,
notificationTypes: notifications.map(n => n.reason).filter((v, i, a) => a.indexOf(v) === i)
});

// Filter for valid reply notifications
const replyNotifications = notifications.filter(notif => {
debug('Processing notification', 'info', {
reason: notif.reason,
isRead: notif.isRead,
author: notif.author.handle,
text: notif.record?.text?.substring(0, 50) + '...',
uri: notif.uri,
indexedAt: notif.indexedAt
});

// Must be a reply and either unread or in debug mode
const isValidReply = notif.reason === 'reply' &&
(!notif.isRead || process.env.DEBUG_MODE === 'true');

if (!isValidReply) return false;
if (!isValidReply) {
debug('Skipping: Not a valid reply or already read', 'info', {
reason: notif.reason,
isRead: notif.isRead,
debugMode: process.env.DEBUG_MODE
});
return false;
}

// Don't reply to our own replies
if (notif.author.did === auth.did) return false;
if (notif.author.did === auth.did) {
debug('Skipping: Own reply', 'info', { authorDid: notif.author.did });
return false;
}

// Don't reply if the post contains certain keywords
const excludedWords = (process.env.EXCLUDED_WORDS || '').split(',')
Expand All @@ -80,15 +104,21 @@ async function checkNotifications(env) {

const replyText = notif.record?.text?.toLowerCase() || '';
if (excludedWords.some(word => replyText.includes(word))) {
debug('Skipping reply containing excluded word', 'verbose', { replyText });
debug('Skipping: Contains excluded word', 'info', {
replyText,
excludedWords
});
return false;
}

// Don't reply to replies older than 24 hours
const replyAge = Date.now() - new Date(notif.indexedAt).getTime();
const maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
if (replyAge > maxAge) {
debug('Skipping old reply', 'verbose', { indexedAt: notif.indexedAt });
debug('Skipping: Reply too old', 'info', {
indexedAt: notif.indexedAt,
age: Math.round(replyAge / (60 * 60 * 1000)) + ' hours'
});
return false;
}

Expand All @@ -98,27 +128,49 @@ async function checkNotifications(env) {
.filter(n => n.author.did === auth.did)
.filter(n => n.reply?.parent?.uri === threadParent);

debug('Thread analysis', 'info', {
threadParent,
ourRepliesInThread: threadReplies.length,
replyText: notif.record?.text?.substring(0, 50) + '...'
});

const isFirstReply = threadReplies.length === 0;

if (isFirstReply) {
// Always reply to the first interaction
debug('First reply in thread, will respond', 'verbose', { threadParent });
debug('First reply in thread, will respond', 'info', {
threadParent,
replyText: notif.record?.text?.substring(0, 50) + '...'
});
return true;
} else {
// 30% chance to reply to subsequent interactions
const replyChance = Math.random() * 100;
if (replyChance > 30) {
debug('Skipping subsequent reply due to random chance', 'verbose', { replyChance });
debug('Skipping: Random chance for subsequent reply', 'info', {
replyChance,
threadReplies: threadReplies.length
});
return false;
}
debug('Responding to subsequent reply', 'verbose', { replyChance });
debug('Responding to subsequent reply', 'info', {
replyChance,
threadReplies: threadReplies.length,
replyText: notif.record?.text?.substring(0, 50) + '...'
});
return true;
}
});

debug('Found valid replies to process', 'info', { count: replyNotifications.length });
debug('Reply notifications filtered', 'info', {
totalNotifications: notifications.length,
validReplies: replyNotifications.length
});

for (const notification of replyNotifications) {
debug('Processing reply notification', 'info', {
author: notification.author.handle,
text: notification.record?.text?.substring(0, 50) + '...',
uri: notification.uri
});
await handleBlueskyReply(notification);
}

Expand Down

0 comments on commit d31ecf9

Please sign in to comment.