Railsアプリでイタズラ投稿(NGワード)と格闘してみた話

# 前提

そうだ、ポートフォリオアプリの開発記録忘れる前に記事にしときたいな...
➡︎ Qiitaに投稿
[30代元食品工場長がフードシェアサービスのポートフォリオを約2週間で開発した話](https://qiita.com/y_h_tomo/items/1c88ffeb724e893730d6)
➡︎ 何故かLGTMたくさん頂けた
**（技術参考になるかも怪しい記事にLGTMありがとうございます！）**
➡︎ ポートフォリオアプリにイタズラ投稿発生

- 「ああああああ」
- 「（NGワードなどなど）」

➡︎ なるほど...これは考えてなかった。勉強になるな...（現在）

# 環境

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/802059/f32df107-66fe-7658-be29-0720d8004503.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/802059/ac900d6e-cc95-3252-63fd-b34126db1114.png)

# 課題の確認と対策想定

1. 「あああああ...」など同じ文字連続への対応　➡︎ モデルへのバリデーション追加でいけそう⭕️
2. 下ネタやNGワードへの対応　➡︎ モデルへのバリデーション追加かgemで対応できるかも⭕️
3. 問題ある画像への対応　➡︎ 技術的に無理✖️
4. 悪質ユーザーが登録することへの対応　➡︎ Email対策してもいたちごっこだと予想して諦め✖️

# 対策実施

#### 変更前

```ruby:models/item.rb

#item（商品管理）モデル
  #アソシエーション関連と追加メソッドは省略。
  #変更がわかりやすいよう、あえて元々のコードとは異なるカラムの順番にしました。
  with_options presence: true do  
    validates :name, length: { maximum: 40 }
    validates :description, length: { maximum: 200 }
    validates :contact_location
    validates :quantity, numericality: { greater_than: -1, less_than: 100 }
    validates :deadline
    validates :price, format: { with: /\A[0-9]+\z/ }, numericality: { less_than: 1_000_000 }
    validates :image
  end
  with_options numericality: { other_than: 1 } do
    validates :prefecture_id
    validates :category_id
    validates :condition_id
  end
```

バリデーション知らない方に説明すると、データ入力の際に、下記のような制限をかけてます。

