diff --git a/plugins/twitterPrimusPlugin/README.md b/plugins/twitterPrimusPlugin/README.md new file mode 100644 index 00000000..9a82e734 --- /dev/null +++ b/plugins/twitterPrimusPlugin/README.md @@ -0,0 +1,152 @@ +# @virtuals-protocol/game-twitter-primus-plugin + +A plugin to fully verify agent activities, including actions and other behaviors that use HTTPS. + +## Overview +This plugin verifies the validity of network requests and responses using the [Primus zk-tls SDK](https://docs.primuslabs.xyz/data-verification/core-sdk/overview). It generates and verifies a zkTLS proof based on the zkTLS protocol. + +## Usage + +Here is full code of PrimusClient. +```typescript + +export class PrimusClient { + private zkTLS: PrimusCoreTLS = new PrimusCoreTLS(); + async init(appId: string, appSecret: string) { + await this.zkTLS.init(appId, appSecret); + console.log('init zkTLS success') + } + + + generateProof = async ( + endpoint: string, + method: string, + headers: Record, + responseParsePath: string, + body?: string, + ): Promise => { + const requestParam = body + ? { + url: endpoint, + method: method, + header: headers, + body: body, + } + : { + url: endpoint, + method: method, + header: headers, + }; + // console.log('requestParam:',requestParam) + const attestationParams = this.zkTLS.generateRequestParams(requestParam, [ + { + keyName: "content", + parsePath: responseParsePath, + parseType: "string", + }, + ]); + attestationParams.setAttMode({ + algorithmType: "proxytls", + }); + return await this.zkTLS.startAttestation(attestationParams); + }; + + verifyProof = async (attestation: Attestation): Promise => { + return this.zkTLS.verifyAttestation(attestation); + }; +} +``` + +The core functions in `PrimusClient` are the following, which are also used in `GameFunction`. +```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, + //A [JSONPath](https://datatracker.ietf.org/doc/rfc9535/) expression to locate the specific field in the response you want to attest. + responseParsePath: string, + // The body of the request. It should be a string. + body?: string + +): Promise + +// Verify the proof. +verifyProof = async (attestation: any): Promise + +``` + +### Verify the Actions + +Below is an example showcasing how to post a price from Binance to Twitter. Developers can easily adapt this process for other functions. +```typescript +//............. +executable: async (args, logger) => { + try { + // + logger("Getting btc price with zktls..."); + // Get price of BTC with primus client + const btcPriceStr = await this.getLatestBTCPriceFromBinance(logger) + const priceInfo = JSON.parse(btcPriceStr.feedback) + // Post tweet with primus client + logger(`Posting tweet with price: ${priceInfo.price}`); + const rsp = await this.twitterScraper.sendTweet(priceInfo.price,logger); + return new ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Done, + JSON.stringify({ + rsp: "Tweet posted", + attestation: rsp.attestation + }) + ); + } catch (e) { + console.log(e) + return new ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Failed, + "Failed to post tweet" + ); + } +} +//......... +``` + +## Installation + +```bash +pnpm add @virtuals-protocol/game-twitter-primus-plugin +``` + +## Configuration + +This is the configuration class, and you must provide all required parameters in it. + +``` +interface ITwitterPrimusPluginOptions { + // Parameter for worker + id: string; + name: string; + description: string; + + // Parameter for PrimusClient + appId: string; + appSecret: string; + + // Parameter for twitter client + username: string; + password: string; + email: string; + twitter2faSecret: string; +} +``` + +***How to get appId and appSecret?*** + +1. Visit the [Primus Developer Hub](https://dev.primuslabs.xyz/). +2. Create a new `Backend` project +3. Save your 'Application ID(appId)' and 'Secret Key(appSecret)' + + +Here is a demo to show how to run this plugin +[Example](./example/README.md) diff --git a/plugins/twitterPrimusPlugin/example/.env.example b/plugins/twitterPrimusPlugin/example/.env.example new file mode 100644 index 00000000..7ef26cf0 --- /dev/null +++ b/plugins/twitterPrimusPlugin/example/.env.example @@ -0,0 +1,16 @@ +GAME_API_KEY= + +# Worker information +WORKER_ID= +WORKER_NAME= +WORKER_DESC= + +# Primus SDK +APP_ID= +APP_SECRET= + +# Twitter account +TWITTER_USER_NAME= +TWITTER_PASSWORD= +TWITTER_EMAIL= +TWITTER_2FA_SECRET=#NOT NECESSARY, Only need for twitter 2FA \ No newline at end of file diff --git a/plugins/twitterPrimusPlugin/example/README.md b/plugins/twitterPrimusPlugin/example/README.md new file mode 100644 index 00000000..5d536d5e --- /dev/null +++ b/plugins/twitterPrimusPlugin/example/README.md @@ -0,0 +1,51 @@ +# twitterPrimusPlugin Demo +## Overview +This is a demo project that demonstrates how to use the `twitterPrimusPlugin` to generate a zkTlS attestation for you function(action). + +## How to run + +### Install +#### Install dependencies in twitterPrimusPlugin +```shell +npm install +``` +#### Install dependencies in example +run `npm install` to install dependencies. +```shell +cd example +npm install +``` + +### Configuration +1. Create a `.env` file in the root directory using the `.env.example` as template. +```shell +cp .env.example .env +``` + +2. Set the environment variables in the `.env` file. +```dotenv +# Get from https://console.game.virtuals.io/ +GAME_API_KEY= + +# Worker information +WORKER_ID= +WORKER_NAME= +WORKER_DESC= + +# Primus SDK +# APP_ID and APP_SECRET get from https://dev.primuslabs.xyz/myDevelopment/myProjects . Create a new Backend project and save your 'Application ID(APP_ID)' and 'Secret Key(APP_SECRET)' +# Docs for Primus SDK : https://docs.primuslabs.xyz/data-verification/core-sdk/overview +APP_ID= +APP_SECRET= + +# Twitter account +TWITTER_USER_NAME= +TWITTER_PASSWORD= +TWITTER_EMAIL= +TWITTER_2FA_SECRET=#NOT NECESSARY, Only need for twitter 2FA +``` + +### Run +```shell +npm run start +``` \ No newline at end of file diff --git a/plugins/twitterPrimusPlugin/example/package.json b/plugins/twitterPrimusPlugin/example/package.json new file mode 100644 index 00000000..66da30be --- /dev/null +++ b/plugins/twitterPrimusPlugin/example/package.json @@ -0,0 +1,14 @@ +{ + "name": "example", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "start": "ts-node src/index.ts" + }, + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "dotenv": "^16.4.7" + } +} diff --git a/plugins/twitterPrimusPlugin/example/src/index.ts b/plugins/twitterPrimusPlugin/example/src/index.ts new file mode 100644 index 00000000..78f97f13 --- /dev/null +++ b/plugins/twitterPrimusPlugin/example/src/index.ts @@ -0,0 +1,64 @@ +import {GameAgent, LLMModel} from "@virtuals-protocol/game"; +import TwitterPrimusPlugin from "../../src"; + +import dotenv from "dotenv"; + +dotenv.config(); + +(async () => { + // Create a worker with the functions + const twitterPlugin = new TwitterPrimusPlugin(); + // Check env has set + if (!process.env.APP_ID || !process.env.APP_SECRET || !process.env.TWITTER_USER_NAME || !process.env.TWITTER_PASSWORD || !process.env.TWITTER_EMAIL) { + throw new Error("Missing environment variables"); + } + await twitterPlugin.init({ + id: process.env.WORKER_ID || "", + name: process.env.WORKER_NAME || "", + description: process.env.WORKER_DESC || "", + + appId: process.env.APP_ID || "", + appSecret: process.env.APP_SECRET || "", + + username: process.env.TWITTER_USER_NAME || "", + password: process.env.TWITTER_PASSWORD || "", + email: process.env.TWITTER_EMAIL || "", + //NOT NECESSARY + twitter2faSecret: process.env.TWITTER_2FA_SECRET || "", + }); + + const gameApiKey = process.env.GAME_API_KEY; + + if(!gameApiKey){ + throw new Error("Missing environment variables"); + } + // Create an agent with the worker + const agent = new GameAgent(gameApiKey, { + name: "Twitter Primus Bot", + goal: "Verify actions", + description: "Get btc price and post tweet by zktls", + workers: [ + twitterPlugin.getWorker({}), + ], + llmModel: LLMModel.DeepSeek_R1, + getAgentState: async () => { + return { + username: "twitter_primus_bot" + }; + }, + }); + + agent.setLogger((agent, message) => { + console.log(`-----[${agent.name}]-----`); + console.log(message); + console.log("\n"); + }); + + await agent.init(); + + while (true) { + await agent.step({ + verbose: true, + }); + } +})(); diff --git a/plugins/twitterPrimusPlugin/package.json b/plugins/twitterPrimusPlugin/package.json new file mode 100644 index 00000000..a17fefbd --- /dev/null +++ b/plugins/twitterPrimusPlugin/package.json @@ -0,0 +1,16 @@ +{ + "name": "@virtuals-protocol/game-twitter-primus-plugin", + "version": "1.0.0", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "dependencies": { + "@virtuals-protocol/game": "^0.1.7", + "@primuslabs/zktls-core-sdk": "^0.1.1", + "agent-twitter-client": "0.0.18" + }, + "author": "dev@primuslabs.xyz", + "license": "ISC", + "description": "" +} diff --git a/plugins/twitterPrimusPlugin/src/index.ts b/plugins/twitterPrimusPlugin/src/index.ts new file mode 100644 index 00000000..4d10115a --- /dev/null +++ b/plugins/twitterPrimusPlugin/src/index.ts @@ -0,0 +1,142 @@ +import { + GameWorker, + GameFunction, + ExecutableGameFunctionResponse, + ExecutableGameFunctionStatus, +} from "@virtuals-protocol/game"; +import {PrimusClient} from "./util/primusClient"; +import {TwitterScraper} from "./util/twitterScraper"; + +interface ITwitterPrimusPluginOptions { + // Parameter for worker + id: string; + name: string; + description: string; + + // Parameter for PrimusClient + appId: string; + appSecret: string; + + // Parameter for twitter client + username: string; + password: string; + email: string; + twitter2faSecret: string; +} + +class TwitterPrimusPlugin { + private id!: string ; + private name!: string; + private description!: string; + + private twitterScraper!: TwitterScraper; + private primusClient!: PrimusClient; + + + constructor() { + } + + public async init(options: ITwitterPrimusPluginOptions) { + // Init primus client + this.id = options.id || "twitter_primus_worker"; + this.name = options.name || "twitter_primus_worker"; + this.description = options.description || "A worker that all behaviors are verifiable with primus zktls"; + this.primusClient = new PrimusClient(); + if (!options.appId || !options.appSecret) { + return new Error("appId and appSecret are required"); + } + await this.primusClient.init(options.appId, options.appSecret) + this.twitterScraper = new TwitterScraper(this.primusClient); + await this.twitterScraper.login(options.username, options.password, options.email, options.twitter2faSecret); + } + + public getWorker(data?: { + functions?: GameFunction[]; + getEnvironment?: () => Promise>; + }): GameWorker { + if(!this.primusClient||!this.twitterScraper){ + throw new Error("Primus client is not initialized"); + } + return new GameWorker({ + id: this.id, + name: this.name, + description: this.description, + functions: data?.functions || [ + this.postTweetFunction + ], + getEnvironment: data?.getEnvironment || this.getMetrics.bind(this), + }); + } + + public async getMetrics() { + return { + status: "success" + }; + } + + + get postTweetFunction() { + return new GameFunction({ + name: "post_tweet", + description: "Post a tweet with BTC price", + args: [] as const, + executable: async (args, logger) => { + try { + // + logger("Getting btc price with zktls..."); + // Get price of BTC with primus client + const btcPriceStr = await this.getLatestBTCPriceFromBinance(logger) + const priceInfo = JSON.parse(btcPriceStr.feedback) + // Post tweet with primus client + logger(`Posting tweet with price: ${priceInfo.price}`); + const rsp = await this.twitterScraper.sendTweet(priceInfo.price,logger); + return new ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Done, + JSON.stringify({ + rsp: "Tweet posted", + attestation: rsp.attestation + }) + ); + } catch (e) { + console.log(e) + return new ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Failed, + "Failed to post tweet" + ); + } + }, + }); + } + + + private async getLatestBTCPriceFromBinance(logger: any) { + //get btc price + const url = `https://api.binance.com/api/v3/ticker/price?symbol=BTCUSDT`; + const method = 'GET'; + const headers = { + 'Accept ': '*/*', + }; + const attestation = await this.primusClient.generateProof(url, method, headers,"$.price"); + const valid = await this.primusClient.verifyProof(attestation); + if (!valid) { + throw new Error("Invalid price attestation"); + } + logger(`price attestation:${JSON.stringify(attestation)}`); + try { + const responseData = JSON.parse((attestation as any).data); + const price = responseData.content; + return new ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Done, + JSON.stringify({ + price: price, + attestation: attestation + }) + ); + } catch (error) { + console.log(error) + throw new Error('Failed to parse price data'); + } + } +} + +export default TwitterPrimusPlugin; diff --git a/plugins/twitterPrimusPlugin/src/util/primusClient.ts b/plugins/twitterPrimusPlugin/src/util/primusClient.ts new file mode 100644 index 00000000..b889b7b8 --- /dev/null +++ b/plugins/twitterPrimusPlugin/src/util/primusClient.ts @@ -0,0 +1,57 @@ +import {PrimusCoreTLS, Attestation} from "@primuslabs/zktls-core-sdk"; + + +export class PrimusClient { + private zkTLS: PrimusCoreTLS = new PrimusCoreTLS(); + async init(appId: string, appSecret: string) { + await this.zkTLS.init(appId, appSecret); + console.log('init zkTLS success') + } + + /** + * + * @param endpoint + * @param method + * @param headers + * @param responseParsePath Data you want to get in response + * @param body + */ + + generateProof = async ( + endpoint: string, + method: string, + headers: Record, + responseParsePath: string, + body?: string, + ): Promise => { + const requestParam = body + ? { + url: endpoint, + method: method, + header: headers, + body: body, + } + : { + url: endpoint, + method: method, + header: headers, + }; + // console.log('requestParam:',requestParam) + const attestationParams = this.zkTLS.generateRequestParams(requestParam, [ + { + keyName: "content", + parsePath: responseParsePath, + parseType: "string", + }, + ]); + attestationParams.setAttMode({ + algorithmType: "proxytls", + }); + return await this.zkTLS.startAttestation(attestationParams); + }; + + verifyProof = async (attestation: Attestation): Promise => { + return this.zkTLS.verifyAttestation(attestation); + }; +} + diff --git a/plugins/twitterPrimusPlugin/src/util/twitterScraper.ts b/plugins/twitterPrimusPlugin/src/util/twitterScraper.ts new file mode 100644 index 00000000..d376636e --- /dev/null +++ b/plugins/twitterPrimusPlugin/src/util/twitterScraper.ts @@ -0,0 +1,240 @@ +import { Scraper } from "agent-twitter-client"; +import {PrimusClient} from "./primusClient"; + +export class TwitterScraper { + private scraper!: Scraper; + private primusClient: PrimusClient; + + constructor(primusClient: PrimusClient) { + this.primusClient = primusClient; + } + + public getScraper(): Scraper { + return this.scraper; + } + + public async getUserIdByScreenName(screenName: string) { + return await this.scraper.getUserIdByScreenName(screenName); + } + + public async login(username: string, password: string, email: string, twitter2faSecret?: string) { + this.scraper = new Scraper(); + if (!username || !password) { + throw new Error( + "Twitter credentials not configured in environment" + ); + } + + // Login with credentials + await this.scraper.login(username, password, email, twitter2faSecret); + if (!(await this.scraper.isLoggedIn())) { + console.error("Login failed"); + return false; + } + console.log("Twitter login successful") + } + + 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: { key: string; }) => 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 this.primusClient.generateProof( + endpoint, + "GET", + headers, + responseParsePath + ); + //log attestation + console.log( + "Tweet getting proof generated successfully:", + attestation + ); + const verifyResult = this.primusClient.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 + console.log(`get tweet content success:${content}`); + return content; + } + + public async sendTweet(content: string,logger: any) { + 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: { key: string; }) => 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 this.primusClient.generateProof(endpoint,method,headers,"$.data.create_tweet.tweet_results.result.rest_id",bodyStr); + + logger( + `Tweet posting proof generated successfully:${JSON.stringify(attestation)}` + ); + + const verifyResult = await this.primusClient.verifyProof(attestation); + if (!verifyResult) { + throw new Error( + "Verify attestation failed, data from source is illegality" + ); + } + const responseData = JSON.parse(attestation.data); + console.log(`send tweet success,tweetId:${responseData.content}`); + + return { + content:responseData.content, + attestation: attestation + }; + } +}