LoginSignup
2
1

More than 5 years have passed since last update.

Mastodonで性的な画像などをNSFWできるようにした

Last updated at Posted at 2018-10-24

はじめに

Mastodon界隈で性的な画像に対してNSFWをつけないで投稿するという行為がたびたび物議を醸していたので、その辺りをよしなにできないかためしてみた

実際のデモ動画

やったこと

Google Cloud Vision API の準備

こちらを参考にGoogle Cloud Vision API のキーなどを取得

Google Cloud Vision API を使う準備をする

Google Cloud VisionをRubyから叩いてみる

あとは、APIキーなどをjson形式で取得してSSHやWebコンソールなどで.env.productionと同じ位置にkey.jsonという名前で配置する

Mastodon側の実装

まず、app/controllers/api/v1/statuses_controller.rbを以下のように変更

app/controllers/api/v1/statuses_controller.rb
require "google/cloud/vision"
require "json"

# frozen_string_literal: true

class Api::V1::StatusesController < Api::BaseController
  include Authorization

  before_action -> { authorize_if_got_token! :read, :'read:statuses' }, except: [:create, :destroy]
  before_action -> { doorkeeper_authorize! :write, :'write:statuses' }, only:   [:create, :destroy]
  before_action :require_user!, except:  [:show, :context, :card]
  before_action :set_status, only:       [:show, :context, :card]

  respond_to :json

  # This API was originally unlimited, pagination cannot be introduced without
  # breaking backwards-compatibility. Arbitrarily high number to cover most
  # conversations as quasi-unlimited, it would be too much work to render more
  # than this anyway
  CONTEXT_LIMIT = 4_096

  def show
    @status = cache_collection([@status], Status).first
    render json: @status, serializer: REST::StatusSerializer
  end

  def context
    ancestors_results   = @status.in_reply_to_id.nil? ? [] : @status.ancestors(CONTEXT_LIMIT, current_account)
    descendants_results = @status.descendants(CONTEXT_LIMIT, current_account)
    loaded_ancestors    = cache_collection(ancestors_results, Status)
    loaded_descendants  = cache_collection(descendants_results, Status)

    @context = Context.new(ancestors: loaded_ancestors, descendants: loaded_descendants)
    statuses = [@status] + @context.ancestors + @context.descendants

    render json: @context, serializer: REST::ContextSerializer, relationships: StatusRelationshipsPresenter.new(statuses, current_user&.account_id)
  end

  def card
    @card = @status.preview_cards.first

    if @card.nil?
      render_empty
    else
      render json: @card, serializer: REST::PreviewCardSerializer
    end
  end

  def create
    @status = PostStatusService.new.call(current_user.account,
                                         status_params[:status],
                                         status_params[:in_reply_to_id].blank? ? nil : Status.find(status_params[:in_reply_to_id]),
                                         media_ids: status_params[:media_ids],
                                         sensitive: check_nsfw(set_image_path),
                                         spoiler_text: status_params[:spoiler_text],
                                         visibility: status_params[:visibility],
                                         application: doorkeeper_token.application,
                                         idempotency: request.headers['Idempotency-Key'])

    render json: @status, serializer: REST::StatusSerializer
  end

  def destroy
    @status = Status.where(account_id: current_user.account).find(params[:id])
    authorize @status, :destroy?

    RemovalWorker.perform_async(@status.id)

    render_empty
  end

  private

  def set_status
    @status = Status.find(params[:id])
    authorize @status, :show?
  rescue Mastodon::NotPermittedError
    # Reraise in order to get a 404 instead of a 403 error code
    raise ActiveRecord::RecordNotFound
  end

  def status_params
    params.permit(:status, :in_reply_to_id, :sensitive, :spoiler_text, :visibility, media_ids: [])
  end

  def pagination_params(core_params)
    params.slice(:limit).permit(:limit).merge(core_params)
  end

  def set_image_path
    paths = Array.new

    if status_params[:media_ids].class != nil.class
      status_params[:media_ids].each do |id|

        image = MediaAttachment.find(id)
        if ENV['S3_REGION'].to_s != "" then
          path =  "0" * (9 - image.id.to_s.size) + image.id.to_s
          ps = "#{path[0] + path[1] + path[2]}/#{path[3] + path[4] + path[5]}/#{path[6] + path[7] + path[8]}/original/#{image.file_file_name.to_s}"
          puts paths.push("https://s3-#{ENV['S3_REGION'].to_s}.amazonaws.com/#{ENV['S3_BUCKET']}/media_attachments/files/#{ps}")
        else
          path =  "0" * (9 - image.id.to_s.size) + image.id.to_s
          ps = "#{path[0] + path[1] + path[2]}/#{path[3] + path[4] + path[5]}/#{path[6] + path[7] + path[8]}/original/#{image.file_file_name.to_s}"
          puts paths.push("public/system/media_attachments/files/#{ps}")
        end
      end
    end

    return paths
  end

  def check_nsfw(paths)
    keys = JSON.parse(File.open("./key.json").read).to_h

    Dotenv.load

    vision = Google::Cloud::Vision.new project: keys["project_id"]

    paths.each do |path|

      if path.to_s =~ /.jpg|.jpeg|.png/
        response = vision.image(path.to_s)

        res = response.safe_search

        if res.adult? || res.violence? || res.medical? then
          return true
        end
      end
    end
    return false
  end
end

set_image_pathメソッドとcheck_nsfwメソッドを追加し、createメソッドの引数をsensitive: check_nsfw(set_image_path)と変更するだけ

あとは、Gemfileに以下のように追加するだけ

gem 'google-cloud-vision'
gem 'json

これで自動NSFWがMastodonに実装されます。

参考文献など

Google Cloud Vision API を使う準備をする

Google Cloud VisionをRubyから叩いてみる

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1