Skip to content

Commit 91edfee

Browse files
committed
feat: Twitter spaces integration
1 parent 76d4f42 commit 91edfee

File tree

6 files changed

+659
-56
lines changed

6 files changed

+659
-56
lines changed

.env.example

+1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ TWITTER_POLL_INTERVAL=120 # How often (in seconds) the bot should check fo
6767
TWITTER_SEARCH_ENABLE=FALSE # Enable timeline search, WARNING this greatly increases your chance of getting banned
6868
TWITTER_TARGET_USERS= # Comma separated list of Twitter user names to interact with
6969
TWITTER_RETRY_LIMIT= # Maximum retry attempts for Twitter login
70+
TWITTER_SPACES_ENABLE=false # Enable or disable Twitter Spaces logic
7071

7172
X_SERVER_URL=
7273
XAI_API_KEY=

characters/c3po.character.json

+34-2
Original file line numberDiff line numberDiff line change
@@ -94,5 +94,37 @@
9494
"Protocol-minded",
9595
"Formal",
9696
"Loyal"
97-
]
98-
}
97+
],
98+
"twitterSpaces": {
99+
"maxSpeakers": 2,
100+
101+
"topics": [
102+
"Blockchain Trends",
103+
"AI Innovations",
104+
"Quantum Computing"
105+
],
106+
107+
"typicalDurationMinutes": 45,
108+
109+
"idleKickTimeoutMs": 300000,
110+
111+
"minIntervalBetweenSpacesMinutes": 1,
112+
113+
"businessHoursOnly": false,
114+
115+
"randomChance": 1,
116+
117+
"enableIdleMonitor": true,
118+
119+
"enableSttTts": true,
120+
121+
"enableRecording": false,
122+
123+
"voiceId": "21m00Tcm4TlvDq8ikWAM",
124+
"sttLanguage": "en",
125+
"gptModel": "gpt-3.5-turbo",
126+
"systemPrompt": "You are a helpful AI co-host assistant.",
127+
128+
"speakerMaxDurationMs": 240000
129+
}
130+
}

packages/client-twitter/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"types": "dist/index.d.ts",
77
"dependencies": {
88
"@elizaos/core": "workspace:*",
9-
"agent-twitter-client": "0.0.17",
9+
"agent-twitter-client": "0.0.18",
1010
"glob": "11.0.0",
1111
"zod": "3.23.8"
1212
},

packages/client-twitter/src/environment.ts

+76-49
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
11
import { parseBooleanFromText, IAgentRuntime } from "@elizaos/core";
2-
import { z } from "zod";
2+
import { z, ZodError } from "zod";
3+
34
export const DEFAULT_MAX_TWEET_LENGTH = 280;
45

56
const twitterUsernameSchema = z.string()
67
.min(1)
78
.max(15)
8-
.regex(/^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]$|^[A-Za-z]$/, 'Invalid Twitter username format');
9+
.regex(
10+
/^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]$|^[A-Za-z]$/,
11+
"Invalid Twitter username format"
12+
);
913

