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

/
#rails

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
1
rails new codr -d postgresql

DB 設定等は割愛

Gem

Gemfile
1
gem 'mini_racer'
2
gem 'rails-i18n'
3
4
gem 'devise'
5
gem 'omniauth'
6
gem 'omniauth-twitter' # twitter login
7
gem 'devise-i18n' # make devise japanize
8
gem 'devise-i18n-views'
9
10
gem 'redcarpet' # markdown processor
11
gem 'rouge' # highlighter
12
13
gem 'meta-tags'
14
15
gem 'aws-sdk-s3' # aws s3

rails.credentials.yml

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

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

rails gあれこれ

undefined
1
# devise
2
rails g devise:install
3
rails g devise User name:String
4
5
# Add Admin column to User
6
rails g migration AddAdminToUsers
7
# add setting at /db/migrate/20191103141531_add_admin_to_users.rb
8
add_column :users, :admin, :boolean, default: false
9
10
# add views and controllers to modify devise
11
rails g devise:controllers users
12
rails g devise:views users
13
14
# japanize
15
# add at /config/application.rb
16
config.i18n.default_locale = :ja
17
=> create /config/locale/devise.view.ja.yml
undefined
1
# scaffold post
2
rails g scaffold Post user:references name:string content:text date:datetime

ActiveRecord Associations関連付け

/app/model
1
# user
2
has_many :posts
3
4
# post
5
belongs_to :user

投稿関連

マークダウン投稿

  • 参照:Redcarpet:Github

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

app/helpers/posts_helper.rb
1
Module PostsHelper
2
require 'rouge/plugins/redcarpet'
3
class RougeRedcarpetRenderer < Redcarpet::Render::HTML
4
include Rouge::Plugins::Redcarpet
5
6
def header(text, level)
7
# make # => h2, ## => h3
8
level += 1
9
"<h#{level}>#{text}</h#{level}>"
10
end
11
end
12
13
def markdown(text)
14
render_options = {
15
# do not allow any user-inputted HTML in the output.
16
filter_html: true,
17
hard_wrap: true,
18
}
19
20
extensions = {
21
# <>で囲まれていない時は、リンクとして認識しない
22
autolink: true,
23
# ```/m```をコードとする
24
fenced_code_blocks: true,
25
lax_spacing: true,
26
no_intra_emphasis: true,
27
strikethrough: true,
28
superscript: true,
29
tables: false,
30
highlight: true,
31
disable_indented_code_blocks: true,
32
# #の後にスペースが無くても良いか
33
space_after_headers: false
34
}
35
renderer = RougeRedcarpetRenderer.new(render_options)
36
Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
37
end
38
end

html_safe => sanitize

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

app/views/posts/index.html.erb
1
# sanitize(html, options = {})
2
<div id="capture" class="content">
3
<%= sanitize(markdown(@post.content), tags: %w(div img h1 h2 h3 h4 h5 strong em a p pre code ), attributes: %w(class href)) %>
4
</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
1
# set up
2
rails active_storage:install
3
# rails g resource comment content:text
4
rails db:migrate
app/models/post.rb
1
class Post < ApplicationRecord
2
# 今回は1つの投稿につき、1枚の画像なので。
3
# 複数なら => has_many_attached :prtscs
4
has_one_attached :prtsc
5
end
app/config/environments/
1
# ファイル保存先変更
2
# development.rb
3
config.active_storage.service = :local
4
# production.rb
5
config.active_storage.service = :amazon

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

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

html2canvas

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

Base64デコード

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

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

AWS S3

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

ツイート Share Button

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

og:imageに画像添付

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

service_url()とurl_for()

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

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

Twitterログイン

TwitterDeveloperAccount が必要。割愛。

app/models/user.rb
1
# 参考ページと同じ基礎的な所は割愛する。
2
class User < ApplicationRecord
3
def self.from_omniauth(auth)
4
find_or_create_by!(provider: auth['provider'], uid: auth['uid']) do |user|
5
# 一部割愛
6
user.username = auth['info']['nickname']
7
# SNS登録時は、ダミーメールを登録
8
user.email = User.dummy_email(auth)
9
end
10
end
11
12
# SNS登録(providerが存在する)時は、パスワード要求をしない
13
def password_required?
14
super && provider.blank?
15
end
16
17
def self.new_with_session(params, session)
18
if session['devise.user_attributes']
19
new(session['devise.user_attributes']) do |user|
20
user.attributes = params
21
end
22
else
23
super
24
end
25
end
26
27
private
28
29
def self.dummy_email(auth)
30
"#{auth.uid}-#{auth.provider}@example.com"
31
end
32
end

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

改修(加筆

メディアクエリ

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

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

最後に

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