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 Post Action Implementation #1422

Merged
merged 6 commits into from
Dec 27, 2024
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
1 change: 1 addition & 0 deletions agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"@elizaos/plugin-multiversx": "workspace:*",
"@elizaos/plugin-near": "workspace:*",
"@elizaos/plugin-zksync-era": "workspace:*",
"@elizaos/plugin-twitter": "workspace:*",
"@elizaos/plugin-cronoszkevm": "workspace:*",
"@elizaos/plugin-3d-generation": "workspace:*",
"readline": "1.3.0",
Expand Down
6 changes: 6 additions & 0 deletions packages/plugin-twitter/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!package.json
!readme.md
!tsup.config.ts
17 changes: 17 additions & 0 deletions packages/plugin-twitter/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@elizaos/plugin-twitter",
"version": "0.1.7-alpha.1",
"main": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"dependencies": {
"@elizaos/core": "workspace:*",
"agent-twitter-client": "^1.0.0",
"tsup": "8.3.5"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"test": "vitest run"
}
}
237 changes: 237 additions & 0 deletions packages/plugin-twitter/src/actions/post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
import {
Action,
IAgentRuntime,
Memory,
State,
composeContext,
elizaLogger,
ModelClass,
formatMessages,
generateObject,
} from "@elizaos/core";
import { Scraper } from "agent-twitter-client";
import { tweetTemplate } from "../templates";
import { isTweetContent, TweetSchema } from "../types";

async function composeTweet(
runtime: IAgentRuntime,
_message: Memory,
state?: State
): Promise<string> {
try {
const context = composeContext({
state,
template: tweetTemplate,
});

const tweetContentObject = await generateObject({
runtime,
context,
modelClass: ModelClass.SMALL,
schema: TweetSchema,
stop: ["\n"],
});

if (!isTweetContent(tweetContentObject.object)) {
elizaLogger.error(
"Invalid tweet content:",
tweetContentObject.object
);
return;
}

const trimmedContent = tweetContentObject.object.text.trim();

// Skip truncation if TWITTER_PREMIUM is true
if (
process.env.TWITTER_PREMIUM?.toLowerCase() !== "true" &&
trimmedContent.length > 180
) {
elizaLogger.warn(
`Tweet too long (${trimmedContent.length} chars), truncating...`
);
return trimmedContent.substring(0, 177) + "...";
}

return trimmedContent;
} catch (error) {
elizaLogger.error("Error composing tweet:", error);
throw error;
}
}

async function postTweet(content: string): Promise<boolean> {
try {
const scraper = new Scraper();
const username = process.env.TWITTER_USERNAME;
const password = process.env.TWITTER_PASSWORD;
const email = process.env.TWITTER_EMAIL;
const twitter2faSecret = process.env.TWITTER_2FA_SECRET;

if (!username || !password) {
elizaLogger.error(
"Twitter credentials not configured in environment"
);
return false;
}

// Login with credentials
await scraper.login(username, password, email, twitter2faSecret);
if (!(await scraper.isLoggedIn())) {
elizaLogger.error("Failed to login to Twitter");
return false;
}

// Send the tweet
elizaLogger.log("Attempting to send tweet:", content);
const result = await scraper.sendTweet(content);

const body = await result.json();
elizaLogger.log("Tweet response:", body);

// Check for Twitter API errors
if (body.errors) {
const error = body.errors[0];
elizaLogger.error(
`Twitter API error (${error.code}): ${error.message}`
);
return false;
}

// Check for successful tweet creation
if (!body?.data?.create_tweet?.tweet_results?.result) {
elizaLogger.error(
"Failed to post tweet: No tweet result in response"
);
return false;
}

return true;
} catch (error) {
// Log the full error details
elizaLogger.error("Error posting tweet:", {
message: error.message,
stack: error.stack,
name: error.name,
cause: error.cause,
});
return false;
}
}