14+
/**
15+
* This schema defines all required/optional environment settings,
16+
* including new fields like TWITTER_SPACES_ENABLE.
17+
*/
1018
export const twitterEnvSchema = z.object({
1119
TWITTER_DRY_RUN: z.boolean(),
1220
TWITTER_USERNAME: z.string().min(1, "Twitter username is required"),
@@ -51,25 +59,23 @@ export const twitterEnvSchema = z.object({
5159
ENABLE_ACTION_PROCESSING: z.boolean(),
5260
ACTION_INTERVAL: z.number().int(),
5361
POST_IMMEDIATELY: z.boolean(),
62+
TWITTER_SPACES_ENABLE: z.boolean().default(false),
5463
});
5564

5665
export type TwitterConfig = z.infer<typeof twitterEnvSchema>;
5766

58-
function parseTargetUsers(targetUsersStr?:string | null): string[] {
67+
/**
68+
* Helper to parse a comma-separated list of Twitter usernames
69+
* (already present in your code).
70+
*/
71+
function parseTargetUsers(targetUsersStr?: string | null): string[] {
5972
if (!targetUsersStr?.trim()) {
6073
return [];
6174
}
62-
6375
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-
*/
76+
.split(",")
77+
.map((user) => user.trim())
78+
.filter(Boolean);
7379
}
7480

7581
function safeParseInt(value: string | undefined | null, defaultValue: number): number {
@@ -78,94 +84,115 @@ function safeParseInt(value: string | undefined | null, defaultValue: number): n
7884
return isNaN(parsed) ? defaultValue : Math.max(1, parsed);
7985
}
8086

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
86-
export async function validateTwitterConfig(
87-
runtime: IAgentRuntime
88-
): Promise<TwitterConfig> {
87+
/**
88+
* Validates or constructs a TwitterConfig object using zod,
89+
* taking values from the IAgentRuntime or process.env as needed.
90+
*/
91+
export async function validateTwitterConfig(runtime: IAgentRuntime): Promise<TwitterConfig> {
8992
try {
9093
const twitterConfig = {
9194
TWITTER_DRY_RUN:
9295
parseBooleanFromText(
9396
runtime.getSetting("TWITTER_DRY_RUN") ||
9497
process.env.TWITTER_DRY_RUN
9598
) ?? false, // parseBooleanFromText return null if "", map "" to false
99+
96100
TWITTER_USERNAME:
97-
runtime.getSetting ("TWITTER_USERNAME") ||
101+
runtime.getSetting("TWITTER_USERNAME") ||
98102
process.env.TWITTER_USERNAME,
103+
99104
TWITTER_PASSWORD:
100105
runtime.getSetting("TWITTER_PASSWORD") ||
101106
process.env.TWITTER_PASSWORD,
107+
102108
TWITTER_EMAIL:
103109
runtime.getSetting("TWITTER_EMAIL") ||
104110
process.env.TWITTER_EMAIL,
105-
MAX_TWEET_LENGTH: // number as string?
111+
112+
MAX_TWEET_LENGTH:
106113
safeParseInt(
107114
runtime.getSetting("MAX_TWEET_LENGTH") ||
108-
process.env.MAX_TWEET_LENGTH
109-
, DEFAULT_MAX_TWEET_LENGTH),
110-
TWITTER_SEARCH_ENABLE: // bool
115+
process.env.MAX_TWEET_LENGTH,
116+
DEFAULT_MAX_TWEET_LENGTH
117+
),
118+
119+
TWITTER_SEARCH_ENABLE:
111120
parseBooleanFromText(
112121
runtime.getSetting("TWITTER_SEARCH_ENABLE") ||
113122
process.env.TWITTER_SEARCH_ENABLE
114123
) ?? false,
115-
TWITTER_2FA_SECRET: // string passthru
124+
125+
TWITTER_2FA_SECRET:
116126
runtime.getSetting("TWITTER_2FA_SECRET") ||
117127
process.env.TWITTER_2FA_SECRET || "",
118-
TWITTER_RETRY_LIMIT: // int
128+
129+
TWITTER_RETRY_LIMIT:
119130
safeParseInt(
120131
runtime.getSetting("TWITTER_RETRY_LIMIT") ||
121-
process.env.TWITTER_RETRY_LIMIT
122-
, 5),
123-
TWITTER_POLL_INTERVAL: // int in seconds
132+
process.env.TWITTER_RETRY_LIMIT,
133+
5
134+
),
135+
136+
TWITTER_POLL_INTERVAL:
124137
safeParseInt(
125138
runtime.getSetting("TWITTER_POLL_INTERVAL") ||
126-
process.env.TWITTER_POLL_INTERVAL
127-
, 120), // 2m
128-
TWITTER_TARGET_USERS: // comma separated string
139+
process.env.TWITTER_POLL_INTERVAL,
140+
120
141+
),
142+
143+
TWITTER_TARGET_USERS:
129144
parseTargetUsers(
130145
runtime.getSetting("TWITTER_TARGET_USERS") ||
131146
process.env.TWITTER_TARGET_USERS
132147
),
133-
POST_INTERVAL_MIN: // int in minutes
148+
149+
POST_INTERVAL_MIN:
134150
safeParseInt(
135151
runtime.getSetting("POST_INTERVAL_MIN") ||
136-
process.env.POST_INTERVAL_MIN
137-
, 90), // 1.5 hours
138-
POST_INTERVAL_MAX: // int in minutes
152+
process.env.POST_INTERVAL_MIN,
153+
90
154+
),
155+
156+
POST_INTERVAL_MAX:
139157
safeParseInt(
140158
runtime.getSetting("POST_INTERVAL_MAX") ||
141-
process.env.POST_INTERVAL_MAX
142-
, 180), // 3 hours
143-
ENABLE_ACTION_PROCESSING: // bool
159+
process.env.POST_INTERVAL_MAX,
160+
180
161+
),
162+
163+
ENABLE_ACTION_PROCESSING:
144164
parseBooleanFromText(
145165
runtime.getSetting("ENABLE_ACTION_PROCESSING") ||
146166
process.env.ENABLE_ACTION_PROCESSING
147167
) ?? false,
148-
ACTION_INTERVAL: // int in minutes (min 1m)
168+
169+
ACTION_INTERVAL:
149170
safeParseInt(
150171
runtime.getSetting("ACTION_INTERVAL") ||
151-
process.env.ACTION_INTERVAL
152-
, 5), // 5 minutes
153-
POST_IMMEDIATELY: // bool
172+
process.env.ACTION_INTERVAL,
173+
5
174+
),
175+
176+
POST_IMMEDIATELY:
154177
parseBooleanFromText(
155178
runtime.getSetting("POST_IMMEDIATELY") ||
156179
process.env.POST_IMMEDIATELY
157180
) ?? false,
181+
182+
TWITTER_SPACES_ENABLE:
183+
parseBooleanFromText(
184+
runtime.getSetting("TWITTER_SPACES_ENABLE") ||
185+
process.env.TWITTER_SPACES_ENABLE
186+
) ?? false,
158187
};
159188

