From 3c27dc1643332fdb8ca7b7b0b92417ddf0d7b481 Mon Sep 17 00:00:00 2001 From: Iris Date: Mon, 13 May 2024 16:04:27 +0200 Subject: [PATCH 1/6] feat: add social networks text records --- src/config.rs | 12 ++ src/endpoints/crosschain/ethereum/mod.rs | 1 + src/endpoints/crosschain/ethereum/resolve.rs | 41 +++++- .../crosschain/ethereum/text_records.rs | 134 ++++++++++++++++++ 4 files changed, 185 insertions(+), 3 deletions(-) create mode 100644 src/endpoints/crosschain/ethereum/text_records.rs diff --git a/src/config.rs b/src/config.rs index 54cab54..d966a61 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,6 +6,8 @@ use std::collections::HashMap; use std::env; use std::fs; +use crate::endpoints::crosschain::ethereum::text_records::HandlerType; + macro_rules! pub_struct { ($($derive:path),*; $name:ident {$($field:ident: $t:ty),* $(,)?}) => { #[derive($($derive),*)] @@ -73,6 +75,7 @@ pub_struct!(Clone, Debug, Deserialize; Variables { rpc_url: String, refresh_delay: f64, ipfs_gateway: String, + discord_token: String, }); #[derive(Deserialize)] @@ -94,6 +97,12 @@ pub_struct!(Clone, Debug, Deserialize; Evm { #[derive(Debug, Clone)] pub struct OffchainResolvers(HashMap); +pub_struct!(Clone, Debug, Deserialize; EvmRecordVerifier { + verifier_contract: FieldElement, + field: String, + handler: HandlerType, +}); + #[derive(Deserialize)] struct RawConfig { server: Server, @@ -107,6 +116,7 @@ struct RawConfig { offchain_resolvers: OffchainResolvers, evm: Evm, evm_networks: HashMap, + evm_records_verifiers: HashMap, } pub_struct!(Clone, Deserialize; Config { @@ -122,6 +132,7 @@ pub_struct!(Clone, Deserialize; Config { offchain_resolvers: OffchainResolvers, evm: Evm, evm_networks: HashMap, + evm_records_verifiers: HashMap, }); impl Altcoins { @@ -229,6 +240,7 @@ impl From for Config { offchain_resolvers: raw.offchain_resolvers, evm: raw.evm, evm_networks: reversed_evm_networks, + evm_records_verifiers: raw.evm_records_verifiers, } } } diff --git a/src/endpoints/crosschain/ethereum/mod.rs b/src/endpoints/crosschain/ethereum/mod.rs index 61616ce..a89f482 100644 --- a/src/endpoints/crosschain/ethereum/mod.rs +++ b/src/endpoints/crosschain/ethereum/mod.rs @@ -1,3 +1,4 @@ pub mod lookup; pub mod resolve; +pub mod text_records; pub mod utils; diff --git a/src/endpoints/crosschain/ethereum/resolve.rs b/src/endpoints/crosschain/ethereum/resolve.rs index 62a93bb..9c86c61 100644 --- a/src/endpoints/crosschain/ethereum/resolve.rs +++ b/src/endpoints/crosschain/ethereum/resolve.rs @@ -37,6 +37,8 @@ use starknet::{ }; use starknet_id::encode; +use super::text_records::get_verifier_data; + #[derive(Deserialize, Debug, Clone)] pub struct ResolveQuery { data: String, // data encoded @@ -147,7 +149,6 @@ pub async fn handler(State(state): State>, query: Query) -> impl I let payload: Vec = match resolver_function_call { ResolverFunctionCall::Text(_alt_hash, record) => { match record.as_str() { - // Records available "com.discord" "com.github" "com.twitter" "avatar" => { match get_profile_picture( &state.conf, @@ -170,8 +171,42 @@ pub async fn handler(State(state): State>, query: Query) -> impl I } } _ => { - println!("Record not implemented: {:?}", record); - return get_error(format!("Record not implemented {}", record)); + let mut res: Vec = vec![]; + + if let Some(record_config) = + state.conf.evm_records_verifiers.get(&record) + { + match get_verifier_data( + &state.conf, + &provider, + id, + record_config, + ) + .await + { + Some(record_data) => { + println!("Data: {:?}", record_data); + return vec![Token::String(record_data)]; + } + None => { + println!("No data found for record: {:?}", record); + // return vec![]; + return get_error(format!( + "No data found for record: {}", + record + )); + } + } + } else { + // fetch user data for record + // header (image url), display, name, url, description, email, + // mail, notice, location, phone + println!("Record not implemented: {:?}", record); + return get_error(format!( + "Record not implemented {}", + record + )); + } } } } diff --git a/src/endpoints/crosschain/ethereum/text_records.rs b/src/endpoints/crosschain/ethereum/text_records.rs new file mode 100644 index 0000000..2f7eb45 --- /dev/null +++ b/src/endpoints/crosschain/ethereum/text_records.rs @@ -0,0 +1,134 @@ +use anyhow::{Context, Result}; +use reqwest::{Client, StatusCode}; +use serde::{Deserialize, Serialize}; +use starknet::{ + core::{ + types::{BlockId, BlockTag, FieldElement, FunctionCall}, + utils::cairo_short_string_to_felt, + }, + macros::selector, + providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider}, +}; + +use crate::config::{Config, EvmRecordVerifier}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum HandlerType { + GetDiscordName, + GetGithubName, + GetTwitterName, +} + +#[derive(Deserialize, Debug)] +struct GithubUser { + login: String, +} + +#[derive(Deserialize, Debug)] +struct DiscordUser { + username: String, +} + +impl EvmRecordVerifier { + pub async fn execute_handler(&self, config: &Config, id: FieldElement) -> Result { + match self.handler { + HandlerType::GetDiscordName => self.get_discord_name(config, id).await, + HandlerType::GetGithubName => self.get_github_name(id).await, + HandlerType::GetTwitterName => self.get_twitter_name(config, id).await, + } + } + + async fn get_discord_name(&self, config: &Config, id: FieldElement) -> Result { + let social_id = FieldElement::to_string(&id); + let url = format!("https://discord.com/api/users/{}", social_id); + let client = Client::new(); + let resp = client + .get(&url) + .header("Content-Type", "application/json") + .header( + "Authorization", + format!("Bot {}", config.variables.discord_token), + ) + .send() + .await? + .json::() + .await + .context("Failed to parse JSON response from Discord API")?; + + Ok(resp.username) + } + async fn get_github_name(&self, id: FieldElement) -> Result { + let social_id = FieldElement::to_string(&id); + let url = format!("https://api.github.com/user/{}", social_id); + let client = Client::builder() + .user_agent("request") + .build() + .context("Failed to build HTTP client")?; + let response = client + .get(&url) + .send() + .await + .context("Failed to send request to GitHub")?; + + if response.status() != StatusCode::OK { + anyhow::bail!("GitHub API returned non-OK status: {}", response.status()); + } + + let text = response + .text() + .await + .context("Failed to read response text")?; + let user: GithubUser = + serde_json::from_str(&text).context("Failed to deserialize GitHub response")?; + + Ok(user.login) + } + + async fn get_twitter_name(&self, config: &Config, id: FieldElement) -> Result { + // implementation + Ok("".to_string()) + } +} + +pub async fn get_verifier_data( + config: &Config, + provider: &JsonRpcClient, + id: FieldElement, + record_config: &EvmRecordVerifier, +) -> Option { + let call_result = provider + .call( + FunctionCall { + contract_address: config.contracts.starknetid, + entry_point_selector: selector!("get_verifier_data"), + calldata: vec![ + id, + cairo_short_string_to_felt(&record_config.field).unwrap(), + record_config.verifier_contract, + FieldElement::ZERO, + ], + }, + BlockId::Tag(BlockTag::Latest), + ) + .await; + + match call_result { + Ok(result) => { + if result[0] == FieldElement::ZERO { + return None; + } + + match record_config.execute_handler(config, result[0]).await { + Ok(name) => Some(name), + Err(e) => { + println!("Error while executing handler: {:?}", e); + None + } + } + } + Err(e) => { + println!("Error while fetchingverifier data: {:?}", e); + None + } + } +} From 8c3312780d3a9e43d4d65f4f7b7752e4eed8e55d Mon Sep 17 00:00:00 2001 From: Iris Date: Mon, 13 May 2024 17:08:43 +0200 Subject: [PATCH 2/6] feat: get_unbounded_user_data for other text records --- src/endpoints/crosschain/ethereum/resolve.rs | 75 +++++++++++-------- .../crosschain/ethereum/text_records.rs | 42 ++++++++++- 2 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/endpoints/crosschain/ethereum/resolve.rs b/src/endpoints/crosschain/ethereum/resolve.rs index 9c86c61..90f8b18 100644 --- a/src/endpoints/crosschain/ethereum/resolve.rs +++ b/src/endpoints/crosschain/ethereum/resolve.rs @@ -37,7 +37,7 @@ use starknet::{ }; use starknet_id::encode; -use super::text_records::get_verifier_data; +use super::text_records::{get_unbounded_user_data, get_verifier_data}; #[derive(Deserialize, Debug, Clone)] pub struct ResolveQuery { @@ -171,41 +171,50 @@ pub async fn handler(State(state): State>, query: Query) -> impl I } } _ => { - let mut res: Vec = vec![]; - - if let Some(record_config) = - state.conf.evm_records_verifiers.get(&record) - { - match get_verifier_data( - &state.conf, - &provider, - id, - record_config, - ) - .await - { - Some(record_data) => { - println!("Data: {:?}", record_data); - return vec![Token::String(record_data)]; + // we check if this data was added through a verifier + match state.conf.evm_records_verifiers.get(&record) { + Some(record_config) => { + let record_data = get_verifier_data( + &state.conf, + &provider, + id, + record_config, + ) + .await; + match record_data { + Some(record_data) => { + return vec![Token::String(record_data)]; + } + None => { + return get_error(format!( + "No data found for record: {}", + record + )); + } } - None => { - println!("No data found for record: {:?}", record); - // return vec![]; - return get_error(format!( - "No data found for record: {}", - record - )); + } + None => { + // if not we fetch user data for this record + // existing records : header (image url), display, name, url, description, email, mail, notice, location, phone + match get_unbounded_user_data( + &state.conf, + &provider, + id, + record, + ) + .await + { + Some(data) => { + return vec![Token::String(data)]; + } + None => { + return get_error(format!( + "No data found for record: {}", + record + )); + } } } - } else { - // fetch user data for record - // header (image url), display, name, url, description, email, - // mail, notice, location, phone - println!("Record not implemented: {:?}", record); - return get_error(format!( - "Record not implemented {}", - record - )); } } } diff --git a/src/endpoints/crosschain/ethereum/text_records.rs b/src/endpoints/crosschain/ethereum/text_records.rs index 2f7eb45..e7e5075 100644 --- a/src/endpoints/crosschain/ethereum/text_records.rs +++ b/src/endpoints/crosschain/ethereum/text_records.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use starknet::{ core::{ types::{BlockId, BlockTag, FieldElement, FunctionCall}, - utils::cairo_short_string_to_felt, + utils::{cairo_short_string_to_felt, parse_cairo_short_string}, }, macros::selector, providers::{jsonrpc::HttpTransport, JsonRpcClient, Provider}, @@ -132,3 +132,43 @@ pub async fn get_verifier_data( } } } + +pub async fn get_unbounded_user_data( + config: &Config, + provider: &JsonRpcClient, + id: FieldElement, + field: String, +) -> Option { + let call_result = provider + .call( + FunctionCall { + contract_address: config.contracts.starknetid, + entry_point_selector: selector!("get_unbounded_user_data"), + calldata: vec![ + id, + cairo_short_string_to_felt(&field).unwrap(), + FieldElement::ZERO, + ], + }, + BlockId::Tag(BlockTag::Latest), + ) + .await; + match call_result { + Ok(result) => { + if result[0] == FieldElement::ZERO { + return None; + } + let res = result + .iter() + .skip(1) + .filter_map(|val| parse_cairo_short_string(val).ok()) + .collect::>() // Collect into a vector of strings + .join(""); + Some(res) + } + Err(e) => { + println!("Error while fetchingverifier data: {:?}", e); + None + } + } +} From 40c23e58c14324191186fcb39700973f63e9f50f Mon Sep 17 00:00:00 2001 From: Iris Date: Mon, 13 May 2024 17:39:23 +0200 Subject: [PATCH 3/6] fix: return error & update config template file --- config.template.toml | 20 ++++++++++++++++++- src/endpoints/crosschain/ethereum/resolve.rs | 12 +++++------ .../crosschain/ethereum/text_records.rs | 4 ++-- 3 files changed, 27 insertions(+), 9 deletions(-) diff --git a/config.template.toml b/config.template.toml index 14096a5..13df642 100644 --- a/config.template.toml +++ b/config.template.toml @@ -22,6 +22,7 @@ argent_multicall = "0xXXXXXXXXXXXX" rpc_url = "xxxxxx" refresh_delay = 60 # in seconds ipfs_gateway = "https://gateway.pinata.cloud/ipfs/" # or https://ipfs.io/ipfs/ +discord_token = "xxxxxx" [starkscan] api_url = "https://api-testnet.starkscan.co/api/v0" @@ -83,4 +84,21 @@ private_key = "0xXXXXXXXXXXXX" polygon = 2147483785 optimism = 2147483658 base = 2147492101 -arbitrum = 2147525809 \ No newline at end of file +arbitrum = 2147525809 + +[evm_records_verifiers] + +[evm_records_verifiers."com.discord"] +verifier_contract = "0x0182EcE8173C216A395f4828e1523541b7e3600bf190CB252E1a1A0cE219d184" +field = "discord" +handler = "GetDiscordName" + +[evm_records_verifiers."com.github"] +verifier_contract = "0x0182EcE8173C216A395f4828e1523541b7e3600bf190CB252E1a1A0cE219d184" +field = "github" +handler = "GetGithubName" + +[evm_records_verifiers."com.twitter"] +verifier_contract = "0x0182EcE8173C216A395f4828e1523541b7e3600bf190CB252E1a1A0cE219d184" +field = "twitter" +handler = "GetTwitterName" \ No newline at end of file diff --git a/src/endpoints/crosschain/ethereum/resolve.rs b/src/endpoints/crosschain/ethereum/resolve.rs index 90f8b18..c402222 100644 --- a/src/endpoints/crosschain/ethereum/resolve.rs +++ b/src/endpoints/crosschain/ethereum/resolve.rs @@ -172,8 +172,9 @@ pub async fn handler(State(state): State>, query: Query) -> impl I } _ => { // we check if this data was added through a verifier + // let record_config = state.conf.evm_records_verifiers.get(&record).unwrap(); match state.conf.evm_records_verifiers.get(&record) { - Some(record_config) => { + Some(record_config) => { let record_data = get_verifier_data( &state.conf, &provider, @@ -183,7 +184,7 @@ pub async fn handler(State(state): State>, query: Query) -> impl I .await; match record_data { Some(record_data) => { - return vec![Token::String(record_data)]; + vec![Token::String(record_data)] } None => { return get_error(format!( @@ -200,13 +201,11 @@ pub async fn handler(State(state): State>, query: Query) -> impl I &state.conf, &provider, id, - record, + &record, ) .await { - Some(data) => { - return vec![Token::String(data)]; - } + Some(data) => vec![Token::String(data)], None => { return get_error(format!( "No data found for record: {}", @@ -216,6 +215,7 @@ pub async fn handler(State(state): State>, query: Query) -> impl I } } } + } } } diff --git a/src/endpoints/crosschain/ethereum/text_records.rs b/src/endpoints/crosschain/ethereum/text_records.rs index e7e5075..94c3c66 100644 --- a/src/endpoints/crosschain/ethereum/text_records.rs +++ b/src/endpoints/crosschain/ethereum/text_records.rs @@ -137,7 +137,7 @@ pub async fn get_unbounded_user_data( config: &Config, provider: &JsonRpcClient, id: FieldElement, - field: String, + field: &str, ) -> Option { let call_result = provider .call( @@ -146,7 +146,7 @@ pub async fn get_unbounded_user_data( entry_point_selector: selector!("get_unbounded_user_data"), calldata: vec![ id, - cairo_short_string_to_felt(&field).unwrap(), + cairo_short_string_to_felt(field).unwrap(), FieldElement::ZERO, ], }, From b0d2632e2bb6b90302e79294b35e04c031ca4014 Mon Sep 17 00:00:00 2001 From: Iris Date: Mon, 13 May 2024 18:35:28 +0200 Subject: [PATCH 4/6] feat: add twitter name --- src/config.rs | 1 + .../crosschain/ethereum/text_records.rs | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/config.rs b/src/config.rs index d966a61..378b83e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -76,6 +76,7 @@ pub_struct!(Clone, Debug, Deserialize; Variables { refresh_delay: f64, ipfs_gateway: String, discord_token: String, + twitter_api_key: String, }); #[derive(Deserialize)] diff --git a/src/endpoints/crosschain/ethereum/text_records.rs b/src/endpoints/crosschain/ethereum/text_records.rs index 94c3c66..160c501 100644 --- a/src/endpoints/crosschain/ethereum/text_records.rs +++ b/src/endpoints/crosschain/ethereum/text_records.rs @@ -1,6 +1,7 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Result, anyhow}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; +use serde_json::Value; use starknet::{ core::{ types::{BlockId, BlockTag, FieldElement, FunctionCall}, @@ -85,8 +86,30 @@ impl EvmRecordVerifier { } async fn get_twitter_name(&self, config: &Config, id: FieldElement) -> Result { - // implementation - Ok("".to_string()) + let social_id = FieldElement::to_string(&id); + let client = Client::new(); + let response = client.get("https://twttrapi.p.rapidapi.com/get-user-by-id") + .header("X-RapidAPI-Key", config.variables.twitter_api_key.clone()) + .header("X-RapidAPI-Host", "twttrapi.p.rapidapi.com") + .query(&[("user_id", &social_id)]) + .send() + .await?; + + if response.status() != StatusCode::OK { + anyhow::bail!("Twitter API returned non-OK status: {}", response.status()); + } + let response_body = response.text().await?; + let json: Value = serde_json::from_str(&response_body)?; + let screen_name = json + .get("data") + .and_then(|data| data.get("user_result")) + .and_then(|user_result| user_result.get("result")) + .and_then(|result| result.get("legacy")) + .and_then(|legacy| legacy.get("screen_name")) + .and_then(|screen_name| screen_name.as_str()) + .ok_or_else(|| anyhow!("Failed to extract screen name")); + + Ok(screen_name.map(|name| name.to_string()).unwrap()) } } From 08253dbb2e5b3e85b2b0f9db925efe1f83aaa880 Mon Sep 17 00:00:00 2001 From: Iris Date: Mon, 13 May 2024 18:35:59 +0200 Subject: [PATCH 5/6] dev: update config template file --- config.template.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/config.template.toml b/config.template.toml index 13df642..e3cb2b1 100644 --- a/config.template.toml +++ b/config.template.toml @@ -23,6 +23,7 @@ rpc_url = "xxxxxx" refresh_delay = 60 # in seconds ipfs_gateway = "https://gateway.pinata.cloud/ipfs/" # or https://ipfs.io/ipfs/ discord_token = "xxxxxx" +twitter_api_key = "xxxxxx" [starkscan] api_url = "https://api-testnet.starkscan.co/api/v0" From b5907ae42ce538f8e645c2262c95eb28ec6a2843 Mon Sep 17 00:00:00 2001 From: Iris Date: Tue, 14 May 2024 13:18:34 +0200 Subject: [PATCH 6/6] fix: move env var to config & add static handler --- config.template.toml | 3 +++ src/config.rs | 3 +++ src/endpoints/crosschain/ethereum/resolve.rs | 3 +-- .../crosschain/ethereum/text_records.rs | 18 ++++++++++++------ 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/config.template.toml b/config.template.toml index e3cb2b1..dc75e0c 100644 --- a/config.template.toml +++ b/config.template.toml @@ -23,7 +23,10 @@ rpc_url = "xxxxxx" refresh_delay = 60 # in seconds ipfs_gateway = "https://gateway.pinata.cloud/ipfs/" # or https://ipfs.io/ipfs/ discord_token = "xxxxxx" +discord_api_url = "https://discord.com/api" twitter_api_key = "xxxxxx" +twitter_api_url = "xxxxxx" +github_api_url = "https://api.github.com" [starkscan] api_url = "https://api-testnet.starkscan.co/api/v0" diff --git a/src/config.rs b/src/config.rs index 378b83e..2a8918a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -76,7 +76,10 @@ pub_struct!(Clone, Debug, Deserialize; Variables { refresh_delay: f64, ipfs_gateway: String, discord_token: String, + discord_api_url: String, twitter_api_key: String, + twitter_api_url: String, + github_api_url: String, }); #[derive(Deserialize)] diff --git a/src/endpoints/crosschain/ethereum/resolve.rs b/src/endpoints/crosschain/ethereum/resolve.rs index c402222..363ec41 100644 --- a/src/endpoints/crosschain/ethereum/resolve.rs +++ b/src/endpoints/crosschain/ethereum/resolve.rs @@ -174,7 +174,7 @@ pub async fn handler(State(state): State>, query: Query) -> impl I // we check if this data was added through a verifier // let record_config = state.conf.evm_records_verifiers.get(&record).unwrap(); match state.conf.evm_records_verifiers.get(&record) { - Some(record_config) => { + Some(record_config) => { let record_data = get_verifier_data( &state.conf, &provider, @@ -215,7 +215,6 @@ pub async fn handler(State(state): State>, query: Query) -> impl I } } } - } } } diff --git a/src/endpoints/crosschain/ethereum/text_records.rs b/src/endpoints/crosschain/ethereum/text_records.rs index 160c501..b146069 100644 --- a/src/endpoints/crosschain/ethereum/text_records.rs +++ b/src/endpoints/crosschain/ethereum/text_records.rs @@ -1,4 +1,4 @@ -use anyhow::{Context, Result, anyhow}; +use anyhow::{anyhow, Context, Result}; use reqwest::{Client, StatusCode}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -15,6 +15,7 @@ use crate::config::{Config, EvmRecordVerifier}; #[derive(Serialize, Deserialize, Debug, Clone)] pub enum HandlerType { + Static, GetDiscordName, GetGithubName, GetTwitterName, @@ -33,15 +34,16 @@ struct DiscordUser { impl EvmRecordVerifier { pub async fn execute_handler(&self, config: &Config, id: FieldElement) -> Result { match self.handler { + HandlerType::Static => Ok(FieldElement::to_string(&id)), HandlerType::GetDiscordName => self.get_discord_name(config, id).await, - HandlerType::GetGithubName => self.get_github_name(id).await, + HandlerType::GetGithubName => self.get_github_name(config, id).await, HandlerType::GetTwitterName => self.get_twitter_name(config, id).await, } } async fn get_discord_name(&self, config: &Config, id: FieldElement) -> Result { let social_id = FieldElement::to_string(&id); - let url = format!("https://discord.com/api/users/{}", social_id); + let url = format!("{}/users/{}", config.variables.discord_api_url, social_id); let client = Client::new(); let resp = client .get(&url) @@ -58,9 +60,9 @@ impl EvmRecordVerifier { Ok(resp.username) } - async fn get_github_name(&self, id: FieldElement) -> Result { + async fn get_github_name(&self, config: &Config, id: FieldElement) -> Result { let social_id = FieldElement::to_string(&id); - let url = format!("https://api.github.com/user/{}", social_id); + let url = format!("{}/user/{}", config.variables.github_api_url, social_id); let client = Client::builder() .user_agent("request") .build() @@ -88,7 +90,11 @@ impl EvmRecordVerifier { async fn get_twitter_name(&self, config: &Config, id: FieldElement) -> Result { let social_id = FieldElement::to_string(&id); let client = Client::new(); - let response = client.get("https://twttrapi.p.rapidapi.com/get-user-by-id") + let response = client + .get(format!( + "{}/get-user-by-id", + config.variables.twitter_api_url + )) .header("X-RapidAPI-Key", config.variables.twitter_api_key.clone()) .header("X-RapidAPI-Host", "twttrapi.p.rapidapi.com") .query(&[("user_id", &social_id)])