Skip to content

Commit

Permalink
Merge pull request #2129 from elizaOS/tcm-fix-plugin-twitter
Browse files Browse the repository at this point in the history
fix: prevent repeated login by reusing client-twitter session
  • Loading branch information
shakkernerd authored Jan 10, 2025
2 parents defe7e4 + 38e076c commit 87d8eca
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 91 deletions.
43 changes: 2 additions & 41 deletions packages/client-twitter/src/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
stringToUuid,
TemplateType,
UUID,
truncateToCompleteSentence,
} from "@elizaos/core";
import { elizaLogger } from "@elizaos/core";
import { ClientBase } from "./base.ts";
Expand Down Expand Up @@ -77,40 +78,6 @@ Tweet:
# Respond with qualifying action tags only. Default to NO action unless extremely confident of relevance.` +
postActionResponseFooter;

/**
* Truncate text to fit within the Twitter character limit, ensuring it ends at a complete sentence.
*/
function truncateToCompleteSentence(
text: string,
maxTweetLength: number
): string {
if (text.length <= maxTweetLength) {
return text;
}

// Attempt to truncate at the last period within the limit
const lastPeriodIndex = text.lastIndexOf(".", maxTweetLength - 1);
if (lastPeriodIndex !== -1) {
const truncatedAtPeriod = text.slice(0, lastPeriodIndex + 1).trim();
if (truncatedAtPeriod.length > 0) {
return truncatedAtPeriod;
}
}

// If no period, truncate to the nearest whitespace within the limit
const lastSpaceIndex = text.lastIndexOf(" ", maxTweetLength - 1);
if (lastSpaceIndex !== -1) {
const truncatedAtSpace = text.slice(0, lastSpaceIndex).trim();
if (truncatedAtSpace.length > 0) {
return truncatedAtSpace + "...";
}
}

// Fallback: Hard truncate and add ellipsis
const hardTruncated = text.slice(0, maxTweetLength - 3).trim();
return hardTruncated + "...";
}

