diff --git a/.gitignore b/.gitignore index efe5175b..0365dcc7 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,5 @@ dist/ # JetBrains IDEs .idea/ + +.venv/ diff --git a/plugins/twitter/README.md b/plugins/twitter/README.md index c6de3689..e81182a0 100644 --- a/plugins/twitter/README.md +++ b/plugins/twitter/README.md @@ -1,99 +1,153 @@ # Twitter Plugin for GAME SDK -The Twitter plugin is a lightweight wrapper over commonly-used twitter API calls. It can be used as a executable on its own or by combining multiple of these into an executable. +The **Twitter Plugin** provides a lightweight interface for integrating Twitter (X) functionality into your GAME SDK agents. Built on top of [`virtuals_tweepy`](https://pypi.org/project/virtuals-tweepy/) by the Virtuals team — a maintained fork of [`Tweepy`](https://pypi.org/project/tweepy/)) — this plugin lets you easily post tweets, fetch data, and execute workflows through agent logic. + +Use it standalone or compose multiple Twitter actions as part of a larger agent job. + +--- ## Installation -From this directory (`twitter`), run the installation: +You can install the plugin using either `poetry` or `pip`: ```bash +# Using Poetry (from the plugin directory) poetry install ``` +or +```bash +# Using pip (recommended for integration projects) +pip install twitter_plugin_gamesdk +``` + +--- -## Usage +## Authentication Methods -The Twitter plugin can be initialized in one of two ways: +We support two primary ways to authenticate: -1. Using GAME's X enterprise API credentials (higher rate limits) +### 1. GAME's Sponsored X Enterprise Access Token (Recommended) - - To get the access token for this option, run the following command: +Virtuals sponsors the community with a **Twitter Enterprise API access plan**, using OAuth 2.0 with PKCE. This provides: - ```bash - poetry run twitter-plugin-gamesdk auth -k - ``` +- Higher rate limits: **35 calls / 5 minutes** +- Smoother onboarding +- Free usage via your `GAME_API_KEY` - You will see the following output: +#### a. Get Your Access Token - ```bash - Waiting for authentication... +Run the following command to authenticate using your `GAME_API_KEY`: - Visit the following URL to authenticate: - https://x.com/i/oauth2/authorize?response_type=code&client_id=VVdyZ0t4WFFRMjBlMzVaczZyMzU6MTpjaQ&redirect_uri=http%3A%2F%2Flocalhost%3A8714%2Fcallback&state=866c82c0-e3f6-444e-a2de-e58bcc95f08b&code_challenge=K47t-0Mcl8B99ufyqmwJYZFB56fiXiZf7f3euQ4H2_0&code_challenge_method=s256&scope=tweet.read%20tweet.write%20users.read%20offline.access - ``` +```bash +poetry run twitter-plugin-gamesdk auth -k +``` + +This will prompt: + +```bash +Waiting for authentication... + +Visit the following URL to authenticate: +https://x.com/i/oauth2/authorize?... + +Authenticated! Here's your access token: +apx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +#### b. Store Your Access Token + +We recommend storing environment variables in a `.env` file: + +``` +# .env + +GAME_TWITTER_ACCESS_TOKEN=apx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` - After authenticating, you will receive the following message: +Then, use `load_dotenv()` to load them: - ```bash - Authenticated! Here's your access token: - apx- - ``` +```python +import os +from dotenv import load_dotenv +from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin - - Set the access token as an environment variable called `GAME_TWITTER_ACCESS_TOKEN` (e.g. using a `.bashrc` or a `.zshrc` file). - - Import and initialize the plugin to use in your worker: +load_dotenv() - ```python - import os - from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin +options = { + "credentials": { + "game_twitter_access_token": os.environ.get("GAME_TWITTER_ACCESS_TOKEN") + } +} - # Define your options with the necessary credentials - options = { - "credentials": { - "gameTwitterAccessToken": os.environ.get("GAME_TWITTER_ACCESS_TOKEN") - }, - } - # Initialize the TwitterPlugin with your options - twitter_plugin = TwitterPlugin(options) +twitter_plugin = TwitterPlugin(options) +client = twitter_plugin.twitter_client - # Post a tweet - post_tweet_fn = twitter_plugin.get_function('post_tweet') - post_tweet_fn("Hello world!") - ``` +client.create_tweet(text="Tweeting with GAME Access Token!") +``` + +--- + +### 2. Use Your Own Twitter Developer Credentials + +Use this option if you need access to Twitter endpoints requiring a different auth level (e.g., **OAuth 1.0a User Context** or **OAuth 2.0 App Only**). + +> See [X API Auth Mapping](https://docs.x.com/resources/fundamentals/authentication/guides/v2-authentication-mapping) to determine which auth level is required for specific endpoints. + +#### a. Get Your Developer Credentials + +1. Sign in to the [Twitter Developer Portal](https://developer.x.com/en/portal/dashboard). +2. Create a project and app. +3. Generate the following keys and store them in your `.env` file: + +``` +# .env + +TWITTER_API_KEY=... +TWITTER_API_SECRET_KEY=... +TWITTER_ACCESS_TOKEN=... +TWITTER_ACCESS_TOKEN_SECRET=... +``` + +#### b. Initialize the Plugin + +```python +import os +from dotenv import load_dotenv +from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin + +load_dotenv() + +options = { + "credentials": { + "api_key": os.environ.get("TWITTER_API_KEY"), + "api_key_secret": os.environ.get("TWITTER_API_SECRET_KEY"), + "access_token": os.environ.get("TWITTER_ACCESS_TOKEN"), + "access_token_secret": os.environ.get("TWITTER_ACCESS_TOKEN_SECRET"), + } +} + +twitter_plugin = TwitterPlugin(options) +client = twitter_plugin.twitter_client + +client.create_tweet(text="Tweeting with personal developer credentials!") +``` -2. Using your own X API credentials +--- - - If you don't already have one, create a X (twitter) account and navigate to the [developer portal](https://developer.x.com/en/portal/dashboard). - - Create a project app, generate the following credentials and set them as environment variables (e.g. using a `.bashrc` or a `.zshrc` file): - - `TWITTER_BEARER_TOKEN` - - `TWITTER_API_KEY` - - `TWITTER_API_SECRET_KEY` - - `TWITTER_ACCESS_TOKEN` - - `TWITTER_ACCESS_TOKEN_SECRET` - - Import and initialize the plugin to use in your worker: +## Examples - ```python - import os - from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin +Explore the [`examples/`](./examples) directory for sample scripts demonstrating how to: - # Define your options with the necessary credentials - options = { - "credentials": { - "bearerToken": os.environ.get("TWITTER_BEARER_TOKEN"), - "apiKey": os.environ.get("TWITTER_API_KEY"), - "apiSecretKey": os.environ.get("TWITTER_API_SECRET_KEY"), - "accessToken": os.environ.get("TWITTER_ACCESS_TOKEN"), - "accessTokenSecret": os.environ.get("TWITTER_ACCESS_TOKEN_SECRET"), - }, - } - # Initialize the TwitterPlugin with your options - twitter_plugin = TwitterPlugin(options) +- Post tweets +- Reply to mentions +- Quote tweets +- Fetch user timelines +- And more! - # Post a tweet - post_tweet_fn = twitter_plugin.twitter_client.create_tweet - post_tweet_fn(text="Hello world! This is a test tweet from the Twitter Plugin!") - ``` +--- -For detailed documentation on each function's parameters and usage, please refer to the [Tweepy Client Documentation](https://docs.tweepy.org/en/stable/client.html). +## API Reference -Example usage: +This plugin wraps [`virtuals_tweepy`](https://pypi.org/project/virtuals-tweepy/), which is API-compatible with [Tweepy’s client interface](https://docs.tweepy.org/en/stable/client.html). Refer to their docs for supported methods and parameters. -You can refer to the example files in the `examples` directory for more examples on how to call the twitter functions. +--- diff --git a/plugins/twitter/examples/sample_media/media_file.png b/plugins/twitter/examples/sample_media/media_file.png deleted file mode 100644 index 45a4a22c..00000000 Binary files a/plugins/twitter/examples/sample_media/media_file.png and /dev/null differ diff --git a/plugins/twitter/examples/sample_media/virtuals-logo.png b/plugins/twitter/examples/sample_media/virtuals-logo.png new file mode 100644 index 00000000..fb5a25fa Binary files /dev/null and b/plugins/twitter/examples/sample_media/virtuals-logo.png differ diff --git a/plugins/twitter/examples/test_twitter.py b/plugins/twitter/examples/test_twitter.py index ef1f60d3..b5d85e96 100644 --- a/plugins/twitter/examples/test_twitter.py +++ b/plugins/twitter/examples/test_twitter.py @@ -1,83 +1,138 @@ +import os +import requests +from dotenv import load_dotenv from twitter_plugin_gamesdk.twitter_plugin import TwitterPlugin -# Define your options with the necessary credentials -# Using your own X API credentials -""" options = { - "credentials": { - "bearerToken": os.environ.get("TWITTER_BEARER_TOKEN"), - "apiKey": os.environ.get("TWITTER_API_KEY"), - "apiSecretKey": os.environ.get("TWITTER_API_SECRET_KEY"), - "accessToken": os.environ.get("TWITTER_ACCESS_TOKEN"), - "accessTokenSecret": os.environ.get("TWITTER_ACCESS_TOKEN_SECRET"), - }, -} """ - -# Using GAME Twitter API credentials -options = { - "credentials": { - "gameTwitterAccessToken": "apx-xxx", - }, -} - -# Initialize the TwitterPlugin with your options -twitter_plugin = TwitterPlugin(options) - -# Test case 1: Post a Tweet -print("\nRunning Test Case 1: Post a Tweet") -post_tweet_fn = twitter_plugin.twitter_client.create_tweet -post_tweet_fn(text="Hello world! This is a test tweet from the Twitter Plugin!") -print("Posted tweet!") - -# Test case 2: Post a Tweet with Media -print("\nRunning Test Case 2: Post a Tweet with Media") -print("\nUpload media") -with open("sample_media/media_file.png", "rb") as f: - media_id = twitter_plugin.twitter_client.upload_media(f) - print(f"Uploaded media_id: {media_id}") -post_tweet_fn = twitter_plugin.twitter_client.create_tweet -post_tweet_fn(text="Hello world! This is a test tweet with media from the Twitter Plugin!", media_ids=[media_id]) -print("Posted tweet with media!") - - -# Test case 3: Reply to a Tweet -print("\nRunning Test Case 3: Reply to a Tweet") -reply_tweet_fn = twitter_plugin.twitter_client.create_tweet -reply_tweet_fn(in_reply_to_tweet_id=1915274034100809968, text="Hey! This is a test reply!") -print("Replied to tweet!") - -# Test case 4: Like a Tweet -print("\nRunning Test Case 4: Like a Tweet") -like_tweet_fn = twitter_plugin.twitter_client.like -like_tweet_fn(tweet_id=1915274034100809968) -print("Liked tweet!") - -# Test case 5: Quote a Tweet -print("\nRunning Test Case 5: Quote a Tweet") -quote_tweet_fn = twitter_plugin.twitter_client.create_tweet -quote_tweet_fn(quote_tweet_id=1915274034100809968, text="Hey! This is a test quote tweet!") -print("Quoted tweet!") - -# Test case 6: Get Metrics -print("\nRunning Test Case 6: Get Metrics") -get_metrics_fn = twitter_plugin.twitter_client.get_me -metrics = get_metrics_fn(user_fields=["public_metrics"]) -print("Metrics:", metrics) - -# Test case 7: Get User From Handle -print("\nRunning Test Case 7: Get User From Handle") -get_user_fn = twitter_plugin.twitter_client.get_user -user = get_user_fn(username='celesteanglm', user_fields=["public_metrics"]) -print("user:", user) - -# Test case 8: Get User Mentions -print("\nRunning Test Case 8: Get User Mentions") -get_user_fn = twitter_plugin.twitter_client.get_user -user = get_user_fn(username="GAME_Virtuals") -get_user_mentions_fn = twitter_plugin.twitter_client.get_users_mentions -user_mentions = get_user_mentions_fn( id = user['data']['id'], - max_results = 10, - tweet_fields = ["id", "created_at", "text"], - expansions = ["attachments.media_keys"], - media_fields = ["url"]) -print("user_mentions:", user_mentions) +def run_twitter_actions(): + load_dotenv() + token = os.getenv("GAME_TWITTER_ACCESS_TOKEN") + if not token: + raise RuntimeError("Please set GAME_TWITTER_ACCESS_TOKEN in your .env") + # ——— Initialization options ——— + # # Option A: Using your own X API credentials (uncomment to use) + # options = { + # "credentials": { + # "api_key": os.environ.get("TWITTER_API_KEY"), + # "api_key_secret": os.environ.get("TWITTER_API_KEY_SECRET"), + # "access_token": os.environ.get("TWITTER_ACCESS_TOKEN"), + # "access_token_secret": os.environ.get("TWITTER_ACCESS_TOKEN_SECRET"), + # }, + # } + + # Option B: Using GAME Twitter API Access Token (Recommended, higher rate limits: 35 calls/5 minutes) + options = { + "credentials": { + "game_twitter_access_token": token + } + } + + twitter_plugin = TwitterPlugin(options) + client = twitter_plugin.twitter_client + + try: + # 1. Who am I? + me = client.get_me() + me_data = me["data"] + user_id = me_data["id"] + print(f"🙋 Logged in as: @{me_data['username']} ({me_data['name']})") + + # 2. Post a tweet + tweet = client.create_tweet(text="Hello Web3 🧵 #GameByVirtuals") + tweet_id = tweet["data"]["id"] + print(f"✅ Tweet posted: https://x.com/i/web/status/{tweet_id}") + + # 3. Like it + client.like(tweet_id=tweet_id) + print("❤️ Tweet liked!") + + # 4. Reply to it + reply = client.create_tweet( + text="Replying to my own tweet 😎", + in_reply_to_tweet_id=tweet_id + ) + print(f"💬 Replied: https://x.com/i/web/status/{reply['data']['id']}") + + # 5. Quote it + quote = client.create_tweet( + text="Excited to be testing the new Game Twitter Plugin!", + quote_tweet_id=tweet_id + ) + print(f"🔁 Quoted: https://x.com/i/web/status/{quote['data']['id']}") + + # 5. Upload local media and tweet + with open("sample_media/virtuals-logo.png", "rb") as img: + media_id = client.upload_media(media=img) + local = client.create_tweet( + text="Check this out! Uploaded with local media!", + media_ids=[media_id] + ) + print(f"🖼️ Local media tweet: https://x.com/i/web/status/{local['data']['id']}") + + # 7. Upload URL media and tweet + url = "https://assets.coingecko.com/coins/images/51063/large/Gaming_Agent_1fe70d54ba.jpg" + resp = requests.get(url) + resp.raise_for_status() + media_id_url = client.upload_media(media=resp.content) + url_tweet = client.create_tweet( + text="Check this out! Uploaded with URL media!", + media_ids=[media_id_url] + ) + print(f"🖼️ URL media tweet: https://x.com/i/web/status/{url_tweet['data']['id']}") + + # 8. Search tweets + search = client.search_recent_tweets(query="#GameByVirtuals", max_results=10) + hits = search.get("data", []) + print(f"🔍 Found {len(hits)} tweets for #GameByVirtuals:") + for i, t in enumerate(hits, 1): + print(f" {i}. https://x.com/i/web/status/{t['id']}") + + # 9. Mentions timeline + mentions = client.get_users_mentions(id=user_id, max_results=20) + mdata = mentions.get("data", []) + print(f"🔔 You have {len(mdata)} recent mentions:") + for i, t in enumerate(mdata, 1): + print(f" {i}. https://x.com/i/web/status/{t['id']}") + + # 10. Followers + followers = client.get_users_followers(id=user_id, max_results=20) + fdata = followers.get("data", []) + print(f"👥 You have {len(fdata)} followers:") + for i, u in enumerate(fdata, 1): + print(f" {i}. @{u['username']} ({u['name']})") + + # 11. Following + following = client.get_users_following(id=user_id, max_results=20) + gdata = following.get("data", []) + print(f"➡️ You are following {len(gdata)} users:") + for i, u in enumerate(gdata, 1): + print(f" {i}. @{u['username']} ({u['name']})") + + # 12. Get my public metrics + metrics = client.get_me(user_fields=["public_metrics"]) + print("📊 My metrics:", metrics["data"]["public_metrics"]) + + # 13. Read-only lookup of another user + other = client.get_user(username="GAME_Virtuals") + print("🔎 Lookup @GAME_Virtuals:", other["data"]) + + # 14. Get metrics for another handle + game_virtuals = client.get_user(username="GAME_Virtuals", user_fields=["public_metrics"]) + print("📊 @GAME_Virtuals metrics:", game_virtuals["data"]["public_metrics"]) + + # 15. Advanced mentions for GAME_Virtuals + adv_user = client.get_user(username="GAME_Virtuals") + adv_mentions = client.get_users_mentions( + id=adv_user["data"]["id"], + max_results=10, + tweet_fields=["id", "created_at", "text"], + expansions=["attachments.media_keys"], + media_fields=["url"] + ) + print("🔔 Advanced mentions for @GAME_Virtuals:", adv_mentions) + + except Exception as e: + print("❌ Error during Twitter actions:", e) + +if __name__ == "__main__": + run_twitter_actions() diff --git a/plugins/twitter/twitter_plugin_gamesdk/twitter_plugin.py b/plugins/twitter/twitter_plugin_gamesdk/twitter_plugin.py index dbeed65c..dd9ee357 100644 --- a/plugins/twitter/twitter_plugin_gamesdk/twitter_plugin.py +++ b/plugins/twitter/twitter_plugin_gamesdk/twitter_plugin.py @@ -26,9 +26,9 @@ ``` """ -import virtuals_tweepy import logging from typing import Dict, Any +from virtuals_tweepy import Client, TweepyException class TwitterPlugin: """ @@ -48,7 +48,7 @@ class TwitterPlugin: id (str): Plugin identifier name (str): Plugin name description (str): Plugin description - twitter_client (tweepy.Client): Authenticated Twitter API client + twitter_client (virtuals_tweepy.Client): Authenticated Twitter API client logger (logging.Logger): Plugin logger Raises: @@ -63,27 +63,26 @@ def __init__(self, options: Dict[str, Any]) -> None: raise ValueError("Twitter API credentials are required.") # Capture token for internal use - self.game_twitter_access_token = credentials.get("gameTwitterAccessToken") + self.game_twitter_access_token = credentials.get("game_twitter_access_token") # Auth gate: require EITHER gameTwitterAccessToken OR full credential set has_api_credentials = all( - credentials.get(key) for key in ["bearerToken", "apiKey", "apiSecretKey", "accessToken", "accessTokenSecret"] + credentials.get(key) for key in ["api_key", "api_key_secret", "access_token", "access_token_secret"] ) if not self.game_twitter_access_token and not has_api_credentials: raise ValueError( - "Missing valid authentication. Provide either a 'gameTwitterAccessToken' or all required Twitter API credentials." + "Missing valid authentication. Provide either a 'game_twitter_access_token' or all required Twitter API credentials." ) # Init Tweepy client - self.twitter_client: virtuals_tweepy.Client = virtuals_tweepy.Client( - bearer_token = credentials.get("bearerToken"), - consumer_key = credentials.get("apiKey"), - consumer_secret = credentials.get("apiSecretKey"), - access_token = credentials.get("accessToken"), - access_token_secret=credentials.get("accessTokenSecret"), + self.twitter_client: Client = Client( + consumer_key = credentials.get("api_key"), + consumer_secret = credentials.get("api_key_secret"), + access_token = credentials.get("access_token"), + access_token_secret=credentials.get("access_token_secret"), return_type = dict, - game_twitter_access_token = credentials.get("gameTwitterAccessToken"), + game_twitter_access_token = credentials.get("game_twitter_access_token"), ) # Configure logging logging.basicConfig(level=logging.INFO) @@ -98,6 +97,6 @@ def _check_authentication(self) -> None: try: user = self.twitter_client.get_me(user_fields=["public_metrics"]).get('data') self.logger.info(f"Authenticated as: {user.get('name')} (@{user.get('username')})") - except virtuals_tweepy.TweepyException as e: + except TweepyException as e: self.logger.error(f"Authentication failed: {e}") raise ValueError("Invalid Twitter credentials or failed authentication.")