LoginSignup
6
2

More than 1 year has passed since last update.

大学院中退するゴミが Expo (React Native) で Twitter ログインしてツイートするまでを誰よりも丁寧に説明するから読んでくれ【即ツイートくん】

Posted at

はじめに

どうも yoshii です。
普段は趣味でアプリやサイトを作っているゴミです。

最近"即ツイートくん"というアプリを作りました。
ほんまにエグい速度でツイートできます。
結構便利なのでぜひ使ってください。

iOS

Android

今回紹介するもの

Expo で Twitter ログインする方法を説明する日本語の記事なかったので、俺がやります。

こんな感じでログインしてツイートできます。

タイトルなし.gif

公式サンプル

JavaScript なのとバックエンドが express なのが微妙ですが、一応 Expo から公式サンプルが提供されています。
今回のコードもこれを参考に作りました。

GitHub

コードだけ見たい人はこのリポジトリ見てください。
src/utils/datas.ts の API_KEY と API_SECRET を自分で取得したキーに置き換えると動作確認ができるはずです。

技術構成

  • OS: macOS Monterey
  • 言語: TypeScript
  • node: v16.15.0
  • パッケージマネージャ: yarn
  • フレームワーク:
    • バックエンド: NestJS
    • モバイルアプリ: React Native (Expo)

前提条件

Twitter Developer Account

Twitter Developer ページ から Developer account を取得しておいてください。
Twitter 連携アプリを作る時は絶対に必要になるので持っておきましょう。

以下の記事なんかを参考に必要事項を英語で入力して承認されたら Twitter API を使えるようになります。

正直、この審査は運ゲーな気がしています。頑張ってください。

Twitter APIキーを取得

上述した Twitter Developer ページ から Twitter アプリを作成し、APIキーを取得してください。
v1.1 しか使わないです。

Nest CLI の導入

以下の公式ドキュメントの通り Nest CLI を入れておきましょう。

Expo CLI の導入から環境構築まで

以下の公式ドキュメントの通り Expo CLI を入れておきましょう。

あとは Xcode 入れたりして、テンプレートプロジェクトをシミュレーターで起動し動作確認できるようにしましょう。

割と面倒な気がするので、こういう賢い人が書いた記事を読みましょう。

ログイン機能の作成

Twitter APIの認証設定

Twitter Developer ページ のアプリの設定の User authentication settings からログインできるようにしておきましょう。

スクリーンショット 2022-07-04 20.37.04.png

Editを押して、 OAuth 1.0a を有効にします。

スクリーンショット 2022-07-04 20.38.36.png

次に、Read and write の権限を追加します。

スクリーンショット 2022-07-04 20.38.45.png

次に、コールバックURLを設定します。
以下の4つを設定します。

image.png

  • http://127.0.0.1:19000/
  • exp://
  • http://localhost:19000/
  • https://auth.expo.io/@{Expo のユーザーID}/{slug}

4つ目は expo login しているアカウントのユーザーIDと、 app.jsonslug が必要です。
よくわからない時は、 後で出てくる makeRedirectUri 関数で生成されるURLを入力すればOKです。

Website URL は今回サンプルなので適当に Twitter の URL でも入れときましょう。

スクリーンショット 2022-07-04 20.46.06.png

Nest CLI でプロジェクトを作成

以下のコマンドでプロジェクトを初期化します。
プロジェクト名は login-server とします。

nest new login-server

パッケージマネージャーを聞かれるので好きなのを選んでください。
筆者は yarn を選びます。
しばらく待つとプロジェクトができます。

プロジェクトができたら src/main.ts を編集します。
デプロイした時用です。

src/main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT || 3000);
}
bootstrap();

以下のコマンドでローカルサーバーが起動します。
http://localhost:3000/ にアクセスして Hello World! って表示されるはずです。

yarn start:dev

ログイン用のエンドポイントを用意

Twitter API のラッパーは twitter-api-v2 を使います。
ライブラリのリポジトリです。

