Skip to content
New issue

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

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

Already on GitHub? Sign in 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 all 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
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ LIVEPEER_IMAGE_MODEL= # Default: ByteDance/SDXL-Lightning
# Speech Synthesis
ELEVENLABS_XI_API_KEY= # API key from elevenlabs

# Transcription Provider
TRANSCRIPTION_PROVIDER= # Default: local (possible values: openai, deepgram, local)

# Direct Client Setting
EXPRESS_MAX_PAYLOAD= # Default: 100kb

Expand All @@ -67,6 +70,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
95 changes: 63 additions & 32 deletions packages/client-twitter/src/environment.ts
odilitime marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
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(4, 'An X/Twitter Username must be at least 4 characters long')
.min(1, 'An X/Twitter Username must be at least 1 characters long')
.max(15, 'n X/Twitter Username cannot exceed 15 characters')
.regex(/^[A-Za-z0-9_]*$/, 'n X Username can only contain letters, numbers, and underscores');

/**
* 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, "X/Twitter username is required"),
Expand Down Expand Up @@ -51,25 +56,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,88 +81,116 @@ function safeParseInt(value: string | undefined | null, defaultValue: number): n
return isNaN(parsed) ? defaultValue : Math.max(1, parsed);
}

/**
* Validates or constructs a TwitterConfig object using zod,
* taking values from the IAgentRuntime or process.env as needed.
*/
// 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> {
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?
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
runtime.getSetting("TWITTER_2FA_SECRET") ||
process.env.TWITTER_2FA_SECRET || "",

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

TWITTER_POLL_INTERVAL: // int in seconds
safeParseInt(
runtime.getSetting("TWITTER_POLL_INTERVAL") ||
process.env.TWITTER_POLL_INTERVAL
, 120), // 2m
process.env.TWITTER_POLL_INTERVAL,
120 // 2m
),

TWITTER_TARGET_USERS: // comma separated string
parseTargetUsers(
runtime.getSetting("TWITTER_TARGET_USERS") ||
process.env.TWITTER_TARGET_USERS
),

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

POST_INTERVAL_MAX: // int in minutes
safeParseInt(
runtime.getSetting("POST_INTERVAL_MAX") ||
process.env.POST_INTERVAL_MAX
, 180), // 3 hours
process.env.POST_INTERVAL_MAX,
180 // 3 hours
),

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

ACTION_INTERVAL: // init in minutes (min 1m)
safeParseInt(
runtime.getSetting("ACTION_INTERVAL") ||
process.env.ACTION_INTERVAL
, 5), // 5 minutes
process.env.ACTION_INTERVAL,
5 // 5 minutes
),

POST_IMMEDIATELY: // bool
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");
Expand Down
45 changes: 40 additions & 5 deletions packages/client-twitter/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,39 @@
import { Client, elizaLogger, IAgentRuntime } from "@elizaos/core";
import {
Client,
elizaLogger,
IAgentRuntime,
} from "@elizaos/core";
import { ClientBase } from "./base.ts";
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 +42,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