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: Primus zkTLS plugin to fully verify agent activities #2086

Merged
merged 28 commits into from
Jan 11, 2025
Merged
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
3b16a4c
add primus plugin
WuEcho Jan 6, 2025
b6eae8d
update primus zktls core sdk invoke
fksyuan Jan 6, 2025
de6b982
fix model name bug and update primus attestation decode
fksyuan Jan 6, 2025
1a098b4
Proof provider and action for twitter-plugin
xudean Jan 7, 2025
b750bfb
update readme log
fksyuan Jan 7, 2025
d8df612
update README in plugin-twitter-primus
xudean Jan 7, 2025
e00f5ea
Merge remote-tracking branch 'origin/develop' into develop
xudean Jan 7, 2025
adb2374
Merge remote-tracking branch 'upstream/develop' into develop
xudean Jan 8, 2025
e2e9063
update npm package 'zktls-core-sdk'
xudean Jan 8, 2025
0563ed5
update README in plugin-primus and plugin-twitter-plugin
xudean Jan 8, 2025
e33cead
get tweet by UserScreenName
xudean Jan 9, 2025
02ef838
refactor plugin-primus
xudean Jan 9, 2025
8266043
refine readme in plugin-primus
xiangxiecrypto Jan 9, 2025
d05c974
fix format
xiangxiecrypto Jan 9, 2025
d8d3ce7
update readme in plugin-primus
xudean Jan 9, 2025
adfd2be
Merge remote-tracking branch 'origin/develop' into develop
xudean Jan 9, 2025
d389b22
Merge remote-tracking branch 'upstream/develop' into develop
xudean Jan 9, 2025
8122abc
fix readme format
xudean Jan 9, 2025
c8500cd
fix code error
xudean Jan 9, 2025
4f08094
update readme in plugin-primus
xudean Jan 9, 2025
14fe671
update import
xudean Jan 9, 2025
ff931a9
update import
xudean Jan 9, 2025
730b929
refine readme
xiangxiecrypto Jan 9, 2025
b6818ec
chore: refine readme
xiangxiecrypto Jan 10, 2025
d55da5f
Merge branch 'develop' into develop
xudean Jan 10, 2025
3a3a9f2
Merge remote-tracking branch 'upstream/develop' into develop
xudean Jan 11, 2025
9bf45e0
Merge remote-tracking branch 'upstream/develop' into develop
xudean Jan 11, 2025
d90f838
resolve conversions problems
xudean Jan 11, 2025
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
@@ -72,6 +72,7 @@
"@elizaos/plugin-near": "workspace:*",
"@elizaos/plugin-zksync-era": "workspace:*",
"@elizaos/plugin-twitter": "workspace:*",
"@elizaos/plugin-primus": "workspace:*",
"@elizaos/plugin-cronoszkevm": "workspace:*",
"@elizaos/plugin-3d-generation": "workspace:*",
"@elizaos/plugin-fuel": "workspace:*",
17 changes: 17 additions & 0 deletions agent/src/index.ts
Original file line number Diff line number Diff line change
@@ -10,6 +10,9 @@ import { SlackClientInterface } from "@elizaos/client-slack";
import { TelegramClientInterface } from "@elizaos/client-telegram";
import { TwitterClientInterface } from "@elizaos/client-twitter";
// import { ReclaimAdapter } from "@elizaos/plugin-reclaim";
import { DirectClient } from "@elizaos/client-direct";
import { PrimusAdapter } from "@elizaos/plugin-primus";

