DjangoとTwitterを連携させ、Twitterへ自動投稿する

独学7ヶ月目の未経験で、技術記事を初めて投稿します。温かい目で見てやってください 🥺

# もくじ
- [何をするか](#何をするか)
- [なぜこの記事を書くか](#なぜこの記事を書くか)
- [どうやって解決したか](#どうやって解決したか)
- [実装](#実装)
- [最後に](#最後に)

# 何をするか
- Twitterアカウントで自作アプリにログイン・Twitterアカウントを自作アプリアカウントに紐付け
- 認証したTwitterアカウントでツイートする

# なぜこの記事を書くか
上記の実装するにあたり、参考記事を探しましたが、「Twitter連携後、自動投稿する」という記事がなかったので、私と同じような実装をしようとしている人の助力になればと思っています。また、自分の備忘録のためでもあります。

# どうやって解決したか
social_django, social_coreモジュールのコード読み、なんかいけそうやなぁと思ってやったらできました。
割と無理矢理感はありますが、とりあえず動きます😅


# 実装
以下、どのように実装したのか記載します。
## ディレクトリ構成
```terminal
.
├── manage.py
├── myapp
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── socials
|   ├── __init__.py
|   ├── admin.py
|   ├── apps.py
|   ├── migrations
|   │   └── __init__.py
|   ├── models.py
|   ├── tests.py
|   ├── twitter_api.py
|   ├── urls.py
|   └── views.py
|
└─── accounts
    ├── __init__.py
    ├── admin.py
    ├── apps.py
    ├── migrations
    │   └── __init__.py
    ├── models.py
    ├── tests.py
    ├── twitter_api.py
    ├── urls.py
    └── views.py
```

## 前提
すでに以下のようなユーザーモデルがあるとします。

今回はユーザーについての詳細は書きません。Twitterアカウントとの連携に的を絞って書いていきたいと思います。

```python

# accounts/models.py
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):
    pass
```

myapp/urls.pyを設定しておきます。

```python
# myapp/urls.py

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('accounts.urls', namespace='accounts')),
    path('socials/', include('socials.urls', namespace='socials')),
]
```

## Twitterアカウントで自作アプリにログイン　or　Twitterアカウントを自作アプリアカウントに紐付け
DjangoアプリへのTwitter OAuth導入は、下記記事に詳しくまとめられています。

[DjangoでTwitter認証アプリケーションを[2018/06最新版]](https://qiita.com/Kyou13/items/713a878065df82a0ecc3)

私は上記記事でTwitterアカウントでのログインの実装はできたのですが、Twitterアカウントと自作アプリアカウントととの紐付け方がわかりませんでした。
調べてみた結果、自作アプリアカウントでログインしながら、 認証URLにGETすれば、勝手に紐付けてくれるようです（楽ちん）

<br>

## 認証したTwitterアカウントでツイートする
Twitterに投稿する際の流れは、2パターンあります。

- Twitterアカウントと既に連携済みで投稿する。
- Twitterアカウントと未連携の状態で、 OAuth認証と投稿を同時に行う。

上記２パターンの解説をする前に、Twitterへ投稿するための、Twitterモジュールをインストールしておきます。

``` terminal
pip intall twitter
```

投稿の仕方は、下記記事を参考にしました。

[Python3で簡単にtwitterに投稿する](https://qiita.com/shtnkgm/items/cc55be0e09b484ce8e43)

では、認証アカウントで投稿する方法を順次説明します。

### ■　Twitterアカウントと既に連携済みで投稿する。
これは簡単でした。以下、大まかな流れです。

1. urls.pyでPOSTを受け取り、views.py/twitterへ投げる。
2. post_twitterメソッドを実行し、POSTの内容を投稿。
3. Twitter投稿結果をレスポンスとして、ユーザーへ返す。


まず、urlsを設定。

```python
# socials/urls.py

from django.urls import path

from . import views


app_name = 'social'
urlpatterns = [
    path('twitter/', views.twitter, name="twitter"),
]

```
<br>
投稿処理をするpost_twitterメソッドを作成しておきます。
Twitterアカウントの情報は、UserSocialAuthモデルから取得しています。

```python
# socials/twitter_api.py

from django.conf import settings
from social_django.models import UserSocialAuth
import twitter

def post_twitter(user, content):
    social_auth = UserSocialAuth.objects.get(user=user, provider='twitter')

    client_key = social_auth.extra_data['access_token']['oauth_token']
    client_secret = social_auth.extra_data['access_token']['oauth_token_secret']

    auth = twitter.OAuth(
        consumer_key=settings.SOCIAL_AUTH_TWITTER_KEY, #settings.pyに設定しているトークン
        consumer_secret=settings.SOCIAL_AUTH_TWITTER_SECRET,　#settings.pyに設定しているシークレットキー
        token=client_key,
        token_secret=client_secret
    )
    t = twitter.Twitter(auth=auth)
    status_update = t.statuses.update(status=content)

    return status_update

```
<br>
あとは、viewを作成し、POSTしてもらうだけ！

```python
# socials/views.py
import json

from django.http.response import JsonResponse

from . import twitter_api


def twitter(request, *args, **kwargs):
    if request.method == 'POST':
        user = request.user
        data = json.loads(request.body) #　データを取り出す。
        content = data['content'] # POSTのnameは、"content"に設定。
        twitter_res = twitter_api.post_twitter(user, content)

    return JsonResponse(twitter_res)

```

### ■　Twitterアカウントと未連携の状態で、 OAuth認証と投稿を同時に行う。
これは少し悩みました。
なぜかというと、ユーザーからPOSTしてもらった投稿データは、Twitter認証のリダイレクト時に消えてしまうからです。解決策として、ユーザーモデルに一時的にデータを保存するカラムを作っておき対応することにしました。
以下、大まかな流れです。

1. urls.pyでPOSTを受け取り、views.authメソッドへ投げる。
2. views.authメソッドを実行し、認証。その時に、POSTデータをUserモデルに保存しておく。
3. リダイレクトで自動的に、views.completeメソッドが呼ばれ、認証モデルが作成される。その時に２で保存していたデータをTwitterに投稿 & 削除。

では、詳細へ。

Userモデルにデータを一時的に保存するカラムを追加

```python
# accounts/models.py
from django.contrib.auth.models import AbstractUser

class User(AbstractUser):

    temp_data = models.CharField(
        max_length=20000,
        blank=True
    )
```
<br>

認証時に投稿処理を追加するため、自作のurls.pyを作成し、自作viewsとsocial_django.viewsを使い分ける。

```python
from django.conf import settings
from django.conf.urls import url
from django.urls import path
from social_core.utils import setting_name
from social_django import views as social_views

from . import views


extra = getattr(settings, setting_name('TRAILING_SLASH'), True) and '/' or ''

app_name = 'social'
urlpatterns = [
    path('twitter/', views.twitter, name="twitter"),
    
    # 以下、追加
    # 自作のviews
    url(r'^login/(?P<backend>[^/]+){0}$'.format(extra), views.auth,
        name='begin'),
    url(r'^complete/(?P<backend>[^/]+){0}$'.format(extra), views.complete,
        name='complete'),
    
    # social_djangoのviews
    url(r'^disconnect/(?P<backend>[^/]+){0}$'.format(extra), social_views.disconnect,
        name='disconnect'),
    url(r'^disconnect/(?P<backend>[^/]+)/(?P<association_id>\d+){0}$'
        .format(extra), social_views.disconnect, name='disconnect_individual'),
]
```

viewを作成。

```python
import json

from django.views.decorators.csrf import csrf_exempt
from django.http.response import JsonResponse
from social_django.views import auth as social_auth, complete as social_complete

from .provider_api import twitter_api


def twitter(request, *args, **kwargs):
    if request.method == 'POST':
        user = request.user
        data = json.loads(request.body)
        content = data['content']
        twitter_res = twitter_api.post_twitter(user, content)

    return JsonResponse(twitter_res)

# 以下、追加
@csrf_exempt # これがないとcsrf_tokenに引っかかる
def auth(request, backend):
    ret = social_auth(request, backend) # social_djangoのviewをそのまま流用

    # POSTデータを一時的に保存
    if request.method == 'POST':
        user = request.user
        user.temp_data = request.POST['content']
        user.save()

    return ret


@csrf_exempt
def complete(request, backend):
    ret = social_complete(request, backend) # social_djangoのviewをそのまま流用
    
    # 一時的に保存されているデータがあれば、twitterに投稿し、データを削除。
    user = request.user
    temp_data = user.temp_data
    if temp_data and backend == 'twitter':
        twitter_api.post_twitter(user, temp_data)
        user.temp_data = ''
        user.save()

    return ret
```

これでサーバーサイドの実装は完了。

お次はフロントエンドをvueで簡単に作成していきます。
まず、非同期POSTするためのメソッドはaxiosを使用。

```node.js
import axios from "axios";
import Cookies from "js-cookie";
import Vue from "vue";
import VueAxios from "vue-axios";

Vue.use(VueAxios, axios);

const Api = {
  post: (url, data) => {
    const csrftoken = Cookies.get("csrftoken");
    const headers = {
      "X-CSRFToken": csrftoken,
      "Content-Type": "application/json"
    };

    return Vue.axios
      .post(url, data, {headers: headers})
      .catch(function(error) {
        alert("エラーが発生しました。");
        throw new Error(`Api postJson ${error}`);
      });
  },
}
```

表示は、Vue.jsを使用し、すでにtwitterと連携しているがどうかで表示させるボタンを変えます。
連携済みの場合は、"socials/twitter"に非同期POSTします。
未連携の場合は、認証処理をしないといけないので、formから"socials/login/twitter/"にPOSTします。

```vue
<!-- component -->
<template>
  <div>
    <form method="post" action="/socials/login/twitter/" ref="twitter">
      <v-textarea name="content" v-model="twitterContent" />
    </form>
    <template>
      <buttom v-if="isLinkedTwitter()" @click="postTwitter()">投稿</buttom>
      <buttom v-else @click="connectAndPostTwitter()">Twitter連携 ＆ 投稿</buttom>
    </template>
  </div>
</template>
<script>
import { mapGetters } from 'vuex'; // ログインユーザーの情報取得用
import { Api } from "@/asynchronous/api"; // 上記の非同期POSTメソッドを読み込み

export default {
  data() {
    return {
      twitterContent: '',
    }
  },
  computed: {
      ...mapGetters('accounts', ['getMyself']), // ログインユーザーの情報取得
  },
  methods: {
    checkLinkedTwitter() {
      const oauths = this.getMyself['social_auth'];
      for (const oauth of oauths) {
        if (oauth['provider'] === 'twitter') {
          this.isLinkedTwitter = true;
          return;
        }
      }
      this.isLinkedTwitter = false;
    },

    postTwitter() {
      const data = {content: this.twitterContent};
      Api.post("socials/twitter", data)
        .then(res => {
          alert('投稿しました')
       })
        .catch(error => {
          alert("投稿に失敗しました");
       })
    },
    connectAndPostTwitter() {
      this.$refs.twitter.submit();
      alert('投稿しました');
    }
  }
}
</script>
```
フロント側も完了！

これで実装は完了です。たぶん、動きます。

# 最後に
最後まで見ていただきありがとうございました。
初めての技術記事投稿のため、わかりにくい部分が多々あったと思いますが、これからも活発に活動して、わかりやすい物を投稿して行けたらと思っていますので、よろしくお願いします。
また、自作アプリを作成しましたので、見ていただけるとありがたいです。
みんたま　：　http://mintama.work
github　：　https://github.com/nakar0/mintama