別にラッパーは好きなのを使ってくれたらいいです。 twitter-lite とかでも大丈夫です。

以下のコマンドでパッケージを入れます。

yarn add twitter-api-v2

まずは、各種トークンの情報を定数で用意します。
本来は .env とかで管理するのが良いかもしれないですが、面倒なのでこんな感じにしちゃいます。

src/utils/datas.ts
export const appKey = {APIキー};
export const appSecret = {APIシークレット};

次に、 src/app.service.ts を以下のように編集します。

src/app.service.ts
import { Injectable } from '@nestjs/common';
import { TwitterApi } from 'twitter-api-v2';
import { appKey, appSecret } from './utils/datas';

@Injectable()
export class AppService {
  getClient() {
    return new TwitterApi({
      appKey,
      appSecret,
    });
  }

  async getRequestToken(callBackUrl: string) {
    return await this.getClient().generateAuthLink(callBackUrl, {
      linkMode: 'authorize',
    });
  }

  async getAccessToken(
    access_token_key: string,
    access_token_secret: string,
    oauth_verifier: string,
  ) {
    const twitter = new TwitterApi({
      appKey,
      appSecret,
      accessToken: access_token_key,
      accessSecret: access_token_secret,
    });
    const { userId, screenName, accessToken, accessSecret } =
      await twitter.login(oauth_verifier);
    return {
      userId,
      screenName,
      accessToken,
      accessSecret,
    };
  }
}

そして、 src/app.controller.ts を編集してエンドポイントを用意します。

src/app.controller.ts
import { Controller, Get, Req } from '@nestjs/common';
import { AppService } from './app.service';
import { Request } from 'express';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('request-token')
  async getRequestToken(@Req() request: Request) {
    const { callback_url } = request.query;
    if (!callback_url) return null;
    return this.appService.getRequestToken(callback_url as string);
  }

  @Get('access-token')
  async getAccessToken(@Req() request: Request) {
    const { oauth_token, oauth_token_secret, oauth_verifier } = request.query;
    return await this.appService.getAccessToken(
      oauth_token as string,
      oauth_token_secret as string,
      oauth_verifier as string,
    );
  }
}

コードの簡単な説明をすると、まず http://localhost:3000/request-token に GET リクエストを送信することで、ログイン画面を表示するためのトークンが取得できます。
このトークンを使ってアプリ側でログイン画面からログインして得られる oauth_verifier を使って http://localhost:3000/request-token に GET リクエストを送信することで、アクセストークンを取得できるという流れです。
このアクセストークンを使うことでそのユーザーの権限で Twitter API を使えるというわけです。
例えば、 Twitter API で鍵垢のツイートを取得する際、そのアカウントをフォローしてる Twitter アカウントでログインして取得したアクセストークンじゃないと権限がなくてエラーになるって感じです。

Expoでアプリを作成

アプリの初期化

以下のコマンドでプロジェクトを初期化します。名前は login-app にします。

expo init login-app

なんか選ばされるんで、以下を選びます。

❯   blank (TypeScript)  same as blank but with TypeScript configuration

しばらく待つとプロジェクトができます。
以下のコマンドで勝手にブラウザが開きます。

cd sample-expo-login/
yarn start

著者は iOS 派なので iOS シミュレータで開きます。
さっきブラウザで開いた画面から Run on iOS simulator っていうボタンを押してしばらく待ちます。
こんな感じの画面が出てきたら成功です。

スクリーンショット 2022-07-01 17.56.18.png

ログイン機能を追加

HTTPクライアントは ky を使います。
ky のリポジトリです。

以下のコマンドでインストールしましょう。

expo install ky

これは好きなやつでいいです。 axios でも、デフォルトの fetch でも大丈夫です。

以下のコマンドで expo-auth-session も入れます。

expo install expo-auth-session

まず、エンドポイントを定数で用意します。
さっき NestJS で作ったやつですね。

utils/endpoints.ts
const API_BASE_URL = "http://localhost:3000/";

