Twitter に見栄え良くコード投稿したかった

/

from Qiita: Twitterにコードを身えばよく投稿したい

はじめに

きっかけ(こんな呟きを見かけた

twitter

できたもの

作成の過程で収穫物

  • ActiveRecord Storage などの Rails 5.2
  • Twitter Login 方法と仕組みなど
  • JS の基礎(getElementById や setAttribute、文字カウントなど)
  • AWS S3 関連
  • XSS 対策

作成の前に

作成要件

Image from Gyazo

作成の流れ:予定

  1. rails new codr, Git init, heroku create、Active Storage
  2. AWS S3 あれこれ
  3. Twitter 登録、ログイン機能作成

開発環境

  • vm : Linux Ubuntu (virtualbox + vagrant)
    • Ruby 2.5.1p57
    • Rails 5.2.3
    • Postgresql

実作業

undefined
rails new codr -d postgresql

DB 設定等は割愛

Gem

Gemfile
gem 'mini_racer'
gem 'rails-i18n'
gem 'devise'
gem 'omniauth'
gem 'omniauth-twitter' # twitter login
gem 'devise-i18n' # make devise japanize
gem 'devise-i18n-views'
gem 'redcarpet' # markdown processor
gem 'rouge' # highlighter
gem 'meta-tags'
gem 'aws-sdk-s3' # aws s3

rails.credentials.yml

当初は.gitignoredotenv などを使っていたが、作成途中で Rails 5.2 からの rails.credentials.yml を利用した。復号化には /config/master.key を利用。

undefined
# editor setting
EDITOR="vim" rails credentials:edit
# edit credentials.yml
rails credentials:edit
# show credential.yml
rails credentials.yml:show
# herokuにmaster.keyを環境変数として指定
# heroku config:set ENV_VAR="環境変数" --app "アプリ名"
# 追加した変数を使用するには
Rails.application.credentials.dig(:twitter, :API_Key)

rails gあれこれ

undefined
# devise
rails g devise:install
rails g devise User name:String
# Add Admin column to User
rails g migration AddAdminToUsers
# add setting at /db/migrate/20191103141531_add_admin_to_users.rb
add_column :users, :admin, :boolean, default: false
# add views and controllers to modify devise
rails g devise:controllers users
rails g devise:views users
# japanize
# add at /config/application.rb
config.i18n.default_locale = :ja
=> create /config/locale/devise.view.ja.yml
undefined
# scaffold post
rails g scaffold Post user:references name:string content:text date:datetime

ActiveRecord Associations関連付け

/app/model
# user
has_many :posts
# post
belongs_to :user

投稿関連

マークダウン投稿

  • 参照:Redcarpet:Github

  • 基本:Redcarpet::Markdown.new(renderer, extensions = {}).render(@post.content) オプションや XSS 対策などを追加したく、helper メソッドを作成した。

app/helpers/posts_helper.rb
Module PostsHelper
require 'rouge/plugins/redcarpet'
class RougeRedcarpetRenderer < Redcarpet::Render::HTML
include Rouge::Plugins::Redcarpet
def header(text, level)
# make # => h2, ## => h3
level += 1
"<h#{level}>#{text}</h#{level}>"
end
end
def markdown(text)
render_options = {
# do not allow any user-inputted HTML in the output.
filter_html: true,
hard_wrap: true,
}
extensions = {
# <>で囲まれていない時は、リンクとして認識しない
autolink: true,
# ```/m```をコードとする
fenced_code_blocks: true,
lax_spacing: true,
no_intra_emphasis: true,
strikethrough: true,
superscript: true,
tables: false,
highlight: true,
disable_indented_code_blocks: true,
# #の後にスペースが無くても良いか
space_after_headers: false
}
renderer = RougeRedcarpetRenderer.new(render_options)
Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
end
end

html_safe => sanitize

ホワイトリスト方式の sanitizeヘルパー を使用した。

app/views/posts/index.html.erb
# sanitize(html, options = {})
<div id="capture" class="content">
<%= sanitize(markdown(@post.content), tags: %w(div img h1 h2 h3 h4 h5 strong em a p pre code ), attributes: %w(class href)) %>
</div>

投稿内容のデータ化、AWSへの画像保存

Heroku では画像保持がされないので、作成画像を AWS S3 に保存し、og:image に添付する形を取った。

  1. Web アプリ内で通常投稿
  2. show ページ表示(同時に html2canvas で Base64 としてデータ取得、hidden_field に格納)
  3. ツイートボタン押す(Post され、post モデル内で base64 をデコード)
  4. Active Storage を通して、AWS S3 に保存

Active Storage

Rail5.2 からの機能で、今までの carrievave や paperclip などを使わずに、クラウドストレージ等へのアップロードが容易になる。今回は AWS S3 を使った。

undefined
# set up
rails active_storage:install
# rails g resource comment content:text
rails db:migrate
app/models/post.rb
class Post < ApplicationRecord
# 今回は1つの投稿につき、1枚の画像なので。
# 複数なら => has_many_attached :prtscs
has_one_attached :prtsc
end
app/config/environments/
# ファイル保存先変更
# development.rb
config.active_storage.service = :local
# production.rb
config.active_storage.service = :amazon

rails credentials:edit で AWS アクセスキーとシークレットキーを追加。

config/credentials.yml.enc
aws:
access_key_id:
secret_access_key:
config/storage.yml
test:
service: Disk
root: <%= Rails.root.join("tmp/storage") %>
local:
service: Disk
root: <%= Rails.root.join("storage") %>
amazon:
service: S3
access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
region: ap-northeast-1
bucket: codr0
undefined
# Gemfile
# gemが必要
gem 'aws-sdk-s3', require: false
# 今回は不要だったので、入れず。
gem 'mini_magick'

html2canvas

  1. ツイートボタン押下時に画像を Post するためのフォーム hidden_field を用意
  2. html2canvas.jsapp/assets/javascripts ディレクトリ配下に保存
  3. html 上に置く script コードを改修
app/views/posts/show.html.erb
<%= form_with(model: @post, local: true) do |form| %>
<%= form.hidden_field :id, value: @post.id %>
<%= form.hidden_field :prtsc, value: "" %> # idはpost_prtscになる。
<%= form.submit "Post", class:"btn btn-outline-dark", id:"tweet", value:"tweet" %>
<% end %>
app/views/layouts/application.html.erb
<script type="text/javascript">
html2canvas(document.querySelector("#capture"),{scale:1, width:600}).then(canvas => {
var base64 = canvas.toDataURL('image/jpeg', 1.0);
document.getElementById('post_prtsc').setAttribute('value', base64);
});
</script>

Base64デコード

app/models/post.rb
attr_accessor :img
def parse_base64(img)
if img.present?
# data:image/jpeg;base64,/9j/4AAQSkZJRgABA・・・から/9j/4AA以降を選択取得
content = img.split(',')[1]
# 今回は、ユーザによる画像アップロード投稿ではなく、拡張子が決まっている
filename = Time.zone.now.to_s + '.jpg'
decoded_data = Base64.decode64(content)
# String.IO.newにより、アプリ内に一時ファイルを作成しなくて済む
prtsc.attach(io: StringIO.new(decoded_data), filename: filename)
end
end

あとは posts_controller で、params から受け取った Base64 データを上の parse_base64(img) で変換し、保存すれば完了。

AWS S3

AWS 上での登録、設定、バケット作成等は割愛。

ツイート Share Button

app/views/layouts/application.html.erb
<script>
var base = 'https://twitter.com/intent/tweet?url=';
var pageUrl = 'https://codr0.herokuapp.com/posts/' + document.getElementById('post_id').value;
var option = '&button_hashtag=Codr0&ref_src=twsrc%5Etfw';
var href = base + pageUrl + option;
var twit = document.getElementById('tweet');
twit.addEventListener('click', function() {
window.open( href );
});
</script>

og:imageに画像添付

なお、head の meta 情報セットには、gem 'meta-tags' を使用

service_url()とurl_for()

基本的にはどちらも、ActiveStorage に保存したデータの Url を取得するメソッドの様だ。 どちらもセキュリティのためにリンクの有効期限が短いみたいだが、違いがわからなかった。今回はツイートボタン押下し、Tweet した際に og:image として表示されればいい。

app/views/posts/show.html.erb
# 画像がActive StorageでAWS S3に保存されて入れば
<% if @post.prtsc.attached? %>
<% set_meta_tags og:{image: @post.prtsc.service_url} %>
<% end %>

Twitterログイン

TwitterDeveloperAccount が必要。割愛。

app/models/user.rb
# 参考ページと同じ基礎的な所は割愛する。
class User < ApplicationRecord
def self.from_omniauth(auth)
find_or_create_by!(provider: auth['provider'], uid: auth['uid']) do |user|
# 一部割愛
user.username = auth['info']['nickname']
# SNS登録時は、ダミーメールを登録
user.email = User.dummy_email(auth)
end
end
# SNS登録(providerが存在する)時は、パスワード要求をしない
def password_required?
super && provider.blank?
end
def self.new_with_session(params, session)
if session['devise.user_attributes']
new(session['devise.user_attributes']) do |user|
user.attributes = params
end
else
super
end
end
private
def self.dummy_email(auth)
"#{auth.uid}-#{auth.provider}@example.com"
end
end

Twitter のニックネームが取得できるようになったので、元からある User の name テーブルは削除した。

改修(加筆

メディアクエリ

想定ユーザーは殆どスマートフォンなのに、パソコンで作成し、CSS をパソコンの見た目でやってた。折角 SCSS でやってるので、変数を利用した。

app/assets/stylesheets/scaffold.scss
// ディスプレイサイズが680pxまでなら。
$tab: 680px;
@mixin tab {
@media (max-width: ($tab)) {
@content;
}
}

最後に

gist などがコードスクショを og:image で表示してくれたらすべて済むのでは