Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: Twitter Spaces Integration #1550

Merged
merged 6 commits into from
Jan 1, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ TWITTER_POLL_INTERVAL=120 # How often (in seconds) the bot should check fo
TWITTER_SEARCH_ENABLE=FALSE # Enable timeline search, WARNING this greatly increases your chance of getting banned
TWITTER_TARGET_USERS= # Comma separated list of Twitter user names to interact with
TWITTER_RETRY_LIMIT= # Maximum retry attempts for Twitter login
TWITTER_SPACES_ENABLE=false # Enable or disable Twitter Spaces logic

X_SERVER_URL=
XAI_API_KEY=
Expand Down
36 changes: 34 additions & 2 deletions characters/c3po.character.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,37 @@
"Protocol-minded",
"Formal",
"Loyal"
]
}
],
"twitterSpaces": {
"maxSpeakers": 2,

"topics": [
"Blockchain Trends",
"AI Innovations",
"Quantum Computing"
],

"typicalDurationMinutes": 45,

"idleKickTimeoutMs": 300000,

"minIntervalBetweenSpacesMinutes": 1,

"businessHoursOnly": false,

"randomChance": 1,

"enableIdleMonitor": true,

"enableSttTts": true,

"enableRecording": false,

"voiceId": "21m00Tcm4TlvDq8ikWAM",
"sttLanguage": "en",
"gptModel": "gpt-3.5-turbo",
"systemPrompt": "You are a helpful AI co-host assistant.",

"speakerMaxDurationMs": 240000
}
}
2 changes: 1 addition & 1 deletion packages/client-twitter/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"types": "dist/index.d.ts",
"dependencies": {
"@elizaos/core": "workspace:*",
"agent-twitter-client": "0.0.17",
"agent-twitter-client": "0.0.18",
"glob": "11.0.0",
"zod": "3.23.8"
},
Expand Down
125 changes: 76 additions & 49 deletions packages/client-twitter/src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
import { parseBooleanFromText, IAgentRuntime } from "@elizaos/core";
import { z } from "zod";
import { z, ZodError } from "zod";

export const DEFAULT_MAX_TWEET_LENGTH = 280;

const twitterUsernameSchema = z.string()
.min(1)
.max(15)
.regex(/^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]$|^[A-Za-z]$/, 'Invalid Twitter username format');
.regex(
/^[A-Za-z][A-Za-z0-9_]*[A-Za-z0-9]$|^[A-Za-z]$/,
"Invalid Twitter username format"
);

/**
* This schema defines all required/optional environment settings,
* including new fields like TWITTER_SPACES_ENABLE.
*/
export const twitterEnvSchema = z.object({
TWITTER_DRY_RUN: z.boolean(),
TWITTER_USERNAME: z.string().min(1, "Twitter username is required"),
Expand Down Expand Up @@ -51,25 +59,23 @@ export const twitterEnvSchema = z.object({
ENABLE_ACTION_PROCESSING: z.boolean(),
ACTION_INTERVAL: z.number().int(),
POST_IMMEDIATELY: z.boolean(),
TWITTER_SPACES_ENABLE: z.boolean().default(false),
});

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

function parseTargetUsers(targetUsersStr?:string | null): string[] {
/**
* Helper to parse a comma-separated list of Twitter usernames
* (already present in your code).
*/
function parseTargetUsers(targetUsersStr?: string | null): string[] {
if (!targetUsersStr?.trim()) {
return [];
}

return targetUsersStr
.split(',')
.map(user => user.trim())
.filter(Boolean); // Remove empty usernames
/*
.filter(user => {
// Twitter username validation (basic example)
return user && /^[A-Za-z0-9_]{1,15}$/.test(user);
});
*/
.split(",")
.map((user) => user.trim())
.filter(Boolean);
}

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

// This also is organized to serve as a point of documentation for the client
// most of the inputs from the framework (env/character)

// we also do a lot of typing/parsing here
// so we can do it once and only once per character
export async function validateTwitterConfig(
runtime: IAgentRuntime
): Promise<TwitterConfig> {
/**
* Validates or constructs a TwitterConfig object using zod,
* taking values from the IAgentRuntime or process.env as needed.
*/
export async function validateTwitterConfig(runtime: IAgentRuntime): Promise<TwitterConfig> {
try {
const twitterConfig = {
TWITTER_DRY_RUN:
parseBooleanFromText(
runtime.getSetting("TWITTER_DRY_RUN") ||
process.env.TWITTER_DRY_RUN
) ?? false, // parseBooleanFromText return null if "", map "" to false

TWITTER_USERNAME:
runtime.getSetting ("TWITTER_USERNAME") ||
runtime.getSetting("TWITTER_USERNAME") ||
process.env.TWITTER_USERNAME,

TWITTER_PASSWORD:
runtime.getSetting("TWITTER_PASSWORD") ||
process.env.TWITTER_PASSWORD,

TWITTER_EMAIL:
runtime.getSetting("TWITTER_EMAIL") ||
process.env.TWITTER_EMAIL,
MAX_TWEET_LENGTH: // number as string?

MAX_TWEET_LENGTH:
safeParseInt(
runtime.getSetting("MAX_TWEET_LENGTH") ||
process.env.MAX_TWEET_LENGTH
, DEFAULT_MAX_TWEET_LENGTH),
TWITTER_SEARCH_ENABLE: // bool
process.env.MAX_TWEET_LENGTH,
DEFAULT_MAX_TWEET_LENGTH
),

TWITTER_SEARCH_ENABLE:
parseBooleanFromText(
runtime.getSetting("TWITTER_SEARCH_ENABLE") ||
process.env.TWITTER_SEARCH_ENABLE
) ?? false,
TWITTER_2FA_SECRET: // string passthru

TWITTER_2FA_SECRET:
runtime.getSetting("TWITTER_2FA_SECRET") ||
process.env.TWITTER_2FA_SECRET || "",
TWITTER_RETRY_LIMIT: // int

TWITTER_RETRY_LIMIT:
safeParseInt(
runtime.getSetting("TWITTER_RETRY_LIMIT") ||
process.env.TWITTER_RETRY_LIMIT
, 5),
TWITTER_POLL_INTERVAL: // int in seconds
process.env.TWITTER_RETRY_LIMIT,
5
),

TWITTER_POLL_INTERVAL:
safeParseInt(
runtime.getSetting("TWITTER_POLL_INTERVAL") ||
process.env.TWITTER_POLL_INTERVAL
, 120), // 2m
TWITTER_TARGET_USERS: // comma separated string
process.env.TWITTER_POLL_INTERVAL,
120
),

TWITTER_TARGET_USERS:
parseTargetUsers(
runtime.getSetting("TWITTER_TARGET_USERS") ||
process.env.TWITTER_TARGET_USERS
),
POST_INTERVAL_MIN: // int in minutes

POST_INTERVAL_MIN:
safeParseInt(
runtime.getSetting("POST_INTERVAL_MIN") ||
process.env.POST_INTERVAL_MIN
, 90), // 1.5 hours
POST_INTERVAL_MAX: // int in minutes
process.env.POST_INTERVAL_MIN,
90
),

POST_INTERVAL_MAX:
safeParseInt(
runtime.getSetting("POST_INTERVAL_MAX") ||
process.env.POST_INTERVAL_MAX
, 180), // 3 hours
ENABLE_ACTION_PROCESSING: // bool
process.env.POST_INTERVAL_MAX,
180
),

ENABLE_ACTION_PROCESSING:
parseBooleanFromText(
runtime.getSetting("ENABLE_ACTION_PROCESSING") ||
process.env.ENABLE_ACTION_PROCESSING
) ?? false,
ACTION_INTERVAL: // int in minutes (min 1m)

ACTION_INTERVAL:
safeParseInt(
runtime.getSetting("ACTION_INTERVAL") ||
process.env.ACTION_INTERVAL
, 5), // 5 minutes
POST_IMMEDIATELY: // bool
process.env.ACTION_INTERVAL,
5
),

POST_IMMEDIATELY:
parseBooleanFromText(
runtime.getSetting("POST_IMMEDIATELY") ||
process.env.POST_IMMEDIATELY
) ?? false,

TWITTER_SPACES_ENABLE:
parseBooleanFromText(
runtime.getSetting("TWITTER_SPACES_ENABLE") ||
process.env.TWITTER_SPACES_ENABLE
) ?? false,
};