import {
AgentRuntime,
CacheManager,
@@ -98,6 +101,7 @@ import net from "net";
import path from "path";
import { fileURLToPath } from "url";
import yargs from "yargs";
import {dominosPlugin} from "@elizaos/plugin-dominos";

const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
const __dirname = path.dirname(__filename); // get the name of the directory
@@ -622,6 +626,19 @@ export async function createAgent(
elizaLogger.log("modelProvider", character.modelProvider);
elizaLogger.log("token", token);
}
if (
process.env.PRIMUS_APP_ID &&
process.env.PRIMUS_APP_SECRET &&
process.env.VERIFIABLE_INFERENCE_ENABLED === "true"){
verifiableInferenceAdapter = new PrimusAdapter({
appId: process.env.PRIMUS_APP_ID,
appSecret: process.env.PRIMUS_APP_SECRET,
attMode: "proxytls",
modelProvider: character.modelProvider,
token,
});
elizaLogger.log("Verifiable inference primus adapter initialized");
}

return new AgentRuntime({
databaseAdapter: db,
2 changes: 2 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
@@ -1477,7 +1477,9 @@ export interface ISlackService extends Service {
* Available verifiable inference providers
*/
export enum VerifiableInferenceProvider {
RECLAIM = "reclaim",
OPACITY = "opacity",
PRIMUS = "primus",
}

/**
6 changes: 6 additions & 0 deletions packages/plugin-primus/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*

!dist/**
!package.json
!readme.md
!tsup.config.ts
208 changes: 208 additions & 0 deletions packages/plugin-primus/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
# @elizaos/plugin-primus

A plugin to fully verify agent activities, including LLM access, actions, and interactions with external providers,
powered by Primus' zkTLS protocol.

## Overview

In the Eliza framework, an agent consists of three key components: a brain (accessing an LLM), actions (the tasks the
agent performs), and perception (gathering external information from providers). To fully verify agent activities, it's
essential to ensure that the agent's thoughts, actions, and external information requests are all verifiable. This
plugin enables full verification of these activities.

The current plugin includes:

- Verification of inference from OpenAI's LLM.
- An example for verifying actions, such as posting a tweet (this can be extended to any other actions).
- An example to verify that the Bitcoin price is accurately fetched from Binance (this can be extended to any other data
providers).

## Usage
### LLM inference verification (PrimusAdapter)
`PrimusAdapter` implements `IVerifiableInferenceAdapter` and can be used as follows.
```typescript
import {PrimusAdapter} from "@elizaos/plugin-primus";
import {VerifiableInferenceOptions} from '@elizaos/core';

// Initialize primus adapter
const primusAdatper = new PrimusAdapter({
appId: process.env.PRIMUS_APP_ID,
appSecret: process.env.PRIMUS_APP_SECRET,
// Choose MPC-TLS or Proxy-TLS
attMode: "proxytls",
modelProvider: character.modelProvider,
token,
});

interface PrimusOptions {
appId: string;
appSecret: string;
attMode: string;
modelProvider?: ModelProviderName;
token?: string;
}

// The options for generating an attestation
const options: VerifiableInferenceOptions = {
// Optional: Override the default endpoint
endpoint: "https://api.openapi.com/chat/completions",
// Optional: Add custom headers
headers: {
"Content-Type": "application/json",
"Authorization": "bearer Token",
},
// Optional: Provider-specific options
providerOptions: {
temperature: 0.7,
},
};

// Generate an attestation for a network request.
const result = await primusAdapter.generateText(context, "gpt-4o", options);
// Verify the validity of the attestation.
const isValid = await primusAdapter.verifyProof(result.proof);
```

The core functions in `PrimusAdatper` are the following, which are also used in Actions and Providers.
```typescript
// Generate a zkTLS proof.
generateProof = async (
// The target endpoint of the network request.
endpoint: string,
// The HTTP method of the request, such as 'GET', 'POST', etc.
method: string,
// A record containing the headers of the request.
headers: Record<string, any>,
// The body of the request. It should be a string.
body: string,
//A [JSONPath](https://datatracker.ietf.org/doc/rfc9535/) expression to locate the specific field in the response you want to attest.
responseParsePath: string
): Promise<any>

// Verify the proof.
verifyProof = async (attestation: any): Promise<boolean>

```

### Verify the interaction with Providers

Here’s an example showcasing how to verify the validity of the BTC price retrieved from Binance. Developers can easily customize this process for other providers.

```typescript
const tokenPriceProvider: Provider = {
get: async (runtime: IAgentRuntime, message: Memory, _state?: State) => {
// Set the URL
const url = "https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT";
const method = 'GET';
const headers = {
'Accept ': '*/*',
};
// Generate the proof
const attestation = await generateProof(url, method, headers, "", "$.price");
// Verify the proof.
const valid = await verifyProof(attestation);
if (!valid) {
throw new Error("Invalid price attestation");
}
......
},
};
```

### Verify the Actions
Below is an example showcasing how to post price information from the [tokenPriceProvider](./src/providers/tokenPriceProvider.ts) to Twitter. Developers can easily adapt this process for other providers.

Note that you need to configure the `.env` file correctly to post tweets.
```typescript
export const postTweetAction: Action = {
description: "Post a tweet on Twitter and be verified by Primus",
examples: [],
handler: async (
runtime: IAgentRuntime,
message: Memory,
state?: State
): Promise<boolean> => {
const contentYouWantToPost = await tokenPriceProvider.get(runtime, message, state);
const endpoint = 'https://twitter.com/i/api/graphql/a1p9RWpkYKBjWv_I3WzS-A/CreateTweet';
const method = 'POST';
const attestation = await generateProof(endpoint,method,headers,bodyStr,"$.data.create_tweet.tweet_results.result.rest_id");
elizaLogger.info(
"Tweet posting proof generated successfully:",
attestation
);
const verifyResult = verifyProof(attestation);
if (!verifyResult) {
throw new Error(
"Attestation verify failed, data from source is illegality"
);
}

},
name: "POST_TWEET",
similes: [],
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;
},
};
```

## Installation

```bash
pnpm add @elizaos/plugin-primus
```

## Configuration

Add the following environment variables to your .env file:

```
PRIMUS_APP_ID=your_app_id
PRIMUS_APP_SECRET=your_app_secret
VERIFIABLE_INFERENCE_ENABLED=true
VERIFIABLE_INFERENCE_PROVIDER=primus
```

***How to get PRIMUS_APP_ID and PRIMUS_APP_SECRET***

1. Visit the [Primus Developer Hub](https://dev.primuslabs.xyz/).
2. Create a new project
3. Save your 'Application ID(PRIMUS_APP_ID)' and 'Secret Key(PRIMUS_APP_SECRET)'

To use the plugin, add `@elizaos/plugin-primus` to the plugins field in your character file. Here's an example of how your character file might look after the update:

```json
{
"name": "trump",
"modelProvider": "openai",
// just support openai now
"plugins": [
"@elizaos/plugin-primus"
],
// other fields
.....
}
```

## Run

```bash
# Start the server
pnpm start --characters="characters/xxx.character.json"
```

```bash
# Start the client
pnpm start:client
```

You can ask the agent: "Get the BTC price and tweet."

32 changes: 32 additions & 0 deletions packages/plugin-primus/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@elizaos/plugin-primus",
"version": "0.1.7",
"type": "module",
"main": "dist/index.js",
"module": "dist/index.js",
"types": "dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"@elizaos/source": "./src/index.ts",
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
}
},
"files": [
"dist"
],
"dependencies": {
"@elizaos/core": "workspace:*",
"agent-twitter-client": "0.0.18",
"@primuslabs/zktls-core-sdk": "^0.1.0",
"tsup": "8.3.5"
},
"scripts": {
"build": "tsup --format esm --dts",
"dev": "tsup --format esm --dts --watch",
"test": "vitest run"
}
}
127 changes: 127 additions & 0 deletions packages/plugin-primus/src/actions/postTweetAction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import {
Action,
elizaLogger,
IAgentRuntime,
Memory,
State,
} from "@elizaos/core";
import { TwitterScraper } from "../util/twitterScraper.ts";
import {tokenPriceProvider} from "../providers/tokenPriceProvider.ts";

export const postTweetAction: Action = {
description: "Post a tweet on Twitter and be verified by Primus",
examples: [
[
{
user: "{{user1}}",
content: {
text: "Get the latest BTC price and post it on my twitter.",
},
},
{
user: "{{agentName}}",
content: {
text: "The latest tweet has posted.",
action: "POST_TWEET",
},
},
],
[
{
user: "{{user1}}",
content: {
text: "Help post a tweet which content is BTC price.",
},
},
{
user: "{{agentName}}",
content: {
text: "Completed!",
action: "POST_TWEET",
},
},
],
[
{
user: "{{user1}}",
content: {
text: "Post a tweet on twitter for me.",
},
},
{
user: "{{agentName}}",
content: {
text: "I'll post the latest tweet to your Twitter account now!",
action: "POST_TWEET",
},
},
],
],
handler: async (
runtime: IAgentRuntime,
message: Memory,
state?: State
): Promise<boolean> => {
const contentYouWantToPost = await tokenPriceProvider.get(runtime, message, state);
//check VERIFIABLE_INFERENCE_ENABLED
if (
!(
process.env.VERIFIABLE_INFERENCE_ENABLED === "true" &&
process.env.PRIMUS_APP_ID &&
process.env.PRIMUS_APP_SECRET
)
) {
elizaLogger.error(
`Parameter 'VERIFIABLE_INFERENCE_ENABLED' not set, Eliza will run this action!`
);
return false;
}

try {
if (
process.env.TWITTER_DRY_RUN &&
process.env.TWITTER_DRY_RUN.toLowerCase() === "true"
) {
elizaLogger.info(
`Dry run: would have posted tweet: ${contentYouWantToPost}`
);
return true;
}

const scraperWithPrimus = new TwitterScraper();
await scraperWithPrimus.login();
if (!(await scraperWithPrimus.getScraper().isLoggedIn())) {
elizaLogger.error("Failed to login to Twitter");
return false;
}
// post the tweet
elizaLogger.log("Attempting to send tweet:", contentYouWantToPost);
const result = await scraperWithPrimus.sendTweet(contentYouWantToPost);

elizaLogger.log("Tweet response:", result);

// Check for Twitter API errors
if (!result) {
elizaLogger.error(`Twitter API error ${result}`);
return false;
}
return true;
} catch (error) {
elizaLogger.error("Error in post action:", error);
return false;
}
},
name: "POST_TWEET",
similes: ["TWEET", "POST", "SEND_TWEET"],
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;
},
};
97 changes: 97 additions & 0 deletions packages/plugin-primus/src/adapter/primusAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { PrimusCoreTLS } from "@primuslabs/zktls-core-sdk";
import {
IVerifiableInferenceAdapter,
VerifiableInferenceOptions,
VerifiableInferenceResult,
VerifiableInferenceProvider,
ModelProviderName,
models,
elizaLogger,
} from "@elizaos/core";
import {generateProof, verifyProof} from "../util/primusUtil.ts";

interface PrimusOptions {
appId: string;
appSecret: string;
attMode: string;
modelProvider?: ModelProviderName;
token?: string;
}

export class PrimusAdapter implements IVerifiableInferenceAdapter {
public options: PrimusOptions;

constructor(options: PrimusOptions) {
this.options = options;
}

async generateText(
context: string,
modelClass: string,
options?: VerifiableInferenceOptions
): Promise<VerifiableInferenceResult> {
const provider = this.options.modelProvider || ModelProviderName.OPENAI;
const baseEndpoint = options?.endpoint || models[provider].endpoint;
const model = models[provider].model[modelClass];
const apiKey = this.options.token;

if (!apiKey) {
throw new Error(
`API key (token) is required for provider: ${provider}`
);
}

// Get provider-specific endpoint, auth header and response json path
let endpoint;
let authHeader;
let responseParsePath;

switch (provider) {
case ModelProviderName.OPENAI:
endpoint = `${baseEndpoint}/chat/completions`;
authHeader = `Bearer ${apiKey}`;
responseParsePath = "$.choices[0].message.content";
break;
default:
throw new Error(`Unsupported model provider: ${provider}`);
}


const headers = {
"Content-Type": "application/json",
"Authorization": authHeader,
};

try {
let body = {
model: model.name,
messages: [{ role: "user", content: context }],
temperature:
options?.providerOptions?.temperature ||
models[provider].model[modelClass].temperature,
};
const attestation = await generateProof(endpoint,"POST",headers,JSON.stringify(body),responseParsePath);
elizaLogger.log(`model attestation:`, attestation);

const responseData = JSON.parse(attestation.data);
let text = JSON.parse(responseData.content);
return {
text,
proof: attestation,
provider: VerifiableInferenceProvider.PRIMUS,
timestamp: Date.now(),
};
} catch (error) {
console.error("Error in Primus generateText:", error);
throw error;
}
}

async verifyProof(result: VerifiableInferenceResult): Promise<boolean> {
const isValid = verifyProof(result.proof)
elizaLogger.log("Proof is valid:", isValid);
return isValid;
}
}

export default PrimusAdapter;
14 changes: 14 additions & 0 deletions packages/plugin-primus/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Plugin } from "@elizaos/core";
import { postTweetAction } from "./actions/postTweetAction.ts";
import {PrimusAdapter} from "./adapter/primusAdapter.ts";

export const twitterPlugin: Plugin = {
name: "twitter",
description: "Twitter integration plugin for posting tweets with proof generated by primus",
actions: [postTweetAction],
evaluators: [],
providers: [],
};

export default twitterPlugin;
export {PrimusAdapter};
35 changes: 35 additions & 0 deletions packages/plugin-primus/src/providers/tokenPriceProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {elizaLogger, IAgentRuntime, Memory, Provider, State} from "@elizaos/core";
import {generateProof, verifyProof} from "../util/primusUtil.ts";

const tokenPriceProvider: Provider = {
get: async (runtime: IAgentRuntime, message: Memory, _state?: State) => {
//get btc price
const url = `${process.env.BINANCE_API_URL||'https://api.binance.com'}/api/v3/ticker/price?symbol=${process.env.BINANCE_SYMBOL || 'BTCUSDT'}`;
const method = 'GET';
const headers = {
'Accept ': '*/*',
};
const attestation = await generateProof(url, method, headers, "", "$.price");
const valid = await verifyProof(attestation);
if(!valid){
throw new Error("Invalid price attestation");
}
elizaLogger.info('price attestation:',attestation);
try{
const responseData = JSON.parse((attestation as any).data);
const price = responseData.content;
return `
Get BTC price from Binance:
BTC: ${price} USDT
Time: ${new Date().toUTCString()}
POST by eliza #eliza
Attested by Primus #primus #zktls
`
}catch (error){
elizaLogger.error('Failed to parse price data:', error);
throw new Error('Failed to parse price data');
}
},
};

export { tokenPriceProvider };
39 changes: 39 additions & 0 deletions packages/plugin-primus/src/providers/tweetProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import {elizaLogger, IAgentRuntime, Memory, Provider, State} from "@elizaos/core";
import {TwitterScraper} from "../util/twitterScraper.ts";

const tweetProvider: Provider = {
get: async (runtime: IAgentRuntime, message: Memory, _state?: State) => {
const scraperWithPrimus = new TwitterScraper();
try {
elizaLogger.info("Attempting Twitter login");
await scraperWithPrimus.login();
elizaLogger.info("Twitter login successful");
}catch (error){
elizaLogger.error("Twitter login failed:", error);
return false;
}

if (!(await scraperWithPrimus.getScraper().isLoggedIn())) {
elizaLogger.error("Failed to login to Twitter");
return false;
}
const userName = process.env.TWITTER_USERNAME_WANT_TO_GET_TWEET;
if(!userName){
elizaLogger.error("TWITTER_USERNAME_WANT_TO_GET_TWEET is not set");
return false;
}
elizaLogger.debug(`Fetching tweets for user: ${userName}`);
const userId = await scraperWithPrimus.getUserIdByScreenName(userName);
elizaLogger.debug(`Fetching tweets for user: ${userName}`);
try {
const result = await scraperWithPrimus.getUserLatestTweet(userId);
elizaLogger.debug("Tweet retrieved successfully");
return result;
} catch (error) {
elizaLogger.error("Failed to fetch tweet:", error);
return false;
}
},
};

export { tweetProvider };
21 changes: 21 additions & 0 deletions packages/plugin-primus/src/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export const summarizeTweetTemplate = (twitterContent:string) => {
if (!twitterContent?.trim()) {
throw new Error('Twitter content cannot be empty');
}
return `
# Context
${twitterContent}
# Task
Generate a tweet that:
1. Summarize the input
2. The content does not contain emoji
3. Must be less than 280 characters (Twitter's limit)
4. The key information should be retained
5. Is concise and engaging
Generate only the tweet text, no other commentary.
Response format should be formatted in a JSON block like this:
{"text": "string", "characterCount": number}
`;
};
42 changes: 42 additions & 0 deletions packages/plugin-primus/src/util/primusUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { PrimusCoreTLS,Attestation } from "@primuslabs/zktls-core-sdk";

export const generateProof = async (
endpoint: string,
method: string,
headers: Record<string, any>,
body: string,
responseParsePath: string
): Promise<Attestation> => {
const zkTLS = new PrimusCoreTLS();
await zkTLS.init(process.env.PRIMUS_APP_ID, process.env.PRIMUS_APP_SECRET);
const requestParam = body
? {
url: endpoint,
method: method,
header: headers,
body: body,
}
: {
url: endpoint,
method: method,
header: headers,
};
// console.log('requestParam:',requestParam)
const attestationParams = zkTLS.generateRequestParams(requestParam, [
{
keyName: "content",
parsePath: responseParsePath,
parseType: "string",
},
]);
attestationParams.setAttMode({
algorithmType: "proxytls",
});
return await zkTLS.startAttestation(attestationParams);
};

export const verifyProof = async (attestation: Attestation): Promise<boolean> => {
const zkTLS = new PrimusCoreTLS();
await zkTLS.init(process.env.PRIMUS_APP_ID, process.env.PRIMUS_APP_SECRET);
return zkTLS.verifyAttestation(attestation);
};
260 changes: 260 additions & 0 deletions packages/plugin-primus/src/util/twitterScraper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
import { Scraper } from "agent-twitter-client";
import { elizaLogger } from "@elizaos/core";
import { verifyProof, generateProof } from "./primusUtil.ts";

export class TwitterScraper {
private scraper: Scraper;

constructor() {}

public getScraper(): Scraper {
return this.scraper;
}

public async getUserIdByScreenName(screenName: string) {
return await this.scraper.getUserIdByScreenName(screenName);
}

public async login() {
this.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;
}

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

public async getUserLatestTweet(userId: string) {
const onboardingTaskUrl =
"https://api.twitter.com/1.1/onboarding/task.json";
const cookies = await (this.scraper as any).auth
.cookieJar()
.getCookies(onboardingTaskUrl);
const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0");

//@ ts-expect-error - This is a private API.
const headers = {
authorization: `Bearer ${(this.scraper as any).auth.bearerToken}`,
cookie: await (this.scraper as any).auth
.cookieJar()
.getCookieString(onboardingTaskUrl),
"content-type": "application/json",
"User-Agent":
"Mozilla/5.0 (Linux; Android 11; Nokia G20) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.88 Mobile Safari/537.36",
"x-guest-token": (this.scraper as any).guestToken,
"x-twitter-auth-type": "OAuth2Client",
"x-twitter-active-user": "yes",
"x-twitter-client-language": "en",
"x-csrf-token": xCsrfToken?.value,
};

const variables = {
userId: userId,
count: 1,
includePromotedContent: true,
withQuickPromoteEligibilityTweetFields: true,
withVoice: true,
withV2Timeline: true,
};
const features = {
profile_label_improvements_pcf_label_in_post_enabled: false,
rweb_tipjar_consumption_enabled: true,
tweetypie_unmention_optimization_enabled: false,
responsive_web_graphql_exclude_directive_enabled: true,
verified_phone_label_enabled: false,
creator_subscriptions_tweet_preview_api_enabled: true,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled:
false,
premium_content_api_read_enabled: false,
communities_web_enable_tweet_community_results_fetch: true,
c9s_tweet_anatomy_moderator_badge_enabled: true,
responsive_web_grok_analyze_button_fetch_trends_enabled: false,
responsive_web_grok_analyze_post_followups_enabled: true,
responsive_web_grok_share_attachment_enabled: true,
articles_preview_enabled: true,
responsive_web_edit_tweet_api_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled: true,
view_counts_everywhere_api_enabled: true,
longform_notetweets_consumption_enabled: true,
responsive_web_twitter_article_tweet_consumption_enabled: true,
tweet_awards_web_tipping_enabled: false,
creator_subscriptions_quote_tweet_preview_enabled: false,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:
true,
rweb_video_timestamps_enabled: true,
longform_notetweets_rich_text_read_enabled: true,
longform_notetweets_inline_media_enabled: true,
responsive_web_enhance_cards_enabled: false,
};
const fieldToggles = {
withArticlePlainText: false,
};
const variablesUrlEncoded = encodeURIComponent(
JSON.stringify(variables)
);
const featureUrlEncoded = encodeURIComponent(JSON.stringify(features));
const fieldTogglesUrlEncoded = encodeURIComponent(
JSON.stringify(fieldToggles)
);
const endpoint = `https://twitter.com/i/api/graphql/V7H0Ap3_Hh2FyS75OCDO3Q/UserTweets?variables=${variablesUrlEncoded}&features=${featureUrlEncoded}&fieldToggles=${fieldTogglesUrlEncoded}`;
const responseParsePath =
"$.data.user.result.timeline_v2.timeline.instructions[1].entry.content.itemContent.tweet_results.result.legacy.full_text";
const attestation = await generateProof(
endpoint,
"GET",
headers,
undefined,
responseParsePath
);
//log attestation
elizaLogger.info(
"Tweet getting proof generated successfully:",
attestation
);
const verifyResult = verifyProof(attestation);
if (!verifyResult) {
throw new Error(
"Verify attestation failed,data from source is illegality"
);
}
const responseData = JSON.parse(attestation.data);
const content = responseData.content;
//log
elizaLogger.info(`get tweet content success:${content}`);
return this.removeEmojis(content);
}

