Skip to content

Commit 005685d

Browse files
authored
Merge pull request elizaOS#2129 from elizaOS/tcm-fix-plugin-twitter
fix: prevent repeated login by reusing client-twitter session
2 parents 57f4bc7 + 4e428f1 commit 005685d

File tree

3 files changed

+115
-91
lines changed

3 files changed

+115
-91
lines changed

packages/client-twitter/src/post.ts

+2-41
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
stringToUuid,
99
TemplateType,
1010
UUID,
11+
truncateToCompleteSentence,
1112
} from "@elizaos/core";
1213
import { elizaLogger } from "@elizaos/core";
1314
import { ClientBase } from "./base.ts";
@@ -77,40 +78,6 @@ Tweet:
7778
# Respond with qualifying action tags only. Default to NO action unless extremely confident of relevance.` +
7879
postActionResponseFooter;
7980

80-
/**
81-
* Truncate text to fit within the Twitter character limit, ensuring it ends at a complete sentence.
82-
*/
83-
function truncateToCompleteSentence(
84-
text: string,
85-
maxTweetLength: number
86-
): string {
87-
if (text.length <= maxTweetLength) {
88-
return text;
89-
}
90-
91-
// Attempt to truncate at the last period within the limit
92-
const lastPeriodIndex = text.lastIndexOf(".", maxTweetLength - 1);
93-
if (lastPeriodIndex !== -1) {
94-
const truncatedAtPeriod = text.slice(0, lastPeriodIndex + 1).trim();
95-
if (truncatedAtPeriod.length > 0) {
96-
return truncatedAtPeriod;
97-
}
98-
}
99-
100-
// If no period, truncate to the nearest whitespace within the limit
101-
const lastSpaceIndex = text.lastIndexOf(" ", maxTweetLength - 1);
102-
if (lastSpaceIndex !== -1) {
103-
const truncatedAtSpace = text.slice(0, lastSpaceIndex).trim();
104-
if (truncatedAtSpace.length > 0) {
105-
return truncatedAtSpace + "...";
106-
}
107-
}
108-
109-
// Fallback: Hard truncate and add ellipsis
110-
const hardTruncated = text.slice(0, maxTweetLength - 3).trim();
111-
return hardTruncated + "...";
112-
}
113-
11481
interface PendingTweet {
11582
cleanedContent: string;
11683
roomId: UUID;
@@ -399,7 +366,6 @@ export class TwitterPostClient {
399366

400367
async handleNoteTweet(
401368
client: ClientBase,
402-
runtime: IAgentRuntime,
403369
content: string,
404370
tweetId?: string
405371
) {
@@ -465,11 +431,7 @@ export class TwitterPostClient {
465431
let result;
466432

467433
if (cleanedContent.length > DEFAULT_MAX_TWEET_LENGTH) {
468-
result = await this.handleNoteTweet(
469-
client,
470-
runtime,
471-
cleanedContent
472-
);
434+
result = await this.handleNoteTweet(client, cleanedContent);
473435
} else {
474436
result = await this.sendStandardTweet(client, cleanedContent);
475437
}
@@ -1204,7 +1166,6 @@ export class TwitterPostClient {
12041166
if (replyText.length > DEFAULT_MAX_TWEET_LENGTH) {
12051167
result = await this.handleNoteTweet(
12061168
this.client,
1207-
this.runtime,
12081169
replyText,
12091170
tweet.id
12101171
);

packages/core/src/parsing.ts

+34
Original file line numberDiff line numberDiff line change
@@ -205,3 +205,37 @@ export const parseActionResponseFromText = (
205205

206206
return { actions };
207207
};
208+
209+
/**
210+
* Truncate text to fit within the character limit, ensuring it ends at a complete sentence.
211+
*/
212+
export function truncateToCompleteSentence(
213+
text: string,
214+
maxLength: number
215+
): string {
216+
if (text.length <= maxLength) {
217+
return text;
218+
}
219+
220+
// Attempt to truncate at the last period within the limit
221+
const lastPeriodIndex = text.lastIndexOf(".", maxLength - 1);
222+
if (lastPeriodIndex !== -1) {
223+
const truncatedAtPeriod = text.slice(0, lastPeriodIndex + 1).trim();
224+
if (truncatedAtPeriod.length > 0) {
225+
return truncatedAtPeriod;
226+
}
227+
}
228+
229+
// If no period, truncate to the nearest whitespace within the limit
230+
const lastSpaceIndex = text.lastIndexOf(" ", maxLength - 1);
231+
if (lastSpaceIndex !== -1) {
232+
const truncatedAtSpace = text.slice(0, lastSpaceIndex).trim();
233+
if (truncatedAtSpace.length > 0) {
234+
return truncatedAtSpace + "...";
235+
}
236+
}
237+
238+
// Fallback: Hard truncate and add ellipsis
239+
const hardTruncated = text.slice(0, maxLength - 3).trim();
240+
return hardTruncated + "...";
241+
}

packages/plugin-twitter/src/actions/post.ts

+79-50
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@ import {
77
elizaLogger,
88
ModelClass,
99
generateObject,
10+
truncateToCompleteSentence,
1011
} from "@elizaos/core";
1112
import { Scraper } from "agent-twitter-client";
1213
import { tweetTemplate } from "../templates";
1314
import { isTweetContent, TweetSchema } from "../types";
1415

16+
export const DEFAULT_MAX_TWEET_LENGTH = 280;
17+
1518
async function composeTweet(
1619
runtime: IAgentRuntime,
1720
_message: Memory,
@@ -39,17 +42,15 @@ async function composeTweet(
3942
return;
4043
}
4144

42-
const trimmedContent = tweetContentObject.object.text.trim();
45+
let trimmedContent = tweetContentObject.object.text.trim();
4346

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

5556
return trimmedContent;
@@ -59,53 +60,79 @@ async function composeTweet(
5960
}
6061
}
6162

62-
async function postTweet(content: string): Promise<boolean> {
63+
async function sendTweet(twitterClient: Scraper, content: string) {
64+
const result = await twitterClient.sendTweet(content);
65+
66+
const body = await result.json();
67+
elizaLogger.log("Tweet response:", body);
68+
69+
// Check for Twitter API errors
70+
if (body.errors) {
71+
const error = body.errors[0];
72+
elizaLogger.error(
73+
`Twitter API error (${error.code}): ${error.message}`
74+
);
75+
return false;
76+
}
77+
78+
// Check for successful tweet creation
79+
if (!body?.data?.create_tweet?.tweet_results?.result) {
80+
elizaLogger.error("Failed to post tweet: No tweet result in response");
81+
return false;
82+
}
83+
84+
return true;
85+
}
86+
87+
async function postTweet(
88+
runtime: IAgentRuntime,
89+
content: string
90+
): Promise<boolean> {
6391
try {
64-
const scraper = new Scraper();
65-
const username = process.env.TWITTER_USERNAME;
66-
const password = process.env.TWITTER_PASSWORD;
67-
const email = process.env.TWITTER_EMAIL;
68-
const twitter2faSecret = process.env.TWITTER_2FA_SECRET;
92+
const twitterClient = runtime.clients.twitter?.client?.twitterClient;
93+
const scraper = twitterClient || new Scraper();
6994

70-
if (!username || !password) {
71-
elizaLogger.error(
72-
"Twitter credentials not configured in environment"
73-
);
74-
return false;
75-
}
95+
if (!twitterClient) {
96+
const username = runtime.getSetting("TWITTER_USERNAME");
97+
const password = runtime.getSetting("TWITTER_PASSWORD");
98+
const email = runtime.getSetting("TWITTER_EMAIL");
99+
const twitter2faSecret = runtime.getSetting("TWITTER_2FA_SECRET");
76100

77-
// Login with credentials
78-
await scraper.login(username, password, email, twitter2faSecret);
79-
if (!(await scraper.isLoggedIn())) {
80-
elizaLogger.error("Failed to login to Twitter");
81-
return false;
101+
if (!username || !password) {
102+
elizaLogger.error(
103+
"Twitter credentials not configured in environment"
104+
);
105+
return false;
106+
}
107+
// Login with credentials
108+
await scraper.login(username, password, email, twitter2faSecret);
109+
if (!(await scraper.isLoggedIn())) {
110+
elizaLogger.error("Failed to login to Twitter");
111+
return false;
112+
}
82113
}
83114

84115
// Send the tweet
85116
elizaLogger.log("Attempting to send tweet:", content);
86-
const result = await scraper.sendTweet(content);
87-
88-
const body = await result.json();
89-
elizaLogger.log("Tweet response:", body);
90117

91-
// Check for Twitter API errors
92-
if (body.errors) {
93-
const error = body.errors[0];
94-
elizaLogger.error(
95-
`Twitter API error (${error.code}): ${error.message}`
96-
);
97-
return false;
98-
}
99-
100-
// Check for successful tweet creation
101-
if (!body?.data?.create_tweet?.tweet_results?.result) {
102-
elizaLogger.error(
103-
"Failed to post tweet: No tweet result in response"
104-
);
105-
return false;
118+
try {
119+
if (content.length > DEFAULT_MAX_TWEET_LENGTH) {
120+
const noteTweetResult = await scraper.sendNoteTweet(content);
121+
if (
122+
noteTweetResult.errors &&
123+
noteTweetResult.errors.length > 0
124+
) {
125+
// Note Tweet failed due to authorization. Falling back to standard Tweet.
126+
return await sendTweet(scraper, content);
127+
} else {
128+
return true;
129+
}
130+
} else {
131+
return await sendTweet(scraper, content);
132+
}
133+
} catch (error) {
134+
throw new Error(`Note Tweet failed: ${error}`);
106135
}
107-
108-
return true;
109136
} catch (error) {
110137
// Log the full error details
111138
elizaLogger.error("Error posting tweet:", {
@@ -127,8 +154,10 @@ export const postAction: Action = {
127154
message: Memory,
128155
state?: State
129156
) => {
130-
const hasCredentials =
131-
!!process.env.TWITTER_USERNAME && !!process.env.TWITTER_PASSWORD;
157+
const username = runtime.getSetting("TWITTER_USERNAME");
158+
const password = runtime.getSetting("TWITTER_PASSWORD");
159+
const email = runtime.getSetting("TWITTER_EMAIL");
160+
const hasCredentials = !!username && !!password && !!email;
132161
elizaLogger.log(`Has credentials: ${hasCredentials}`);
133162

134163
return hasCredentials;
@@ -160,7 +189,7 @@ export const postAction: Action = {
160189
return true;
161190
}
162191

163-
return await postTweet(tweetContent);
192+
return await postTweet(runtime, tweetContent);
164193
} catch (error) {
165194
elizaLogger.error("Error in post action:", error);
166195
return false;

0 commit comments

Comments
 (0)