Help us understand the problem. What is going on with this article?

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

たぶん、長い投稿

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

出来たもの

キャプチャ.JPG

作成の過程で収穫物

  • Rails5.2での追加分(Active Record Storage含む
  • Twitter Login方法と仕組み、そのたTwitterあれこれ
  • JSの基礎(getElementByIdやsetAttribute、文字カウントなど
  • AWS S3の使い方
  • XSS対策

未だ残る改修すべき箇所

  • 検索結果画面のリダイレクトエラー(多分route.rbの書き順番由来
  • js辺りのエラー(動いてるけど、consoleではjs/mapのルーティングが何とか
  • スマホで
    タグ等を打つの面倒なので、なにか投稿補助ボタンでも
  • 本来の目的をよく考えたら、マークダウンの方は不要なのでは。
  • 作成要件

    Untitled.png

    • マークダウン投稿、シンタックスハイライト
      • gem: redcarpet, rouge(結局syntax-hightlightだけは反映されないまま
    • 投稿から画像生成
    • AWS S3にog:image用の画像を保存

    作成の流れ:予定

    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

    今回は公開にまで至る予定なので、railsやdeviseの日本語化等も。が、想定ユーザはエンジニアだしと思い、殆ど英語になった。

    Gemfile
    gem 'mini_racer' # uncomment
    gem 'rails-i18n' # japanize
    
    # authentication
    gem 'devise' # login
    gem 'omniauth' # SNS login
    gem 'omniauth-twitter' # twitter login
    gem 'devise-i18n' # japanize devise
    gem 'devise-i18n-views'
    
    gem 'redcarpet' # for markdown
    gem 'rouge' # for syntax highlight
    
    gem 'meta-tags'
    
    gem 'aws-sdk-s3' # for aws s3
    

    kpumuk/meta-tags:Search Engine Optimization (SEO) for Ruby on Rails applications.は割愛。

    gitignore => rails.credentials.yml

    当初は.gitignoregem 'dotenv'等を使っていた。が、作成途中でRails5.2からのrails.credentials.ymlを利用した。復号化には/config/master.keyを利用。

    terminal
    # 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あれこれ

    terminal
    # devise
    # install 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
    
    terminal
    # scaffold post
    rails g scaffold Post user:references name:string content:text date:datetime
    

    Active Record Associations関連付け

    /app/model/user.rb
    has_many :posts
    
    /app/model/post.rb
    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)  # #や##等がh2、h3となるようにした。
          level += 1
          "<h#{level}>#{text}</h#{level}>"
        end
      end
    
      def markdown(text)
        render_options = {
          filter_html: true,  # do not allow any user-inputted HTML in the output.
          hard_wrap: true,
        }
    
        extensions = {
          autolink: true, # <>で囲まれていない時は、リンクとして認識しない
          fenced_code_blocks: true,   #  ```\n ```内をコード部分と見做す
          lax_spacing: true, 
          no_intra_emphasis: true,
          strikethrough: true,
          superscript: true,
          tables: false,  # テーブルを認識しない
          highlight: true,
          disable_indented_code_blocks: true,
          space_after_headers: false # #の後にスペースが無くても、h1等とする。
        }
        renderer = RougeRedcarpetRenderer.new(render_options)
        Redcarpet::Markdown.new(renderer, extensions).render(text).html_safe
      end
    end
    

    html_safe => sanitize

    html_safeではXSS対策としては駄目と知った。名前詐欺である。
    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への画像保存

    最初はTwitterAPIを利用して、投稿から作成、DBに直接保存した画像でTwitter投稿しようとした。だが、Herokuでは画像が保持されない事、TwitterAPIの変更などいろいろ面倒なことが発生したので、最終的には画像を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を使った。

    terminal
    # set up
    rails active_storage:install
    # 今回は画像が紐づくPostテーブルが既にあるので、不要
    # 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

    参考:htmlを画像化する方法(html2canvasの使い方)
    jsはProgateレベルだったので、DOM操作は初めてで、なんか楽しかったぞ。

    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デコード

    大学で画像処理していたとはいえ、 Base64とは?Blobとは?となり、良い機会だった。

    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 button

    公式で生成されるTweetボタンのURLを利用し、押下時にwindow.openでTweet投稿ページを開くようにした。rubyonrailsで用意した変数をjsに渡すgem 'gon'も考えたが、見送った。

    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'を使用。参照 : kpumuk/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が必要。割愛。

    なお、omniauthは脆弱性が見つかっており、githubの方でもアラートが来るのだが、パッチが無いのだが。クックパッドの人が対処してくれたので、感謝したい。

    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

    今回はBootstrapを部分的に使用した。cssの優先順位など収穫があった。

    改修(加筆

    メディアクエリ

    想定ユーザは殆どスマホなのに、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で表示してくれたら全て済むんじゃと思った。
    因みにもう1段階先のWebアプリを考えてあるけど、たぶんjsの知識が足りないので、今は無理。
    転職活動中の無職です。働きたい。。。。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした