Skip to content

Commit 12407dc

Browse files
authored
Merge pull request #1514 from odilitime/test-eliza
fix: client-twitter lowerCase bug and environment clean up (+lint fixes, and TWITTER_SEARCH_ENABLE double start fix)
2 parents 0bcf50d + b7db673 commit 12407dc

File tree

7 files changed

+177
-107
lines changed

7 files changed

+177
-107
lines changed

packages/client-twitter/src/base.ts

+12-13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
Tweet,
1717
} from "agent-twitter-client";
1818
import { EventEmitter } from "events";
19+
import { TwitterConfig } from "./environment.ts";
1920

2021
export function extractAnswer(text: string): string {
2122
const startIndex = text.indexOf("Answer: ") + 8;
@@ -85,6 +86,7 @@ export class ClientBase extends EventEmitter {
8586
static _twitterClients: { [accountIdentifier: string]: Scraper } = {};
8687
twitterClient: Scraper;
8788
runtime: IAgentRuntime;
89+
twitterConfig: TwitterConfig;
8890
directions: string;
8991
lastCheckedTweetId: bigint | null = null;
9092
imageDescriptionService: IImageDescriptionService;
@@ -134,10 +136,11 @@ export class ClientBase extends EventEmitter {
134136
);
135137
}
136138

137-
constructor(runtime: IAgentRuntime) {
139+
constructor(runtime: IAgentRuntime, twitterConfig:TwitterConfig) {
138140
super();
139141
this.runtime = runtime;
140-
const username = this.runtime.getSetting("TWITTER_USERNAME");
142+
this.twitterConfig = twitterConfig;
143+
const username = twitterConfig.TWITTER_USERNAME;
141144
if (ClientBase._twitterClients[username]) {
142145
this.twitterClient = ClientBase._twitterClients[username];
143146
} else {
@@ -153,15 +156,11 @@ export class ClientBase extends EventEmitter {
153156
}
154157

155158
async init() {
156-
const username = this.runtime.getSetting("TWITTER_USERNAME");
157-
const password = this.runtime.getSetting("TWITTER_PASSWORD");
158-
const email = this.runtime.getSetting("TWITTER_EMAIL");
159-
let retries = parseInt(
160-
this.runtime.getSetting("TWITTER_RETRY_LIMIT") || "5",
161-
10
162-
);
163-
const twitter2faSecret =
164-
this.runtime.getSetting("TWITTER_2FA_SECRET") || undefined;
159+
const username = this.twitterConfig.TWITTER_USERNAME;
160+
const password = this.twitterConfig.TWITTER_PASSWORD;
161+
const email = this.twitterConfig.TWITTER_EMAIL;
162+
let retries = this.twitterConfig.TWITTER_RETRY_LIMIT
163+
const twitter2faSecret = this.twitterConfig.TWITTER_2FA_SECRET;
165164

166165
if (!username) {
167166
throw new Error("Twitter username not configured");
@@ -314,7 +313,7 @@ export class ClientBase extends EventEmitter {
314313
async fetchTimelineForActions(count: number): Promise<Tweet[]> {
315314
elizaLogger.debug("fetching timeline for actions");
316315

317-
const agentUsername = this.runtime.getSetting("TWITTER_USERNAME");
316+
const agentUsername = this.twitterConfig.TWITTER_USERNAME
318317
const homeTimeline = await this.twitterClient.fetchHomeTimeline(
319318
count,
320319
[]
@@ -510,7 +509,7 @@ export class ClientBase extends EventEmitter {
510509
}
511510

512511
const timeline = await this.fetchHomeTimeline(cachedTimeline ? 10 : 50);
513-
const username = this.runtime.getSetting("TWITTER_USERNAME");
512+
const username = this.twitterConfig.TWITTER_USERNAME;
514513

515514
// Get the most recent 20 mentions and interactions
516515
const mentionsAndInteractions = await this.fetchSearchTweets(

packages/client-twitter/src/environment.ts

+131-16
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,160 @@
1-
import { IAgentRuntime } from "@elizaos/core";
1+
import { parseBooleanFromText, IAgentRuntime } from "@elizaos/core";
22
import { z } from "zod";
3-
43
export const DEFAULT_MAX_TWEET_LENGTH = 280;
54

5+
const twitterUsernameSchema = z.string()
6+
.min(1)
7+
.max(15)
8+
.regex(/^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]$|^[A-Za-z]$/, 'Invalid Twitter username format');
9+
610
export const twitterEnvSchema = z.object({
7-
TWITTER_DRY_RUN: z
8-
.string()
9-
.transform((val) => val.toLowerCase() === "true"),
11+
TWITTER_DRY_RUN: z.boolean(),
1012
TWITTER_USERNAME: z.string().min(1, "Twitter username is required"),
1113
TWITTER_PASSWORD: z.string().min(1, "Twitter password is required"),
1214
TWITTER_EMAIL: z.string().email("Valid Twitter email is required"),
13-
MAX_TWEET_LENGTH: z
15+
MAX_TWEET_LENGTH: z.number().int().default(DEFAULT_MAX_TWEET_LENGTH),
16+
TWITTER_SEARCH_ENABLE: z.boolean().default(false),
17+
TWITTER_2FA_SECRET: z.string(),
18+
TWITTER_RETRY_LIMIT: z.number().int(),
19+
TWITTER_POLL_INTERVAL: z.number().int(),
20+
TWITTER_TARGET_USERS: z.array(twitterUsernameSchema).default([]),
21+
// I guess it's possible to do the transformation with zod
22+
// not sure it's preferable, maybe a readability issue
23+
// since more people will know js/ts than zod
24+
/*
25+
z
1426
.string()
15-
.pipe(z.coerce.number().min(0).int())
16-
.default(DEFAULT_MAX_TWEET_LENGTH.toString()),
27+
.transform((val) => val.trim())
28+
.pipe(
29+
z.string()
30+
.transform((val) =>
31+
val ? val.split(',').map((u) => u.trim()).filter(Boolean) : []
32+
)
33+
.pipe(
34+
z.array(
35+
z.string()
36+
.min(1)
37+
.max(15)
38+
.regex(
39+
/^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]$|^[A-Za-z]$/,
40+
'Invalid Twitter username format'
41+
)
42+
)
43+
)
44+
.transform((users) => users.join(','))
45+
)
46+
.optional()
47+
.default(''),
48+
*/
49+
POST_INTERVAL_MIN: z.number().int(),
50+
POST_INTERVAL_MAX: z.number().int(),
51+
ENABLE_ACTION_PROCESSING: z.boolean(),
52+
ACTION_INTERVAL: z.number().int(),
53+
POST_IMMEDIATELY: z.boolean(),
1754
});
1855

1956
export type TwitterConfig = z.infer<typeof twitterEnvSchema>;
2057

58+
function parseTargetUsers(targetUsersStr?:string | null): string[] {
59+
if (!targetUsersStr?.trim()) {
60+
return [];
61+
}
62+
63+
return targetUsersStr
64+
.split(',')
65+
.map(user => user.trim())
66+
.filter(Boolean); // Remove empty usernames
67+
/*
68+
.filter(user => {
69+
// Twitter username validation (basic example)
70+
return user && /^[A-Za-z0-9_]{1,15}$/.test(user);
71+
});
72+
*/
73+
}
74+
75+
function safeParseInt(value: string | undefined | null, defaultValue: number): number {
76+
if (!value) return defaultValue;
77+
const parsed = parseInt(value, 10);
78+
return isNaN(parsed) ? defaultValue : Math.max(1, parsed);
79+
}
80+
81+
// This also is organized to serve as a point of documentation for the client
82+
// most of the inputs from the framework (env/character)
83+
84+
// we also do a lot of typing/parsing here
85+
// so we can do it once and only once per character
2186
export async function validateTwitterConfig(
2287
runtime: IAgentRuntime
2388
): Promise<TwitterConfig> {
2489
try {
2590
const twitterConfig = {
2691
TWITTER_DRY_RUN:
27-
runtime.getSetting("TWITTER_DRY_RUN") ||
28-
process.env.TWITTER_DRY_RUN ||
29-
"false",
92+
parseBooleanFromText(
93+
runtime.getSetting("TWITTER_DRY_RUN") ||
94+
process.env.TWITTER_DRY_RUN
95+
) ?? false, // parseBooleanFromText return null if "", map "" to false
3096
TWITTER_USERNAME:
31-
runtime.getSetting("TWITTER_USERNAME") ||
97+
runtime.getSetting ("TWITTER_USERNAME") ||
3298
process.env.TWITTER_USERNAME,
3399
TWITTER_PASSWORD:
34100
runtime.getSetting("TWITTER_PASSWORD") ||
35101
process.env.TWITTER_PASSWORD,
36102
TWITTER_EMAIL:
37103
runtime.getSetting("TWITTER_EMAIL") ||
38104
process.env.TWITTER_EMAIL,
39-
MAX_TWEET_LENGTH:
40-
runtime.getSetting("MAX_TWEET_LENGTH") ||
41-
process.env.MAX_TWEET_LENGTH ||
42-
DEFAULT_MAX_TWEET_LENGTH.toString(),
105+
MAX_TWEET_LENGTH: // number as string?
106+
safeParseInt(
107+
runtime.getSetting("MAX_TWEET_LENGTH") ||
108+
process.env.MAX_TWEET_LENGTH
109+
, DEFAULT_MAX_TWEET_LENGTH),
110+
TWITTER_SEARCH_ENABLE: // bool
111+
parseBooleanFromText(
112+
runtime.getSetting("TWITTER_SEARCH_ENABLE") ||
113+
process.env.TWITTER_SEARCH_ENABLE
114+
) ?? false,
115+
TWITTER_2FA_SECRET: // string passthru
116+
runtime.getSetting("TWITTER_2FA_SECRET") ||
117+
process.env.TWITTER_2FA_SECRET || "",
118+
TWITTER_RETRY_LIMIT: // int
119+
safeParseInt(
120+
runtime.getSetting("TWITTER_RETRY_LIMIT") ||
121+
process.env.TWITTER_RETRY_LIMIT
122+
, 5),
123+
TWITTER_POLL_INTERVAL: // int in seconds
124+
safeParseInt(
125+
runtime.getSetting("TWITTER_POLL_INTERVAL") ||
126+
process.env.TWITTER_POLL_INTERVAL
127+
, 120), // 2m
128+
TWITTER_TARGET_USERS: // comma separated string
129+
parseTargetUsers(
130+
runtime.getSetting("TWITTER_TARGET_USERS") ||
131+
process.env.TWITTER_TARGET_USERS
132+
),
133+
POST_INTERVAL_MIN: // int in minutes
134+
safeParseInt(
135+
runtime.getSetting("POST_INTERVAL_MIN") ||
136+
process.env.POST_INTERVAL_MIN
137+
, 90), // 1.5 hours
138+
POST_INTERVAL_MAX: // int in minutes
139+
safeParseInt(
140+
runtime.getSetting("POST_INTERVAL_MAX") ||
141+
process.env.POST_INTERVAL_MAX
142+
, 180), // 3 hours
143+
ENABLE_ACTION_PROCESSING: // bool
144+
parseBooleanFromText(
145+
runtime.getSetting("ENABLE_ACTION_PROCESSING") ||
146+
process.env.ENABLE_ACTION_PROCESSING
147+
) ?? false,
148+
ACTION_INTERVAL: // int in minutes (min 1m)
149+
safeParseInt(
150+
runtime.getSetting("ACTION_INTERVAL") ||
151+
process.env.ACTION_INTERVAL
152+
, 5), // 5 minutes
153+
POST_IMMEDIATELY: // bool
154+
parseBooleanFromText(
155+
runtime.getSetting("POST_IMMEDIATELY") ||
156+
process.env.POST_IMMEDIATELY
157+
) ?? false,
43158
};
44159

45160
return twitterEnvSchema.parse(twitterConfig);

packages/client-twitter/src/index.ts

+6-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Client, elizaLogger, IAgentRuntime } from "@elizaos/core";
22
import { ClientBase } from "./base.ts";
3-
import { validateTwitterConfig } from "./environment.ts";
3+
import { validateTwitterConfig, TwitterConfig } from "./environment.ts";
44
import { TwitterInteractionClient } from "./interactions.ts";
55
import { TwitterPostClient } from "./post.ts";
66
import { TwitterSearchClient } from "./search.ts";
@@ -10,11 +10,11 @@ class TwitterManager {
1010
post: TwitterPostClient;
1111
search: TwitterSearchClient;
1212
interaction: TwitterInteractionClient;
13-
constructor(runtime: IAgentRuntime, enableSearch: boolean) {
14-
this.client = new ClientBase(runtime);
13+
constructor(runtime: IAgentRuntime, twitterConfig:TwitterConfig) {
14+
this.client = new ClientBase(runtime, twitterConfig);
1515
this.post = new TwitterPostClient(this.client, runtime);
1616

17-
if (enableSearch) {
17+
if (twitterConfig.TWITTER_SEARCH_ENABLE) {
1818
// this searches topics from character file
1919
elizaLogger.warn("Twitter/X client running in a mode that:");
2020
elizaLogger.warn("1. violates consent of random users");
@@ -30,11 +30,11 @@ class TwitterManager {
3030

3131
export const TwitterClientInterface: Client = {
3232
async start(runtime: IAgentRuntime) {
33-
await validateTwitterConfig(runtime);
33+
const twitterConfig:TwitterConfig = await validateTwitterConfig(runtime);
3434

3535
elizaLogger.log("Twitter client started");
3636

37-
const manager = new TwitterManager(runtime, runtime.getSetting("TWITTER_SEARCH_ENABLE").toLowerCase() === "true");
37+
const manager = new TwitterManager(runtime, twitterConfig);
3838

3939
await manager.client.init();
4040

@@ -45,8 +45,6 @@ export const TwitterClientInterface: Client = {
4545

4646
await manager.interaction.start();
4747

48-
await manager.search?.start();
49-
5048
return manager;
5149
},
5250
async stop(_runtime: IAgentRuntime) {

packages/client-twitter/src/interactions.ts

+8-24
Original file line numberDiff line numberDiff line change
@@ -100,18 +100,15 @@ export class TwitterInteractionClient {
100100
this.handleTwitterInteractions();
101101
setTimeout(
102102
handleTwitterInteractionsLoop,
103-
Number(
104-
this.runtime.getSetting("TWITTER_POLL_INTERVAL") || 120
105-
) * 1000 // Default to 2 minutes
103+
// Defaults to 2 minutes
104+
this.client.twitterConfig.TWITTER_POLL_INTERVAL * 1000
106105
);
107106
};
108107
handleTwitterInteractionsLoop();
109108
}
110109

111110
async handleTwitterInteractions() {
112111
elizaLogger.log("Checking Twitter interactions");
113-
// Read from environment variable, fallback to default list if not set
114-
const targetUsersStr = this.runtime.getSetting("TWITTER_TARGET_USERS");
115112

116113
const twitterUsername = this.client.profile.username;
117114
try {
@@ -130,11 +127,8 @@ export class TwitterInteractionClient {
130127
);
131128
let uniqueTweetCandidates = [...mentionCandidates];
132129
// Only process target users if configured
133-
if (targetUsersStr && targetUsersStr.trim()) {
134-
const TARGET_USERS = targetUsersStr
135-
.split(",")
136-
.map((u) => u.trim())
137-
.filter((u) => u.length > 0); // Filter out empty strings after split
130+
if (this.client.twitterConfig.TWITTER_TARGET_USERS.length) {
131+
const TARGET_USERS = this.client.twitterConfig.TWITTER_TARGET_USERS;
138132

139133
elizaLogger.log("Processing target users:", TARGET_USERS);
140134

@@ -347,7 +341,7 @@ export class TwitterInteractionClient {
347341

348342
let state = await this.runtime.composeState(message, {
349343
twitterClient: this.client.twitterClient,
350-
twitterUserName: this.runtime.getSetting("TWITTER_USERNAME"),
344+
twitterUserName: this.client.twitterConfig.TWITTER_USERNAME,
351345
currentPost,
352346
formattedConversation,
353347
});
@@ -383,18 +377,8 @@ export class TwitterInteractionClient {
383377
this.client.saveRequestMessage(message, state);
384378
}
385379

386-
// 1. Get the raw target users string from settings
387-
const targetUsersStr = this.runtime.getSetting("TWITTER_TARGET_USERS");
388-
389-
// 2. Process the string to get valid usernames
390-
const validTargetUsersStr =
391-
targetUsersStr && targetUsersStr.trim()
392-
? targetUsersStr
393-
.split(",") // Split by commas: "user1,user2" -> ["user1", "user2"]
394-
.map((u) => u.trim()) // Remove whitespace: [" user1 ", "user2 "] -> ["user1", "user2"]
395-
.filter((u) => u.length > 0)
396-
.join(",")
397-
: "";
380+
// get usernames into str
381+
const validTargetUsersStr = this.client.twitterConfig.TWITTER_TARGET_USERS.join(",");
398382

399383
const shouldRespondContext = composeContext({
400384
state,
@@ -450,7 +434,7 @@ export class TwitterInteractionClient {
450434
this.client,
451435
response,
452436
message.roomId,
453-
this.runtime.getSetting("TWITTER_USERNAME"),
437+
this.client.twitterConfig.TWITTER_USERNAME,
454438
tweet.id
455439
);
456440
return memories;

0 commit comments

Comments
 (0)