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

Twitter tool #525

Merged
merged 33 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
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
23 changes: 21 additions & 2 deletions gui/pages/Content/Toolkits/ToolkitWorkspace.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, {useEffect, useState} from 'react';
import Image from 'next/image';
import {ToastContainer, toast} from 'react-toastify';
import {updateToolConfig, getToolConfig, authenticateGoogleCred} from "@/pages/api/DashboardService";
import {updateToolConfig, getToolConfig, authenticateGoogleCred, authenticateTwitterCred} from "@/pages/api/DashboardService";
import styles from './Tool.module.css';
import {EventBus} from "@/utils/eventBus";

Expand All @@ -25,6 +25,13 @@ export default function ToolkitWorkspace({toolkitDetails}){
window.location.href = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${client_id}&redirect_uri=${redirect_uri}&access_type=offline&response_type=code&scope=${scope}`;
}

function getTwitterToken(oauth_data){
const oauth_token = oauth_data.oauth_token
const oauth_token_secret = oauth_data.oauth_token_secret
const authUrl = `https://api.twitter.com/oauth/authenticate?oauth_token=${oauth_token}`
window.location.href = authUrl
}

useEffect(() => {
if(toolkitDetails !== null) {
if (toolkitDetails.tools) {
Expand All @@ -36,7 +43,7 @@ export default function ToolkitWorkspace({toolkitDetails}){
const apiConfigs = response.data || [];
setApiConfigs(apiConfigs);
})
.catch((error) => {
.catch((errPor) => {
console.log('Error fetching API data:', error);
})
.finally(() => {
Expand Down Expand Up @@ -71,6 +78,17 @@ export default function ToolkitWorkspace({toolkitDetails}){
});
};

const handleTwitterAuthClick = async () => {
authenticateTwitterCred(toolkitDetails.id)
.then((response) => {
getTwitterToken(response.data);
localStorage.setItem("twitter_toolkit_id", toolkitDetails.id)
})
.catch((error) => {
console.error('Error fetching data: ', error);
});
};

return (<>
<div className={styles.tools_container}>
<div style={{display: 'flex',justifyContent:'flex-start',marginBottom:'20px', width:'600px'}}>
Expand Down Expand Up @@ -116,6 +134,7 @@ export default function ToolkitWorkspace({toolkitDetails}){
<div style={{ marginLeft: 'auto', display: 'flex', justifyContent:'space-between'}}>
<div>
{toolkitDetails.name === 'Google Calendar Toolkit' && <button style={{width:'200px'}} className={styles.primary_button} onClick={handleAuthenticateClick}>Authenticate Tool</button>}
{toolkitDetails.name === 'Twitter Toolkit' && <button style={{width:'200px'}} className={styles.primary_button} onClick={handleTwitterAuthClick}>Authenticate Tool</button>}
</div>
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button className={styles.primary_button} onClick={handleUpdateChanges} >Update Changes</button>
Expand Down
22 changes: 21 additions & 1 deletion gui/pages/Dashboard/Content.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ import Settings from "./Settings/Settings";
import styles from './Dashboard.module.css';
import Image from "next/image";
import { EventBus } from "@/utils/eventBus";
import {getAgents, getToolKit, getLastActiveAgent} from "@/pages/api/DashboardService";
import {getAgents, getToolKit, getLastActiveAgent, sendTwitterCreds} from "@/pages/api/DashboardService";
import Market from "../Content/Marketplace/Market";
import AgentTemplatesList from '../Content/Agents/AgentTemplatesList';
import { useRouter } from 'next/router';
import querystring from 'querystring';
import { userInfo } from 'os';
import { parse } from 'path';

export default function Content({env, selectedView, selectedProjectId, organisationId}) {
const [tabs, setTabs] = useState([]);
Expand All @@ -19,6 +23,7 @@ export default function Content({env, selectedView, selectedProjectId, organisat
const [toolkits, setToolkits] = useState(null);
const tabContainerRef = useRef(null);
const [toolkitDetails, setToolkitDetails] = useState({})
const router = useRouter();

function fetchAgents() {
getAgents(selectedProjectId)
Expand Down Expand Up @@ -124,6 +129,21 @@ export default function Content({env, selectedView, selectedProjectId, organisat
}
}
}
const queryParams = router.asPath.split('?')[1];
const parsedParams = querystring.parse(queryParams);
parsedParams["toolkit_id"] = toolkitDetails.toolkit_id;
if (window.location.href.indexOf("twitter_creds") > -1){
const toolkit_id = localStorage.getItem("twitter_toolkit_id") || null;
parsedParams["toolkit_id"] = toolkit_id;
const params = JSON.stringify(parsedParams)
sendTwitterCreds(params)
.then((response) => {
console.log("Authentication completed successfully");
})
.catch((error) => {
console.error("Error fetching data: ",error);
})
};
}, [selectedTab]);

useEffect(() => {
Expand Down
8 changes: 8 additions & 0 deletions gui/pages/api/DashboardService.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ export const authenticateGoogleCred = (toolKitId) => {
return api.get(`/google/get_google_creds/toolkit_id/${toolKitId}`);
}

export const authenticateTwitterCred = (toolKitId) => {
return api.get(`/twitter/get_twitter_creds/toolkit_id/${toolKitId}`);
}

export const sendTwitterCreds = (twitter_creds) => {
return api.post(`/twitter/send_twitter_creds/${twitter_creds}`);
}

export const fetchToolTemplateList = () => {
return api.get(`/toolkits/get/list?page=0`);
}
Expand Down
55 changes: 54 additions & 1 deletion main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@
from sqlalchemy.orm import sessionmaker

import superagi
import urllib.parse
import json
import http.client as http_client
from superagi.helper.twitter_tokens import TwitterTokens
from datetime import datetime, timedelta
from superagi.agent.agent_prompt_builder import AgentPromptBuilder
from superagi.config.config import get_config
from superagi.controllers.agent import router as agent_router
Expand All @@ -40,7 +45,7 @@
from superagi.models.agent_workflow_step import AgentWorkflowStep
from superagi.models.organisation import Organisation
from superagi.models.tool_config import ToolConfig
from superagi.models.toolkit import Toolkit
from superagi.models.oauth_tokens import OauthTokens
from superagi.models.types.login_request import LoginRequest
from superagi.models.user import User

Expand Down Expand Up @@ -320,6 +325,16 @@ async def google_auth_calendar(code: str = Query(...), Authorize: AuthJWT = Depe
frontend_url = superagi.config.config.get_config("FRONTEND_URL", "http://localhost:3000")
return RedirectResponse(frontend_url)

@app.get('/oauth-twitter')
async def twitter_oauth(oauth_token: str = Query(...),oauth_verifier: str = Query(...), Authorize: AuthJWT = Depends()):
token_uri = f'https://api.twitter.com/oauth/access_token?oauth_verifier={oauth_verifier}&oauth_token={oauth_token}'
conn = http_client.HTTPSConnection("api.twitter.com")
conn.request("POST", token_uri, "")
res = conn.getresponse()
response_data = res.read().decode('utf-8')
frontend_url = superagi.config.config.get_config("FRONTEND_URL", "http://localhost:3000")
redirect_url_success = f"{frontend_url}/twitter_creds/?{response_data}"
return RedirectResponse(url=redirect_url_success)

@app.get('/github-login')
def github_login():
Expand Down Expand Up @@ -411,6 +426,44 @@ def get_google_calendar_tool_configs(toolkit_id: int):
"client_id": google_calendar_config.value
}

@app.get("/twitter/get_twitter_creds/toolkit_id/{toolkit_id}")
def get_twitter_tool_configs(toolkit_id: int):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you move this to twitter_auth.rb in controller.

twitter_config_key = db.session.query(ToolConfig).filter(ToolConfig.toolkit_id == toolkit_id,ToolConfig.key == "TWITTER_API_KEY").first()
twitter_config_secret = db.session.query(ToolConfig).filter(ToolConfig.toolkit_id == toolkit_id,ToolConfig.key == "TWITTER_API_SECRET").first()
api_data = {
"api_key": twitter_config_key.value,
"api_secret": twitter_config_secret.value
}
response = TwitterTokens().get_request_token(api_data)
return response

@app.post("/twitter/send_twitter_creds/{twitter_creds}")
def send_twitter_tool_configs(twitter_creds: str, Authorize: AuthJWT = Depends()):
Session = sessionmaker(bind=engine)
session = Session()
try:
Authorize.jwt_required()
current_user_email = Authorize.get_jwt_subject()
current_user = session.query(User).filter(User.email == current_user_email).first()
except:
current_user = session.query(User).filter(User.email == "[email protected]").first()
user_id = current_user.id
credentials = json.loads(twitter_creds)
credentials["user_id"] = user_id
api_key = session.query(ToolConfig).filter(ToolConfig.key == "TWITTER_API_KEY", ToolConfig.toolkit_id == credentials["toolkit_id"]).first()
api_key_secret = session.query(ToolConfig).filter(ToolConfig.key == "TWITTER_API_SECRET", ToolConfig.toolkit_id == credentials["toolkit_id"]).first()
final_creds = {
"api_key": api_key.value,
"api_key_secret": api_key_secret.value,
"oauth_token": credentials["oauth_token"],
"oauth_token_secret": credentials["oauth_token_secret"]
}
tokens = OauthTokens.add_or_update(session,credentials["toolkit_id"], current_user.id, "TWITTER_OAUTH_TOKENS", str(final_creds))
if tokens:
success = True
else:
success = False
return success

@app.get("/validate-open-ai-key/{open_ai_key}")
async def root(open_ai_key: str, Authorize: AuthJWT = Depends()):
Expand Down
47 changes: 47 additions & 0 deletions migrations/versions/cc1dcc508611_table_for_oauth_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""Table for oauth tokens

Revision ID: cc1dcc508611
Revises: 7a3e336c0fba
Create Date: 2023-06-29 07:57:22.861763

"""
from alembic import op
import sqlalchemy as sa


# revision identifiers, used by Alembic.
revision = 'cc1dcc508611'
down_revision = '7a3e336c0fba'
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
TransformerOptimus marked this conversation as resolved.
Show resolved Hide resolved
op.create_table('oauth_tokens',
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('toolkit_id', sa.Integer(), nullable=True),
sa.Column('key', sa.String(), nullable=True),
sa.Column('value', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.drop_index('ix_agent_execution_permissions_agent_execution_id', table_name='agent_execution_permissions')
op.drop_index('ix_atc_agnt_template_id_key', table_name='agent_template_configs')
op.drop_index('ix_agt_agnt_name', table_name='agent_templates')
op.drop_index('ix_agt_agnt_organisation_id', table_name='agent_templates')
op.drop_index('ix_agt_agnt_workflow_id', table_name='agent_templates')
# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_index('ix_agt_agnt_workflow_id', 'agent_templates', ['agent_workflow_id'], unique=False)
op.create_index('ix_agt_agnt_organisation_id', 'agent_templates', ['organisation_id'], unique=False)
op.create_index('ix_agt_agnt_name', 'agent_templates', ['name'], unique=False)
op.create_index('ix_atc_agnt_template_id_key', 'agent_template_configs', ['agent_template_id', 'key'], unique=False)
op.create_index('ix_agent_execution_permissions_agent_execution_id', 'agent_execution_permissions', ['agent_execution_id'], unique=False)
op.drop_table('oauth_tokens')
# ### end Alembic commands ###
42 changes: 42 additions & 0 deletions superagi/helper/twitter_helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import os
import json
import base64
import requests
from requests_oauthlib import OAuth1
from requests_oauthlib import OAuth1Session
from superagi.helper.resource_helper import ResourceHelper

class TwitterHelper:

def get_media_ids(self, media_files, creds, agent_id):
media_ids = []
oauth = OAuth1(creds.api_key,
client_secret=creds.api_key_secret,
resource_owner_key=creds.oauth_token,
resource_owner_secret=creds.oauth_token_secret)
for file in media_files:
file_path = self.get_file_path(file, agent_id)
image_data = open(file_path, 'rb').read()
b64_image = base64.b64encode(image_data)
upload_endpoint = 'https://upload.twitter.com/1.1/media/upload.json'
headers = {'Authorization': 'application/octet-stream'}
response = requests.post(upload_endpoint, headers=headers,
data={'media_data': b64_image},
auth=oauth)
ids = json.loads(response.text)['media_id']
media_ids.append(str(ids))
return media_ids

def get_file_path(self, file_name, agent_id):
final_path = ResourceHelper().get_agent_resource_path(file_name, agent_id)
TransformerOptimus marked this conversation as resolved.
Show resolved Hide resolved
return final_path

def send_tweets(self, params, creds):
tweet_endpoint = "https://api.twitter.com/2/tweets"
oauth = OAuth1Session(creds.api_key,
client_secret=creds.api_key_secret,
resource_owner_key=creds.oauth_token,
resource_owner_secret=creds.oauth_token_secret)

response = oauth.post(tweet_endpoint,json=params)
return response
97 changes: 97 additions & 0 deletions superagi/helper/twitter_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import os
import pickle
import json
import hmac
import time
import random
import base64
import hashlib
import urllib.parse
import http.client as http_client
from superagi.config.config import get_config
from sqlalchemy.orm import Session
from superagi.models.tool_config import ToolConfig
from superagi.resource_manager.manager import ResourceManager

class Creds:

def __init__(self,api_key, api_key_secret, oauth_token, oauth_token_secret):
self.api_key = api_key
self.api_key_secret = api_key_secret
self.oauth_token = oauth_token
self.oauth_token_secret = oauth_token_secret

class TwitterTokens:
TransformerOptimus marked this conversation as resolved.
Show resolved Hide resolved
session: Session

def get_request_token(self,api_data):
api_key = api_data["api_key"]
api_secret_key = api_data["api_secret"]
http_method = 'POST'
base_url = 'https://api.twitter.com/oauth/request_token'

params = {
'oauth_callback': 'http://localhost:3000/api/oauth-twitter',
'oauth_consumer_key': api_key,
'oauth_nonce': self.gen_nonce(),
'oauth_signature_method': 'HMAC-SHA1',
'oauth_timestamp': int(time.time()),
'oauth_version': '1.0'
}

params_sorted = sorted(params.items())
params_qs = '&'.join([f'{k}={self.percent_encode(str(v))}' for k, v in params_sorted])

base_string = f'{http_method}&{self.percent_encode(base_url)}&{self.percent_encode(params_qs)}'

signing_key = f'{self.percent_encode(api_secret_key)}&'
signature = hmac.new(signing_key.encode(), base_string.encode(), hashlib.sha1)
params['oauth_signature'] = base64.b64encode(signature.digest()).decode()

auth_header = 'OAuth ' + ', '.join([f'{k}="{self.percent_encode(str(v))}"' for k, v in params.items()])

headers = {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': auth_header
}
conn = http_client.HTTPSConnection("api.twitter.com")
conn.request("POST", "/oauth/request_token", "", headers)
res = conn.getresponse()
response_data = res.read().decode('utf-8')
conn.close()
request_token_resp = dict(urllib.parse.parse_qsl(response_data))
return request_token_resp

def percent_encode(self, val):
return urllib.parse.quote(val, safe='')

def gen_nonce(self):
nonce = ''.join([str(random.randint(0, 9)) for i in range(32)])
return nonce

def get_twitter_creds(self, toolkit_id):
file_name = "twitter_credentials.pickle"
root_dir = get_config('RESOURCES_OUTPUT_ROOT_DIR')
file_path = file_name
if root_dir is not None:
root_dir = root_dir if root_dir.startswith("/") else os.getcwd() + "/" + root_dir
root_dir = root_dir if root_dir.endswith("/") else root_dir + "/"
file_path = root_dir + file_name
else:
file_path = os.getcwd() + "/" + file_name
if os.path.exists(file_path):
with open(file_path,'rb') as file:
creds = pickle.load(file)
if isinstance(creds, str):
creds = json.loads(creds)
twitter_creds = self.session.query(ToolConfig).filter(ToolConfig.toolkit_id == toolkit_id).all()
api_key = ""
api_key_secret = ""
for credentials in twitter_creds:
credentials = credentials.__dict__
if credentials["key"] == "TWITTER_API_KEY":
api_key = credentials["value"]
if credentials["key"] == "TWITTER_API_SECRET":
api_key_secret = credentials["value"]
final_creds = Creds(api_key, api_key_secret, creds["oauth_token"], creds["oauth_token_secret"])
return final_creds
Loading