160189
return twitterEnvSchema.parse(twitterConfig);
161190
} catch (error) {
162-
if (error instanceof z.ZodError) {
191+
if (error instanceof ZodError) {
163192
const errorMessages = error.errors
164193
.map((err) => `${err.path.join(".")}: ${err.message}`)
165194
.join("\n");
166-
throw new Error(
167-
`Twitter configuration validation failed:\n${errorMessages}`
168-
);
195+
throw new Error(`Twitter configuration validation failed:\n${errorMessages}`);
169196
}
170197
throw error;
171198
}

packages/client-twitter/src/index.ts

+35-4
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,32 @@ import { validateTwitterConfig, TwitterConfig } from "./environment.ts";
44
import { TwitterInteractionClient } from "./interactions.ts";
55
import { TwitterPostClient } from "./post.ts";
66
import { TwitterSearchClient } from "./search.ts";
7+
import { TwitterSpaceClient } from "./spaces.ts";
78

9+
/**
10+
* A manager that orchestrates all specialized Twitter logic:
11+
* - client: base operations (login, timeline caching, etc.)
12+
* - post: autonomous posting logic
13+
* - search: searching tweets / replying logic
14+
* - interaction: handling mentions, replies
15+
* - space: launching and managing Twitter Spaces (optional)
16+
*/
817
class TwitterManager {
918
client: ClientBase;
1019
post: TwitterPostClient;
1120
search: TwitterSearchClient;
1221
interaction: TwitterInteractionClient;
13-
constructor(runtime: IAgentRuntime, twitterConfig:TwitterConfig) {
22+
space?: TwitterSpaceClient;
23+
24+
constructor(runtime: IAgentRuntime, twitterConfig: TwitterConfig) {
25+
// Pass twitterConfig to the base client
1426
this.client = new ClientBase(runtime, twitterConfig);
27+
28+
// Posting logic
1529
this.post = new TwitterPostClient(this.client, runtime);
1630

31+
// Optional search logic (enabled if TWITTER_SEARCH_ENABLE is true)
1732
if (twitterConfig.TWITTER_SEARCH_ENABLE) {
18-
// this searches topics from character file
1933
elizaLogger.warn("Twitter/X client running in a mode that:");
2034
elizaLogger.warn("1. violates consent of random users");
2135
elizaLogger.warn("2. burns your rate limit");
@@ -24,29 +38,46 @@ class TwitterManager {
2438
this.search = new TwitterSearchClient(this.client, runtime);
2539
}
2640

41+
// Mentions and interactions
2742
this.interaction = new TwitterInteractionClient(this.client, runtime);
43+
44+
// Optional Spaces logic (enabled if TWITTER_SPACES_ENABLE is true)
45+
if (twitterConfig.TWITTER_SPACES_ENABLE) {
46+
this.space = new TwitterSpaceClient(this.client, runtime);
47+
}
2848
}
2949
}
3050

3151
export const TwitterClientInterface: Client = {
3252
async start(runtime: IAgentRuntime) {
33-
const twitterConfig:TwitterConfig = await validateTwitterConfig(runtime);
53+
const twitterConfig: TwitterConfig = await validateTwitterConfig(runtime);
3454

3555
elizaLogger.log("Twitter client started");
3656

3757
const manager = new TwitterManager(runtime, twitterConfig);
3858

59+
// Initialize login/session
3960
await manager.client.init();
4061

62+
// Start the posting loop
4163
await manager.post.start();
4264

43-
if (manager.search)
65+
// Start the search logic if it exists
66+
if (manager.search) {
4467
await manager.search.start();
68+
}
4569

70+
// Start interactions (mentions, replies)
4671
await manager.interaction.start();
4772

73+
// If Spaces are enabled, start the periodic check
74+
if (manager.space) {
75+
manager.space.startPeriodicSpaceCheck();
76+
}
77+
4878
return manager;
4979
},
80+
5081
async stop(_runtime: IAgentRuntime) {
5182
elizaLogger.warn("Twitter client does not support stopping yet");
5283
},

0 commit comments

Comments
 (0)