Skip to content

Commit

Permalink
Merge pull request #1550 from slkzgm/main
Browse files Browse the repository at this point in the history
feat: Twitter Spaces Integration
  • Loading branch information
lalalune authored Jan 1, 2025
2 parents 41b83b9 + 0c06d22 commit 6f576b6
Show file tree
Hide file tree
Showing 9 changed files with 1,306 additions and 70 deletions.
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
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

0 comments on commit 6f576b6

Please sign in to comment.