export const API_REQUEST_TOKEN_URL = `${API_BASE_URL}request-token`;
export const API_ACCESS_TOKEN_URL = `${API_BASE_URL}access-token`;

export const AUTH_ENDPOINT = "https://api.twitter.com/oauth/authorize";

適当に API のレスポンスの型を定義しときます。

types/index.d.ts
export type AuthResult = {
  authentication: TokenResponse | null;
  error?: AuthError | null;
  errorCode: string | null;
  params: {
    [key: string]: string;
  };
  type: "error" | "success";
  url: string;
};

export type RequestToken = {
  url: string;
  oauth_token: string;
  oauth_token_secret: string;
  oauth_callback_confirmed: string;
};

export type AccountInformation = {
  userId: string;
  accessToken: string;
  accessSecret: string;
  photoUrl: string;
  screenName: string;
};

次に、ログインの処理 twitterLogin を作成します。
成功したら userId とか accessToken とかの情報を返却して、エラーが起きたら undefined を返却する関数です。
makeRedirectUri 関数でさっきのコールバックURLを作成しています。
さっきわからなかった人は、ここで作られたURLをconsole.logとかして見てみましょう。

lib/twitter.ts
import { Platform } from "react-native";
import { makeRedirectUri, startAsync } from "expo-auth-session";
import {
  API_ACCESS_TOKEN_URL,
  API_REQUEST_TOKEN_URL,
  AUTH_ENDPOINT,
} from "../utils/endpoints";
import { AccountInformation, AuthResult, RequestToken } from "../types";
import ky from "ky";

export const toQueryString = (params: any) =>
  "?" +
  Object.entries(params)
    .map(
      ([key, value]) =>
        `${encodeURIComponent(key)}=${encodeURIComponent(value as any)}`
    )
    .join("&");

export const twitterLogin = async () => {
  try {
    const useProxy = Platform.select({ default: true, web: false });
    const redirect = makeRedirectUri({ useProxy });
    const requestRes = await ky(
      `${API_REQUEST_TOKEN_URL}${toQueryString({
        callback_url: redirect,
      })}`,
      {
        method: "GET",
      }
    );
    const tokens = (await requestRes.json()) as RequestToken;
    const authResponse = await startAsync({
      authUrl: `${AUTH_ENDPOINT}${toQueryString(tokens)}`,
    });
    if (
      authResponse.type === "cancel" ||
      authResponse.type === "locked" ||
      authResponse.type === "error"
    )
      throw new Error("cancel auth");
    const authResponseResult = authResponse as AuthResult;
    if (authResponseResult.params && authResponseResult.params.denied)
      throw new Error("cancel auth");
    const { oauth_token, oauth_token_secret } = tokens;
    const accessRes = await ky(
      `${API_ACCESS_TOKEN_URL}${toQueryString({
        oauth_token,
        oauth_token_secret,
        oauth_verifier: authResponseResult.params.oauth_verifier,
      })}`,
      {
        method: "GET",
      }
    );
    return (await accessRes.json()) as AccountInformation;
  } catch (e) {
    console.log(e);
    return undefined;
  }
};

最後に画面を作成します。
ログインが完了したら ID とトークンが確認できます。

App.tsx
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { twitterLogin } from "./lib/twitter";
import { AccountInformation } from "./types";

