目的
RustでTwitterAPIv2のライブラリを書いています。
twapi-v2
TwitterAPIv2からOAuth2.0がサポートされてBearer認証で呼びだせます。
実はTwitterAPIv1で使われていたOAuth認証もまだ使えると気が付いたのでサポートしました。
OAuth2.0とOAuth1.0aの違い
OAuth2.0
■ WebなどのOAuth結果
access_token : APIを呼び出すトークン
refresh_token : 有効期限が切れたaccess_tokenの更新のためのトークン
expires_in : access_tokenの有効期間
■ APIを呼び出す時の使い方
ヘッダーに「Authorization: Bearer <access_token>」を設定
■ 有効期限
あり
OAuth1.0a
■ WebなどのOAuth結果
oauth_token : APIの認証の計算に使用
oauth_token_secret : APIの認証の計算に使用
■ APIを呼び出す時の使い方
以下を使って認証トークンを計算する
- アプリケーションを登録した時に得られるoauth_consumer_key, oauth_consumer_secret
- 上記のoauth_token, oauth_token_secret
- APIのURL, メソッド, クエリーパラメーター
ヘッダーに「Authorization: <認証トークン>」を設定
■ 有効期限
無し
OAuth2.0しかサポートしなかった0.5系
Bearer認証しかサポートしていないコードは以下のようになります。最初に認証コードを渡しています。
use twapi_v2::api::get_2_tweets_id;
#[tokio::main]
async fn main() {
let bearer_code = std::env::var("BEARER_CODE").unwrap();
let tweet_id = std::env::var("TWEET_ID").unwrap();
let res = get_2_tweets_id::Api::open(&bearer_code, &tweet_id)
.execute()
.await;
if let Some((val, rate_limit)) = res {
println!("{:?}", val);
}
}
OAuth2.0とOAuth1.0aをサポートした0.7系
認証方法は実行時に渡すことにしました。以下のようなコードになります。
use twapi_v2::api::{get_2_tweets_id, BearerAuthentication};
#[tokio::main]
async fn main() {
let bearer_code = std::env::var("BEARER_CODE").unwrap();
let auth = BearerAuthentication::new(bearer_code);
let tweet_id = std::env::var("TWEET_ID").unwrap();
let res = get_2_tweets_id::Api::open(&tweet_id)
.execute(&auth)
.await;
if let Some((val, rate_limit)) = res {
println!("{:?}", val);
}
}
OAuth1.0aは以下のようになります。
use twapi_v2::api::{get_2_tweets_id, BearerAuthentication};
use twapi_v2::oauth10a::OAuthAuthentication;
#[tokio::main]
async fn main() {
let auth = OAuthAuthentication::new(
std::env::var("CONSUMER_KEY").unwrap_or_default(),
std::env::var("CONSUMER_SECRET").unwrap_or_default(),
std::env::var("ACCESS_KEY").unwrap_or_default(),
std::env::var("ACCESS_SECRET").unwrap_or_default(),
);
let tweet_id = std::env::var("TWEET_ID").unwrap();
let res = get_2_tweets_id::Api::open(&tweet_id)
.execute(&auth)
.await;
if let Some((val, rate_limit)) = res {
println!("{:?}", val);
}
}
コードの説明
Authenticationトレイト
まず共通する認証用のトレイトを作成しました。
pub trait Authentication {
fn execute(
&self,
builder: RequestBuilder,
method: &str,
uri: &str,
options: &[(&str, &str)],
) -> RequestBuilder;
}
builderにはここで計算した認証情報を設定して返します。それ以外のパラメーターはOAuth1.0aの計算で必要なものです。
OAuth2.0
OAuth2.0用の実装です。bearer_codeだけが必要になります。
pub struct BearerAuthentication {
bearer_code: String,
}
impl BearerAuthentication {
pub fn new<T: Into<String>>(bearer_code: T) -> Self {
Self {
bearer_code: bearer_code.into(),
}
}
}
impl Authentication for BearerAuthentication {
fn execute(
&self,
builder: RequestBuilder,
_method: &str,
_uri: &str,
_options: &[(&str, &str)],
) -> RequestBuilder {
builder.bearer_auth(&self.bearer_code)
}
}
Bearer認証はとても簡単です。reqwestが専用のメソッドを用意しています。
OAuth1.0a
OAuth1.0a用の実装です。4つの値が必要になります。
pub struct OAuthAuthentication {
consumer_key: String,
consumer_secret: String,
access_key: String,
access_secret: String,
}
impl OAuthAuthentication {
pub fn new<T: Into<String>>(
consumer_key: T,
consumer_secret: T,
access_key: T,
access_secret: T,
) -> Self {
Self {
consumer_key: consumer_key.into(),
consumer_secret: consumer_secret.into(),
access_key: access_key.into(),
access_secret: access_secret.into(),
}
}
}
impl Authentication for OAuthAuthentication {
fn execute(
&self,
builder: RequestBuilder,
method: &str,
uri: &str,
options: &[(&str, &str)],
) -> RequestBuilder {
let auth = oauth1_authorization_header(
&self.consumer_key,
&self.consumer_secret,
&self.access_key,
&self.access_secret,
method,
uri,
&options.to_vec(),
);
builder.header(reqwest::header::AUTHORIZATION, auth)
}
}
この認証ではbuilder以外の引数を全て使って認証コードを計算します。builderにはAuthorizationのヘッダーとして設定します。
各API
実際に利用してるコードは以下のようになります。
pub fn build(self, authentication: &impl Authentication) -> RequestBuilder {
let mut query_parameters = vec![];
if let Some(expansions) = self.expansions {
query_parameters.push(("expansions", expansions.iter().join(",")));
}
if let Some(media_fields) = self.media_fields {
query_parameters.push(("media.fields", media_fields.iter().join(",")));
}
if let Some(place_fields) = self.place_fields {
query_parameters.push(("place.fields", place_fields.iter().join(",")));
}
if let Some(poll_fields) = self.poll_fields {
query_parameters.push(("poll.fields", poll_fields.iter().join(",")));
}
if let Some(tweet_fields) = self.tweet_fields {
query_parameters.push(("tweet.fields", tweet_fields.iter().join(",")));
}
if let Some(user_fields) = self.user_fields {
query_parameters.push(("user.fields", user_fields.iter().join(",")));
}
let client = reqwest::Client::new();
let url = URL.replace(":id", &self.id);
let builder = client.get(&url).query(&query_parameters);
authentication.execute(
builder,
"GET",
&url,
&query_parameters
.iter()
.map(|it| (it.0, it.1.as_str()))
.collect::<Vec<_>>(),
)
}
query_parameterは実際にリクエストに渡すだけでなくOAuthの計算にも必要になります。
まとめ
共通トレイトを書いて認証方法をそれぞれ実装しました。利用者は使いたい認証を選択するだけでAPIは同じように呼び出すことができます。