```ruby
with_options presence: true do ~ end #〜範囲のデータは「　空入力禁止　」
with_options numericality: { other_than: 1 } do ~ end #〜範囲のデータは「　1（未選択）禁止　」
length: { maximum: 数値 } #最大文字数
numericality: { greater_than: -1, less_than: 100 } #数値限定で最大値と最小値設定
format: { with: /\A[0-9]+\z/ }　#指定した正規表現含ませる設定
```
他、日付やユーザー関連の制限は追加メソッドで指定していたりします。
バリデーションに関しては、他ユーザー様記事や[Railsガイド](https://railsguides.jp/active_record_validations.html)をご参考にされると良いと思います。

## 1. 「あああああ...」など同じ文字連続への対応

文字指定なら**正規表現！**
以前から感じてますが、初心者はSQLと正規表現の習得は必修だと思います。
自分もまだスラスラとは書けないので身につけたいです。

#### 1: NGWORD_REGEXを設定します。
語句はなんでもいいですが、エラーが出るので必ずバリデーションの前に設定してください。
今回は```5文字以上の同じ連続した文字があった場合は制限する```ことにしました。

```ruby
 NGWORD_REGEX = /(.)\1{4,}/.freeze
#1 (.)　で、全文字中1個指定(後ろで\1などするために（）で囲む）
#2 \1  で、直前文字と同じ1文字 
#3 {4,} で、\1が4桁以上のとき
#結果 1で文字1個指定して、2で同じ文字を選んで、3で2が4桁以上だったら ＝　同じ文字が5桁連続
#応用で同じフレーズとかの指定も可
```
正規表現は奥が深いですね。

#### 2: NGWORD_REGEXを利用して、バリデーションをかける
指定した正規表現含ませる```format:```に、除くを意味する```without```を合わせると良さそうです。

```ruby
format: { without: NGWORD_REGEX }
```

#### 3: 記述する
商品名```:name```、商品説明```:description```、引き渡し場所```:contact_location```にかけようと思いますが、
3箇所にかけるので、また```with_options```を使い、固有メッセージも加えます。

```ruby:models/item.rb
  
   + NGWORD_REGEX = /(.)\1{4,}/.freeze
  
  with_options presence: true do  
   + with_options format: { without: NGWORD_REGEX, message: 'は5文字以上の繰り返しは禁止です' } do
      validates :name, length: { maximum: 40 }
      validates :description, length: { maximum: 200 }
      validates :contact_location
   + end
    validates :quantity, numericality: { greater_than: -1, less_than: 100 }
    validates :deadline
    validates :price, format: { with: /\A[0-9]+\z/ }, numericality: { less_than: 1_000_000 }
    validates :image
  end
  with_options numericality: { other_than: 1 } do
    validates :prefecture_id
    validates :category_id
    validates :condition_id
  end
```

#### 4: 確認する
[![Image from Gyazo](https://i.gyazo.com/f69cf302faaa7d4320eee782a1cb31c4.png)](https://gyazo.com/f69cf302faaa7d4320eee782a1cb31c4)

うまくいきました。

```ruby
 NGWORD_REGEX = /(.)\1{4,}/.freeze
```

引き渡し場所を見る通り、正規表現通り、どの場所からでも5文字連続だと制限されるようになりました。

## 2. 下ネタやNGワードへの対応

最初は```NGWORD_REGEX```のように、NGワードを定義してとも思ったのですが、

1. コードが長くなりすぎる
2. 可読性の悪さ
3. 追加しずらい

ということで却下。

カスタムバリデーションも考慮したのですが、良いgemないかと調査したところ、
少々古いのですが、[Obscenity](https://github.com/tjackiw/obscenity)というgemを見つけたので実装してみることに。
こちらの記事[ブラックリストワードをフィルタリングする Obscenity](https://www.bokukoko.info/entry/2015/03/31/%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%E3%83%AA%E3%82%B9%E3%83%88%E3%83%AF%E3%83%BC%E3%83%89%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B_Obscenity)を参考にさせていただきました。

#### 1: gemの導入

```ruby:gemfile
+ gem 'obscenity'
```

```cl:通常コンソール
bundle install
```
```cl:Dockerの場合
docker-compose up --build  
```

#### 2: 設定ファイルの作成
config/initializers　に　obscenity.rb　を作成

```ruby:config/initializers/obscenity.rb
Obscenity.configure do |config|
  config.blacklist   = "config/blacklist.yml"  #　ブラックリストへのpath
  config.replacement = :stars       #　NGワード置き換え設定
end
```
詳細は[Obscenity](https://github.com/tjackiw/obscenity)を確認してください。

#### 3: 単語ファイルの作成
config　に　blacklist.yml　を作成
規制されるのがこわいので、単語は載せません！
追加もしやすい設計です。

```yml:config/blacklist.yml
---
- NGワード
- NGワード
- NGワード
- NGワード
- NGワード
#以下略
```

#### 4: 読み込みの設定

このままだと、modelで使用できないので、
```config/application.rb```に```obscenity/active_model```を呼ぶ設定を記述します。
私はこちらで上手くいったのですが、他の場所がいいよ！という方は教えていただきたいです...

```ruby:config/application.rb
# 省略
require "action_cable/engine"
require "sprockets/railtie"
+ require 'obscenity/active_model'
# require "rails/test_unit/railtie"

# 省略
```

#### 5: バリデーションの記述

では、itemモデルに戻ってバリデーションをかけていきます。
方針としては、
1.商品名にNGワードは✖️
2.説明や引き渡し場所も✖️　にしたいところですが、商品名と違い最初がたまたまNGワードになっていたという場合も考慮して、＊に置き換えたいです。

というわけで、以下のように記述。

```ruby:models/item.rb
  
   NGWORD_REGEX = /(.)\1{4,}/.freeze
  
  with_options presence: true do  
   with_options format: { without: NGWORD_REGEX, message: 'は5文字以上の繰り返しは禁止です' } do
     + validates :name, length: { maximum: 40 },obscenity: { message: 'はNGワードになっています' }
     + validates :description, length: { maximum: 200 },obscenity: { sanitize: true }
     + validates :contact_location,obscenity: { sanitize: true }
   end
    validates :quantity, numericality: { greater_than: -1, less_than: 100 }
    validates :deadline
    validates :price, format: { with: /\A[0-9]+\z/ }, numericality: { less_than: 1_000_000 }
    validates :image
  end
  with_options numericality: { other_than: 1 } do
    validates :prefecture_id
    validates :category_id
    validates :condition_id
  end
```

```ruby:説明
obscenity: true
# 上記で、バリーデーションがかけられるが、
obscenity: { message: 'はNGワードになっています' }
# のように、固有メッセージ付きで記述
obscenity: { sanitize: true }
# NGワードを置き換え設定、obscenity.rb　の　config.replacement = :stars 通りに＊に置き換え
```

#### 6: 確認する
今回はわかりやすく「スカート」をNGワードにしてみました。

##### 商品名を確認してみます。
[![Image from Gyazo](https://i.gyazo.com/3ede60952d6fb7edb2dbdce4e95f2321.png)](https://gyazo.com/3ede60952d6fb7edb2dbdce4e95f2321)

成功しています。
空白が前に合っても大丈夫です。
しかし、**前後に単語が入る、つまり文章の一部と認識されると機能しません。**
```巻きスカート```とかだと通っちゃいますが、
```巻き　スカート```とか、```巻き、スカート```のように、
**１単語と認識される記述**であれば制限してくれます。

###### 伏字も確認してみます。

このように入力してみましょう。
[![Image from Gyazo](https://i.gyazo.com/1b674ed031bc02b3ce1133e07ee3ec53.png)](https://gyazo.com/1b674ed031bc02b3ce1133e07ee3ec53)

こうなりました。
[![Image from Gyazo](https://i.gyazo.com/605944369afd631501def2e248b4eb05.png)](https://gyazo.com/605944369afd631501def2e248b4eb05)

1単語として認識された部分が、伏字（＊＊＊）になっていますが、文章中は認識されてないので、表示されています。
ちなみに編集する際にもフォーム内の文字は伏字化されています。

これで一応は、あからさまなNGワードがそうとわかるように表示されることはなさそうです。

# 結論

勉強になりました。
私が知らないだけかもしれませんが、railsアプリのNGワード対策って思ったほど、資料がありません。
正規表現もそうですが、意外とこうゆう部分を見逃している方は多そうです。（自分も）
今回、調べながらで時間かかりましたが、数時間程度で実装できたので、ポートフォリオアプリに実装してみても面白いのではないでしょうか。
NG具合を見てみたい方は、私のポートフォリオアプリを使ってみてください。
ただし、終わったら消してくださるよう伏してお願い致しますm(_ _)m


# 参考サイト様
[基本的な正規表現](https://murashun.jp/article/programming/regular-expression.html)
[Railsガイド　バリデーション](https://railsguides.jp/active_record_validations.html)
[Railsバリデーションまとめ](https://qiita.com/h1kita/items/772b81a1cc066e67930e)
https://teratail.com/questions/283606
[同じパターンの繰り返しを探す正規表現](https://mofc.hatenadiary.org/entry/20081013/1223881119)
[Obscenity](https://github.com/tjackiw/obscenity)
[ブラックリストワードをフィルタリングする Obscenity](https://www.bokukoko.info/entry/2015/03/31/%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%E3%83%AA%E3%82%B9%E3%83%88%E3%83%AF%E3%83%BC%E3%83%89%E3%82%92%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%99%E3%82%8B_Obscenity)