Twitterのプロフィール欄に、このページに乗っているスコアや順位を自動的に取得して、自動的にそれを記入・更新し続けてくれるというアプリです。
知り合いには、プロフィール欄に日本何位とか世界何位とかを載せている人が多いのですが、手動でやる以上「ピーク何位」的な表現になっている人が多いので、リアルタイムに連動させられたら便利ではなかろうかという考えで作りました。個人的にも、Twitter認証とかは初めての試みだったので、Webアプリ制作の経験にもなりました。
言語とフレームワーク
プログラマというのはひな鳥🐣と一緒で最初の覚えた言語を親だと思うのです(引用)。私にとってはPython🐍が親なので当然それを選びます。また、WebフレームワークはDjango、サーバーはHerokuを使います。
OAuth認証
今回のアプリのキモとなる部分がOAuth認証です。他人のTwitterのプロフィールを変えさせてもらうので、一部権限を頂かなければなりません。まず、Twitter Developer Portal[(https://developer.twitter.com/en/portal/dashboard) で、アプリの権限設定をします。
これを「Read and Write」にすることにより、認証をする際に多くの権限(アクセストークン)をもらうことができます。ちょっと問題なのが、これだと
- プロフィールの変更
- ツイートの送信・削除
- フォロー/リムーブ
- ミュート、ブロック
と色々なことができてしまいますが、今回実現したいのは上の「プロフィールの変更」だけで、それ以外は明らかに過剰な権限です。特にミュート、ブロックあたりは扱いを間違えたら、確実に人間関係にトラブルをもたらします(逆にこれを使うのって、どんなアプリだ......?)。よく知らないアプリからいきなりこれだけ要求されたら、誰だって身構えます。
結論的には、これはどうしようもありません。プロフィールを変更させていただく以上、それ以外の権限もセットでいただかなくてはならないのです。仕方ないので、連携画面に注意書きをつけて、後は信用してもらうしかないという形になりました。ルールではなく信用でセキュリティを成り立たせるの、非常にプログラム的でない......。
コールバックURL
認証画面にはコールバックURLというものが必要らしいです。今回のアプリ作成にあたってはDjangoでTwitter認証アプリケーションを作成する[2018/06最新版]という神記事に色々助けられました。というか、ほぼここのコードをまるまる流用させてもらっています。感謝。ただ、プログラムの常でバージョンの違いのせいか、ルーティングの記述が変わっています(リンク先のコメントでも言及されていますが)。結果的には、ルーティングは以下のようになります。
import django.contrib.auth.views
from django.urls import path
from . import views
app_name = 'user_auth'
urlpatterns = [
path('',views.top_page, name = "default"), #これはホームURLでもページを表示するための改造
path('top/',views.top_page, name="top"),
path('login/', # ログイン
django.contrib.auth.views.LoginView.as_view(
template_name='user_auth/login.html'),
name='login'),
path('logout/', # ログアウト
django.contrib.auth.views.LogoutView.as_view(
template_name='user_auth/login.html'),
name='logout'),
]
今更ながら、各モジュールのバージョンは以下になります。
dj-database-url==0.5.0
Django==3.1
django-appconf==1.0.3
django-bootstrap4==1.1.1
django-filter==2.3.0
django-fontawesome==1.0
django-heroku==0.3.1
django-imagekit==4.0.2
djangorestframework==3.11.1
requests_oauthlib 1.3.0
requests==2.22.0
requests-oauthlib==1.3.0
requests-toolbelt==0.9.1
コールバックURLですが、Twitter開発者サイトで以下の部分に設定する必要があります。
Djangoでは、ローカルサーバーのアドレスが、http://127.0.0.1:8000/
になるので、
http://127:0.0.1:8000/user/compelete/twitter/
を打ち込みます。
ただ、今回本番環境にあげてみてわかったことですが、本番環境ではコールバックアドレスを変える必要があります。アプリの本番稼働時のホームアドレスはhttps://scoresaber-profile.herokuapp.com/
になるので、これを
https://scoresaber-profile.herokuapp.com/user/complete/twitter/
に変えます。これ、開発環境と本番環境でいちいちブラウザ上で手動で変えるのが非常にデバッグしづらいのですが、よりよいプラクティスがあるのかもしれません。
あと、参考にさせてもらったコードでは、ログインボタンのリンク先が、
<button type="button" onclick="location.href='{% url 'social:begin' 'twitter' %}'">Twitterログイン</button>
となっているのですが、これだとなぜか本番環境で500サーバーエラーが出てうまくいきませんでした。
結果的には、本番環境ではリンク先を強制的に
<button type="button" onclick="location.href='https://scoresaber-profile.herokuapp.com/user/login/twitter/'">Twitterログイン</button>
にすることで一応解決しました。恐らくこのせいで、認証済のユーザーがアクセスした場合でも毎回認証処理が入ってしまうようになりましたが、まあ大体のユーザーは1回認証するだけなので、これでよしとしました。
内部プログラム
本アプリのロジックは、
- ユーザーのプロフィールからIDと該当ページを取得
- 該当ページから置換文字列を取得して置換
- 置換後の文字列をプロフィールに再設定
というものになります。
1.のID取得は、アプリ側にユーザーページを設けて自分で設定する方式も考えましたが、実装がめんどうくさいので なるべくTwitter内だけでUIを完結させるために、プロフィール内にURLを書いてもらう方式にしました。API側でURLを探索するプログラムは以下になります。
twitter = OAuth1Session(CK, CKS, AT, ATS)
url = "https://api.twitter.com/1.1/account/verify_credentials.json"
res = twitter.get(url)
if res.status_code != 200:
if res.status_code == 401: #連携解除されたユーザーはリストから削除
user.delete()
continue
j = res.json()
orig_desc = j['description']
orig_loc = j['location']
orig_name = j['name']
sid = ''
all_urls = []
ent = j['entities']
if 'url' in ent:
all_urls.extend(ent['url']['urls'])
if 'description' in ent:
all_urls.extend(ent['description']['urls'])
for a in all_urls:
url = a['expanded_url']
if 'scoresaber' in url:
pat = r'scoresaber.com/u/(.*)'
res = re.findall(pat, url)[0]
sid = res
break
ユーザーのアクセストークンを元に、https://api.twitter.com/1.1/account/verify_credentials.json
から情報をもらいます。entities
の中にURLがあり、プロフィールのWebサイトに設定したURLをはその中にあるurl
に、プロフィール本文内にあるurlはdescription
にあります(ややこしい)。このapiに対する詳しい解説はここにあります。
2.と3.については特につまる部分はなかったのですが、1点諦めたことが。プロフィールを書き換える時に、最初に生成する時(locallank
-> 🇯🇵#XXX
)はユニークな変換ができるのですが、それを更新する時は🇯🇵#XXX
という文字列を正規表現で探して更新先とする、という処理を取りました。このため、生成されたプロフィール文によっては複数の文字列を更新先と見なしたり、正しい更新先が特定できないという問題があります。あまりない事例だとは思うので放置しましたが、ここら辺もっとスマートな実装があるかもしれません。
あと、今回初めて知ったのですが、ユニコードの国旗は、特定のアルファベット絵文字が2文字並んだ時にその国名コードを持つ国を国旗として表示するという仕組みみたいです。これ、OSやブラウザによってはそのままアルファベットが2文字並んでしまうので、非常にやりづらい……。幸い、該当ページから得られる国コードをそのままユニコードの絵文字アルファベットに並べ替えるだけで処理できました。
def emojinate(s):
first = ord(s[0])-ord('a')
second = ord(s[1])-ord('a')
return chr(int(0x1F1E6)+first) + chr(int(0x1F1E6)+second)
後は、更新後のプロフィールを以下のapiに投げつけて完了です。
url = "https://api.twitter.com/1.1/account/update_profile.json"
params = {'description': new_desc, 'location': new_loc, 'name': new_name}
twitter.post(url, params=params)
デプロイ
ここら辺は、前々回、前回となかなか格闘した覚えがあるので、慣れたものです。今回も、作っている中で、過去の記事を参照して「対策をまとめた過去の自分、優秀か......?」となったので、今回の記事もいつか未来の自分か誰かを救うでしょう、多分。
変わった部分としては、HerokuがサポートしているPythonのバージョンが合わなかったので、デプロイ時に警告を食らいました。ここら辺は公式のここを見ましょう。
また、謎のつまづきポイントで、デプロイ時に、
push failed: can not parse Procfile
というエラーメッセージが出て、何度文面を見直しても、間違えてないよな......? となって混乱したのですが、結果的にはProcfile
の文字エンコーディングがUTF-8
になっていないのが原因でした。
requirements.txt
も、django_heroku
だけではなくて、
requests_oauthlib
social-auth-app-django
を加えなければいけないあたりも注意ポイントでしょうか。まあ、ここら辺もデプロイ時にコケるならエラーで教えてくれるとは思いますが。
それぐらいで、ここら辺は概ねスムーズに完了。デプロイして、スケジューラーで更新プログラムを定期的に走らせるようにしたアプリの実装完了です。実際にデプロイした後、上に述べたコールバックやログインURLの問題がありましたが、とりあえず現在のところは安定稼働しています。