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

post on
post cover image

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

はじめに

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

出来たもの

作成の過程で収穫物

  • Active Record Storage等のRails5.2
  • Twitter Login方法と仕組みなど
  • JSの基礎(getElementByIdやsetAttribute、文字カウントなど
  • AWS S3関連
  • XSS対策

作成の前に

作成要件

table

作成の流れ:予定

  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

実作業

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等を使っていたが、作成途中で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に添付する形を取った。

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

Active Storage

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

# 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

  1. Tweetボタン押下時に画像を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上での登録、設定、バケット作成等は割愛。

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が必要。割愛。

# 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で表示してくれたら全て済むのでは