export const postAction: Action = {
name: "POST_TWEET",
similes: ["TWEET", "POST", "SEND_TWEET"],
description: "Post a tweet to Twitter",
validate: async (
runtime: IAgentRuntime,
message: Memory,
state?: State
) => {
const hasCredentials =
!!process.env.TWITTER_USERNAME && !!process.env.TWITTER_PASSWORD;
elizaLogger.log(`Has credentials: ${hasCredentials}`);

return hasCredentials;
},
handler: async (
runtime: IAgentRuntime,
message: Memory,
state?: State
): Promise<boolean> => {
try {
// Generate tweet content using context
const tweetContent = await composeTweet(runtime, message, state);

if (!tweetContent) {
elizaLogger.error("No content generated for tweet");
return false;
}

elizaLogger.log(`Generated tweet content: ${tweetContent}`);

// Check for dry run mode - explicitly check for string "true"
if (
process.env.TWITTER_DRY_RUN &&
process.env.TWITTER_DRY_RUN.toLowerCase() === "true"
) {
elizaLogger.info(
`Dry run: would have posted tweet: ${tweetContent}`
);
return true;
}

return await postTweet(tweetContent);
} catch (error) {
elizaLogger.error("Error in post action:", error);
return false;
}
},
examples: [
0xPBIT marked this conversation as resolved.
Show resolved Hide resolved
[
{
user: "{{user1}}",
content: { text: "You should tweet that" },
},
{
user: "{{agentName}}",
content: {
text: "I'll share this update with my followers right away!",
action: "POST_TWEET",
},
},
],
[
{
user: "{{user1}}",
content: { text: "Post this tweet" },
},
{
user: "{{agentName}}",
content: {
text: "I'll post that as a tweet now.",
action: "POST_TWEET",
},
},
],
[
{
user: "{{user1}}",
content: { text: "Share that on Twitter" },
},
{
user: "{{agentName}}",
content: {
text: "I'll share this message on Twitter.",
action: "POST_TWEET",
},
},
],
[
{
user: "{{user1}}",
content: { text: "Post that on X" },
},
{
user: "{{agentName}}",
content: {
text: "I'll post this message on X right away.",
action: "POST_TWEET",
},
},
],
[
{
user: "{{user1}}",
content: { text: "You should put that on X dot com" },
},
{
user: "{{agentName}}",
content: {
text: "I'll put this message up on X.com now.",
action: "POST_TWEET",
},
},
],
],
};
12 changes: 12 additions & 0 deletions packages/plugin-twitter/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Plugin } from "@elizaos/core";
import { postAction } from "./actions/post";

export const twitterPlugin: Plugin = {
name: "twitter",
description: "Twitter integration plugin for posting tweets",
actions: [postAction],
evaluators: [],
providers: [],
};

export default twitterPlugin;
22 changes: 22 additions & 0 deletions packages/plugin-twitter/src/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export const tweetTemplate = `
# Context
{{recentMessages}}

# Topics
{{topics}}

# Post Directions
{{postDirections}}

# Recent interactions between {{agentName}} and other users:
{{recentPostInteractions}}

# Task
Generate a tweet that:
1. Relates to the recent conversation or requested topic
2. Matches the character's style and voice
3. Is concise and engaging
4. Must be UNDER 180 characters (this is a strict requirement)
5. Speaks from the perspective of {{agentName}}

Generate only the tweet text, no other commentary.`;
13 changes: 13 additions & 0 deletions packages/plugin-twitter/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { z } from "zod";

export interface TweetContent {
text: string;
}

export const TweetSchema = z.object({
text: z.string().describe("The text of the tweet"),
});

export const isTweetContent = (obj: any): obj is TweetContent => {
return TweetSchema.safeParse(obj).success;
};
13 changes: 13 additions & 0 deletions packages/plugin-twitter/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "../core/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"types": [
"node"
]
},
"include": [
"src/**/*.ts"
]
}
10 changes: 10 additions & 0 deletions packages/plugin-twitter/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from "tsup";

export default defineConfig({
entry: ["src/index.ts"],
outDir: "dist",
sourcemap: true,
clean: true,
format: ["esm"],
external: ["dotenv", "fs", "path", "https", "http", "agentkeepalive"],
});
Loading