Created code share app for Twitter with RubyonRails

from Qiita: Twitterにコードを身えばよく投稿したい
はじめに
きっかけ(こんな呟きを見かけた
出来たもの
作成の過程で収穫物
- Active Record Storage等のRails5.2
- Twitter Login方法と仕組みなど
- JSの基礎(getElementByIdやsetAttribute、文字カウントなど
- AWS S3関連
- XSS対策
作成の前に
作成要件

- マークダウン投稿、シンタックスハイライト
- gem: redcarpet, rouge
- 投稿から画像生成
- html2canvas
- 参照:html2canvas
- 参照:JSでhtmlを画像化する方法(html2canvasの使い方) from 湧くべく
- AWS S3にog:image用の画像を保存
作成の流れ:予定
- rails new codr, git init, heroku create、Active Storage
- AWS S3あれこれ
- twitter登録、ログイン機能作成
開発環境
- vm : Linux Ubuntu (virtualbox + vagrant)
- Ruby 2.5.1p57
- Rails 5.2.3
- Postgresql
実作業
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
- 参照: kpumuk/meta-tags
rails.credentials.yml
当初は.gitignore
とdotenv
等を使っていたが、作成途中でRails5.2からのrails.credentials.yml
を利用した。復号化には/config/master.key
を利用。
# 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あれこれ
# 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
# scaffold post
rails g scaffold Post user:references name:string content:text date:datetime
Active Record Associations関連付け
# /app/model/
# user
has_many :posts
# post
belongs_to :user
投稿関連
マークダウン投稿
基本: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に添付する形を取った。
- Webアプリ内で通常投稿
- showページ表示(同時にhtml2canvasでBase64としてデータ取得、hidden_fieldに格納
- Tweetボタン押す(Postされ、postモデル内でbase64をデコード
- Active Storageを通して、AWS S3に保存
Active Storage
Rail5.2からの機能で、今までのcarrievaveやpaperclip等を使わずに、クラウドストレージ等へのアップロードが容易になる。今回はAWS S3を使った。
- 参照: Active Storage
# 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
# Gemfile
# gemが必要
gem 'aws-sdk-s3', require: false
# 今回は不要だったので、入れず。
gem 'mini_magick'
html2canvas
- Tweetボタン押下時に画像をPostするためのフォーム
hidden_field
を用意 html2canvas.js
をapp/assets/javascripts
ディレクトリ配下に保存。- 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
- refferrence
AWS上での登録、設定、バケット作成等は割愛。
Tweet 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ボタン押下し、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が必要。割愛。
- 参照
- gem 'omniauth-twitter' github
- deviseの使い方(rails5版)
- [ominiauth脆弱性に対するクックパドによるパッチ]](https://github.com/cookpad/omniauth-rails_csrf_protection)
# 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テーブルは削除した。
改修(加筆
メディアクエリ
想定ユーザは殆どスマホなのに、PCで作成し、CSSをPCの見た目でやってた。折角SCSSでやってるので、変数を利用した。
# app/assets/stylesheets/scaffold.scss
// ディスプレイサイズが680pxまでなら。
$tab: 680px;
@mixin tab {
@media (max-width: ($tab)) {
@content;
}
}
// .box {
// @include tab {
// background-color: blue;
// };
// }
最後に
gist等がコードスクショをog:imageで表示してくれたら全て済むのでは