interface PendingTweet {
cleanedContent: string;
roomId: UUID;
Expand Down Expand Up @@ -399,7 +366,6 @@ export class TwitterPostClient {

async handleNoteTweet(
client: ClientBase,
runtime: IAgentRuntime,
content: string,
tweetId?: string
) {
Expand Down Expand Up @@ -465,11 +431,7 @@ export class TwitterPostClient {
let result;

if (cleanedContent.length > DEFAULT_MAX_TWEET_LENGTH) {
result = await this.handleNoteTweet(
client,
runtime,
cleanedContent
);
result = await this.handleNoteTweet(client, cleanedContent);
} else {
result = await this.sendStandardTweet(client, cleanedContent);
}
Expand Down Expand Up @@ -1204,7 +1166,6 @@ export class TwitterPostClient {
if (replyText.length > DEFAULT_MAX_TWEET_LENGTH) {
result = await this.handleNoteTweet(
this.client,
this.runtime,
replyText,
tweet.id
);
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/parsing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,37 @@ export const parseActionResponseFromText = (

return { actions };
};

/**
* Truncate text to fit within the character limit, ensuring it ends at a complete sentence.
*/
export function truncateToCompleteSentence(
text: string,
maxLength: number
): string {
if (text.length <= maxLength) {
return text;
}

// Attempt to truncate at the last period within the limit
const lastPeriodIndex = text.lastIndexOf(".", maxLength - 1);
if (lastPeriodIndex !== -1) {
const truncatedAtPeriod = text.slice(0, lastPeriodIndex + 1).trim();
if (truncatedAtPeriod.length > 0) {
return truncatedAtPeriod;
}
}

// If no period, truncate to the nearest whitespace within the limit
const lastSpaceIndex = text.lastIndexOf(" ", maxLength - 1);
if (lastSpaceIndex !== -1) {
const truncatedAtSpace = text.slice(0, lastSpaceIndex).trim();
if (truncatedAtSpace.length > 0) {
return truncatedAtSpace + "...";
}
}

// Fallback: Hard truncate and add ellipsis
const hardTruncated = text.slice(0, maxLength - 3).trim();
return hardTruncated + "...";
}
129 changes: 79 additions & 50 deletions packages/plugin-twitter/src/actions/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,14 @@ import {
elizaLogger,
ModelClass,
generateObject,
truncateToCompleteSentence,
} from "@elizaos/core";
import { Scraper } from "agent-twitter-client";
import { tweetTemplate } from "../templates";
import { isTweetContent, TweetSchema } from "../types";

export const DEFAULT_MAX_TWEET_LENGTH = 280;

async function composeTweet(
runtime: IAgentRuntime,
_message: Memory,
Expand Down Expand Up @@ -39,17 +42,15 @@ async function composeTweet(
return;
}

const trimmedContent = tweetContentObject.object.text.trim();
let trimmedContent = tweetContentObject.object.text.trim();

// Skip truncation if TWITTER_PREMIUM is true
if (
process.env.TWITTER_PREMIUM?.toLowerCase() !== "true" &&
trimmedContent.length > 180
) {
elizaLogger.warn(
`Tweet too long (${trimmedContent.length} chars), truncating...`
// Truncate the content to the maximum tweet length specified in the environment settings.
const maxTweetLength = runtime.getSetting("MAX_TWEET_LENGTH");
if (maxTweetLength) {
trimmedContent = truncateToCompleteSentence(
trimmedContent,
Number(maxTweetLength)
);
return trimmedContent.substring(0, 177) + "...";
}

return trimmedContent;
Expand All @@ -59,53 +60,79 @@ async function composeTweet(
}
}

async function postTweet(content: string): Promise<boolean> {
async function sendTweet(twitterClient: Scraper, content: string) {
const result = await twitterClient.sendTweet(content);

const body = await result.json();
elizaLogger.log("Tweet response:", body);

// Check for Twitter API errors
if (body.errors) {
const error = body.errors[0];
elizaLogger.error(
`Twitter API error (${error.code}): ${error.message}`
);
return false;
}

// Check for successful tweet creation
if (!body?.data?.create_tweet?.tweet_results?.result) {
elizaLogger.error("Failed to post tweet: No tweet result in response");
return false;
}

return true;
}

async function postTweet(
runtime: IAgentRuntime,
content: string
): Promise<boolean> {
try {
const scraper = new Scraper();
const username = process.env.TWITTER_USERNAME;
const password = process.env.TWITTER_PASSWORD;
const email = process.env.TWITTER_EMAIL;
const twitter2faSecret = process.env.TWITTER_2FA_SECRET;
const twitterClient = runtime.clients.twitter?.client?.twitterClient;
const scraper = twitterClient || new Scraper();

if (!username || !password) {
elizaLogger.error(
"Twitter credentials not configured in environment"
);
return false;
}
if (!twitterClient) {
const username = runtime.getSetting("TWITTER_USERNAME");
const password = runtime.getSetting("TWITTER_PASSWORD");
const email = runtime.getSetting("TWITTER_EMAIL");
const twitter2faSecret = runtime.getSetting("TWITTER_2FA_SECRET");

// Login with credentials
await scraper.login(username, password, email, twitter2faSecret);
if (!(await scraper.isLoggedIn())) {
elizaLogger.error("Failed to login to Twitter");
return false;
if (!username || !password) {
elizaLogger.error(
"Twitter credentials not configured in environment"
);
return false;
}
// Login with credentials
await scraper.login(username, password, email, twitter2faSecret);
if (!(await scraper.isLoggedIn())) {
elizaLogger.error("Failed to login to Twitter");
return false;
}
}

// Send the tweet
elizaLogger.log("Attempting to send tweet:", content);
const result = await scraper.sendTweet(content);

const body = await result.json();
elizaLogger.log("Tweet response:", body);

// Check for Twitter API errors
if (body.errors) {
const error = body.errors[0];
elizaLogger.error(
`Twitter API error (${error.code}): ${error.message}`
);
return false;
}

// Check for successful tweet creation
if (!body?.data?.create_tweet?.tweet_results?.result) {
elizaLogger.error(
"Failed to post tweet: No tweet result in response"
);
return false;
try {
if (content.length > DEFAULT_MAX_TWEET_LENGTH) {
const noteTweetResult = await scraper.sendNoteTweet(content);
if (
noteTweetResult.errors &&
noteTweetResult.errors.length > 0
) {
// Note Tweet failed due to authorization. Falling back to standard Tweet.
return await sendTweet(scraper, content);
} else {
return true;
}
} else {
return await sendTweet(scraper, content);
}
} catch (error) {
throw new Error(`Note Tweet failed: ${error}`);
}

return true;
} catch (error) {
// Log the full error details
elizaLogger.error("Error posting tweet:", {
Expand All @@ -127,8 +154,10 @@ export const postAction: Action = {
message: Memory,
state?: State
) => {
const hasCredentials =
!!process.env.TWITTER_USERNAME && !!process.env.TWITTER_PASSWORD;
const username = runtime.getSetting("TWITTER_USERNAME");
const password = runtime.getSetting("TWITTER_PASSWORD");
const email = runtime.getSetting("TWITTER_EMAIL");
const hasCredentials = !!username && !!password && !!email;
elizaLogger.log(`Has credentials: ${hasCredentials}`);

return hasCredentials;
Expand Down Expand Up @@ -160,7 +189,7 @@ export const postAction: Action = {
return true;
}

return await postTweet(tweetContent);
return await postTweet(runtime, tweetContent);
} catch (error) {
elizaLogger.error("Error in post action:", error);
return false;
Expand Down

0 comments on commit 87d8eca

Please sign in to comment.