export default function App() {
  const [accountInfo, setAccountInfo] = useState<AccountInformation>();

  const onPressLoginButton = async () => {
    setAccountInfo(await twitterLogin());
  };

  return (
    <View style={styles.container}>
      <StatusBar style="auto" />
      <Text>ボタンを押してログインしてね</Text>
      <TouchableOpacity onPress={onPressLoginButton} style={styles.button}>
        <Text style={styles.buttonLabel}>Login</Text>
      </TouchableOpacity>
      {accountInfo && (
        <View>
          <Text>{accountInfo.screenName}</Text>
          <Text>{accountInfo.accessToken}</Text>
          <Text>{accountInfo.accessSecret}</Text>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  button: {
    borderColor: "lightgray",
    borderWidth: 2,
    borderRadius: 10,
    backgroundColor: "#00acee",
    width: 200,
    height: 40,
  },
  buttonLabel: {
    color: "white",
    fontWeight: "bold",
    fontSize: 24,
    textAlign: "center",
    textAlignVertical: "center",
    lineHeight: 32,
  },
});

これでひとまずログイン機能は作成完了です!

ツイート機能の作成

ツイート用のエンドポイントを作成

NestJSプロジェクトの方でツイート用のエンドポイントを作成していきます。
まずはサービスで処理を追加します。

src/app.service.ts
import { Injectable } from '@nestjs/common';
import { TwitterApi } from 'twitter-api-v2';
import { appKey, appSecret } from './utils/datas';

@Injectable()
export class AppService {
  getClient() {
    return new TwitterApi({
      appKey,
      appSecret,
    });
  }

  async getRequestToken(callBackUrl: string) {
    return await this.getClient().generateAuthLink(callBackUrl, {
      linkMode: 'authorize',
    });
  }

  async getAccessToken(
    access_token_key: string,
    access_token_secret: string,
    oauth_verifier: string,
  ) {
    const twitter = new TwitterApi({
      appKey,
      appSecret,
      accessToken: access_token_key,
      accessSecret: access_token_secret,
    });
    const { userId, screenName, accessToken, accessSecret } =
      await twitter.login(oauth_verifier);
    return {
      userId,
      screenName,
      accessToken,
      accessSecret,
    };
  }

  async postTweet(accessToken: string, accessSecret: string, text: string) {
    const twitter = new TwitterApi({
      appKey,
      appSecret,
      accessToken,
      accessSecret,
    });
    await twitter.v1.tweet(text);
  }
}

そして、コントローラーでエンドポイントを定義します。

src/app.controller.ts
import { Controller, Get, Post, Req } from '@nestjs/common';
import { AppService } from './app.service';
import { Request } from 'express';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get('request-token')
  async getRequestToken(@Req() request: Request) {
    const { callback_url } = request.query;
    if (!callback_url) return null;
    return this.appService.getRequestToken(callback_url as string);
  }

  @Get('access-token')
  async getAccessToken(@Req() request: Request) {
    const { oauth_token, oauth_token_secret, oauth_verifier } = request.query;
    return await this.appService.getAccessToken(
      oauth_token as string,
      oauth_token_secret as string,
      oauth_verifier as string,
    );
  }

  @Post('tweet')
  async postTweet(@Req() request: Request) {
    const { accessToken, accessSecret, text } = request.body;
    await this.appService.postTweet(accessToken, accessSecret, text);
  }
}

アプリにツイート機能を追加

Expo アプリの方でツイート機能を追加していきます。
まずは utils/endpoints.ts にさっき作ったエンドポイントを追加しましょう。

utils/endpoints.ts
const API_BASE_URL = "http://localhost:3000/";

export const API_REQUEST_TOKEN_URL = `${API_BASE_URL}request-token`;
export const API_ACCESS_TOKEN_URL = `${API_BASE_URL}access-token`;
export const API_TWEET_URL = `${API_BASE_URL}tweet`;

export const AUTH_ENDPOINT = "https://api.twitter.com/oauth/authorize";

次に、 lib/twitter.tspostTweet 関数を作成します。
さっき作ったエンドポイントに POST リクエストを送るだけですね。

lib/twitter.ts
import { Platform } from "react-native";
import { makeRedirectUri, startAsync } from "expo-auth-session";
import {
  API_ACCESS_TOKEN_URL,
  API_REQUEST_TOKEN_URL,
  API_TWEET_URL,
  AUTH_ENDPOINT,
} from "../utils/endpoints";
import { AccountInformation, AuthResult, RequestToken } from "../types";
import ky from "ky";

export const toQueryString = (params: any) =>
  "?" +
  Object.entries(params)
    .map(
      ([key, value]) =>
        `${encodeURIComponent(key)}=${encodeURIComponent(value as any)}`
    )
    .join("&");

export const twitterLogin = async () => {
  try {
    const useProxy = Platform.select({ default: true, web: false });
    const redirect = makeRedirectUri({ useProxy });
    const requestRes = await ky(
      `${API_REQUEST_TOKEN_URL}${toQueryString({
        callback_url: redirect,
      })}`,
      {
        method: "GET",
      }
    );
    const tokens = (await requestRes.json()) as RequestToken;
    const authResponse = await startAsync({
      authUrl: `${AUTH_ENDPOINT}${toQueryString(tokens)}`,
    });
    if (
      authResponse.type === "cancel" ||
      authResponse.type === "locked" ||
      authResponse.type === "error"
    )
      throw new Error("cancel auth");
    const authResponseResult = authResponse as AuthResult;
    if (authResponseResult.params && authResponseResult.params.denied)
      throw new Error("cancel auth");
    const { oauth_token, oauth_token_secret } = tokens;
    const accessRes = await ky(
      `${API_ACCESS_TOKEN_URL}${toQueryString({
        oauth_token,
        oauth_token_secret,
        oauth_verifier: authResponseResult.params.oauth_verifier,
      })}`,
      {
        method: "GET",
      }
    );
    return (await accessRes.json()) as AccountInformation;
  } catch (e) {
    console.log(e);
    return undefined;
  }
};

export const postTweet = async (
  { accessToken, accessSecret }: AccountInformation,
  text: string
) => {
  await ky.post(API_TWEET_URL, {
    json: {
      accessToken,
      accessSecret,
      text,
    },
  });
};

最後に画面を作成します。

App.tsx
import { StatusBar } from "expo-status-bar";
import { useState } from "react";
import {
  Alert,
  StyleSheet,
  Text,
  TextInput,
  TouchableOpacity,
  View,
} from "react-native";
import { postTweet, twitterLogin } from "./lib/twitter";
import { AccountInformation } from "./types";

export default function App() {
  const [accountInfo, setAccountInfo] = useState<AccountInformation>();
  const [text, setText] = useState<string>("");

  const onPressLoginButton = async () => {
    setAccountInfo(await twitterLogin());
  };

  const onPressTweetButton = async () => {
    try {
      accountInfo && (await postTweet(accountInfo, text));
      setText("");
      Alert.alert("ツイート完了!");
    } catch (e) {
      console.log(e);
      Alert.alert("ツイート失敗...");
    }
  };

  return (
    <View style={styles.container}>
      <StatusBar style="auto" />
      {accountInfo ? (
        <View>
          <TextInput
            value={text}
            onChangeText={(value) => setText(value)}
            style={styles.input}
          />
          <TouchableOpacity onPress={onPressTweetButton} style={styles.button}>
            <Text style={styles.buttonLabel}>ツイートする</Text>
          </TouchableOpacity>
        </View>
      ) : (
        <View>
          <Text>ボタンを押してログインしてね</Text>
          <TouchableOpacity onPress={onPressLoginButton} style={styles.button}>
            <Text style={styles.buttonLabel}>Login</Text>
          </TouchableOpacity>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: "#fff",
    alignItems: "center",
    justifyContent: "center",
  },
  button: {
    borderColor: "lightgray",
    borderWidth: 2,
    borderRadius: 10,
    backgroundColor: "#00acee",
    width: 200,
    height: 40,
  },
  buttonLabel: {
    color: "white",
    fontWeight: "bold",
    fontSize: 24,
    textAlign: "center",
    textAlignVertical: "center",
    lineHeight: 32,
  },
  input: {
    borderWidth: 1,
    borderColor: "black",
    borderRadius: 5,
    fontSize: 18,
    padding: 2,
  },
});

これで TextInput に入力した文字をツイートできるはずです!

まとめ

Expo + NestJS で Twitter ログインする方法を丁寧に説明してみました。
わからないところとかあったらコメントで質問してください。
一緒に考えます。

あと、友達が少ないのでよかったら友達になってくれると嬉しいです…
この記事見てきたって言ってくれたらフォロー返せると思います。

それではまた〜

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2