private isEmoji(char: string) {
const codePoint = char.codePointAt(0);
return (
(codePoint >= 0x1f600 && codePoint <= 0x1f64f) ||
(codePoint >= 0x1f300 && codePoint <= 0x1f5ff) ||
(codePoint >= 0x1f680 && codePoint <= 0x1f6ff) ||
(codePoint >= 0x2600 && codePoint <= 0x26ff) ||
(codePoint >= 0x2700 && codePoint <= 0x27bf) ||
(codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
(codePoint >= 0x1f1e6 && codePoint <= 0x1f1ff)
);
}

private removeEmojis(input: string) {
return Array.from(input)
.filter((char) => !this.isEmoji(char))
.join("");
}

public async sendTweet(content: string) {
const onboardingTaskUrl =
"https://api.twitter.com/1.1/onboarding/task.json";

const cookies = await (this.scraper as any).auth
.cookieJar()
.getCookies(onboardingTaskUrl);
const xCsrfToken = cookies.find((cookie) => cookie.key === "ct0");

//@ ts-expect-error - This is a private API.
const headers = {
authorization: `Bearer ${(this.scraper as any).auth.bearerToken}`,
cookie: await (this.scraper as any).auth
.cookieJar()
.getCookieString(onboardingTaskUrl),
"content-type": "application/json",
"User-Agent":
"Mozilla/5.0 (Linux; Android 11; Nokia G20) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.88 Mobile Safari/537.36",
"x-guest-token": (this.scraper as any).guestToken,
"x-twitter-auth-type": "OAuth2Client",
"x-twitter-active-user": "yes",
"x-twitter-client-language": "en",
"x-csrf-token": xCsrfToken?.value,
};

const variables = {
tweet_text: content,
dark_request: false,
media: {
media_entities: [],
possibly_sensitive: false,
},
semantic_annotation_ids: [],
};
const bodyStr = JSON.stringify({
variables,
features: {
interactive_text_enabled: true,
longform_notetweets_inline_media_enabled: false,
responsive_web_text_conversations_enabled: false,
tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled:
false,
vibe_api_enabled: false,
rweb_lists_timeline_redesign_enabled: true,
responsive_web_graphql_exclude_directive_enabled: true,
verified_phone_label_enabled: false,
creator_subscriptions_tweet_preview_api_enabled: true,
responsive_web_graphql_timeline_navigation_enabled: true,
responsive_web_graphql_skip_user_profile_image_extensions_enabled:
false,
tweetypie_unmention_optimization_enabled: true,
responsive_web_edit_tweet_api_enabled: true,
graphql_is_translatable_rweb_tweet_is_translatable_enabled:
true,
view_counts_everywhere_api_enabled: true,
longform_notetweets_consumption_enabled: true,
tweet_awards_web_tipping_enabled: false,
freedom_of_speech_not_reach_fetch_enabled: true,
standardized_nudges_misinfo: true,
longform_notetweets_rich_text_read_enabled: true,
responsive_web_enhance_cards_enabled: false,
subscriptions_verification_info_enabled: true,
subscriptions_verification_info_reason_enabled: true,
subscriptions_verification_info_verified_since_enabled: true,
super_follow_badge_privacy_enabled: false,
super_follow_exclusive_tweet_notifications_enabled: false,
super_follow_tweet_api_enabled: false,
super_follow_user_api_enabled: false,
android_graphql_skip_api_media_color_palette: false,
creator_subscriptions_subscription_count_enabled: false,
blue_business_profile_image_shape_enabled: false,
unified_cards_ad_metadata_container_dynamic_card_content_query_enabled:
false,
rweb_video_timestamps_enabled: false,
c9s_tweet_anatomy_moderator_badge_enabled: false,
responsive_web_twitter_article_tweet_consumption_enabled: false,
},
fieldToggles: {},
});
const endpoint = 'https://twitter.com/i/api/graphql/a1p9RWpkYKBjWv_I3WzS-A/CreateTweet';
const method = 'POST';
const attestation = await generateProof(endpoint,method,headers,bodyStr,"$.data.create_tweet.tweet_results.result.rest_id");

elizaLogger.info(
"Tweet posting proof generated successfully:",
attestation
);

const verifyResult = verifyProof(attestation);
if (!verifyResult) {
throw new Error(
"Verify attestation failed, data from source is illegality"
);
}
const responseData = JSON.parse(attestation.data);
elizaLogger.info(`send tweet success,tweetId:${responseData.content}`);

return responseData.content;
}
}
9 changes: 9 additions & 0 deletions packages/plugin-primus/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../core/tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src/**/*.ts"]
}
10 changes: 10 additions & 0 deletions packages/plugin-primus/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"],
});
6,994 changes: 2,231 additions & 4,763 deletions pnpm-lock.yaml

Large diffs are not rendered by default.