return twitterEnvSchema.parse(twitterConfig);
} catch (error) {
if (error instanceof z.ZodError) {
if (error instanceof ZodError) {
const errorMessages = error.errors
.map((err) => `${err.path.join(".")}: ${err.message}`)
.join("\n");
throw new Error(
`Twitter configuration validation failed:\n${errorMessages}`
);
throw new Error(`Twitter configuration validation failed:\n${errorMessages}`);
}
throw error;
}
Expand Down
39 changes: 35 additions & 4 deletions packages/client-twitter/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,32 @@ import { validateTwitterConfig, TwitterConfig } from "./environment.ts";
import { TwitterInteractionClient } from "./interactions.ts";
import { TwitterPostClient } from "./post.ts";
import { TwitterSearchClient } from "./search.ts";
import { TwitterSpaceClient } from "./spaces.ts";

/**
* A manager that orchestrates all specialized Twitter logic:
* - client: base operations (login, timeline caching, etc.)
* - post: autonomous posting logic
* - search: searching tweets / replying logic
* - interaction: handling mentions, replies
* - space: launching and managing Twitter Spaces (optional)
*/
class TwitterManager {
client: ClientBase;
post: TwitterPostClient;
search: TwitterSearchClient;
interaction: TwitterInteractionClient;
constructor(runtime: IAgentRuntime, twitterConfig:TwitterConfig) {
space?: TwitterSpaceClient;

constructor(runtime: IAgentRuntime, twitterConfig: TwitterConfig) {
// Pass twitterConfig to the base client
this.client = new ClientBase(runtime, twitterConfig);

// Posting logic
this.post = new TwitterPostClient(this.client, runtime);

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

// Mentions and interactions
this.interaction = new TwitterInteractionClient(this.client, runtime);

// Optional Spaces logic (enabled if TWITTER_SPACES_ENABLE is true)
if (twitterConfig.TWITTER_SPACES_ENABLE) {
this.space = new TwitterSpaceClient(this.client, runtime);
}
}
}

export const TwitterClientInterface: Client = {
async start(runtime: IAgentRuntime) {
const twitterConfig:TwitterConfig = await validateTwitterConfig(runtime);
const twitterConfig: TwitterConfig = await validateTwitterConfig(runtime);

elizaLogger.log("Twitter client started");

const manager = new TwitterManager(runtime, twitterConfig);

// Initialize login/session
await manager.client.init();

// Start the posting loop
await manager.post.start();

if (manager.search)
// Start the search logic if it exists
if (manager.search) {
await manager.search.start();
}

// Start interactions (mentions, replies)
await manager.interaction.start();

// If Spaces are enabled, start the periodic check
if (manager.space) {
manager.space.startPeriodicSpaceCheck();
}

return manager;
},

async stop(_runtime: IAgentRuntime) {
elizaLogger.warn("Twitter client does not support stopping yet");
},
Expand Down
Loading
Loading