Sinatraで作る中高生向け悩み相談Webサービス「Ballon」- 回答機能、「いいね」機能、セキュリティ対策、コンテンツモデレーション (Part 4)


## はじめに

前回のPart 3では、悩み投稿機能の実装とデータベース設計について詳しく解説しました。今回のPart 4では、以下の内容について説明します：

1. 回答機能の実装
2. 「いいね」機能の実装
3. 高度なセキュリティ対策
4. コンテンツモデレーション

これらの機能追加とベストプラクティスの適用により、Ballonの機能性、安全性、そしてユーザー体験を向上させます。

## 1. 回答機能の実装

まず、回答機能を実装します。ユーザーが悩みに対して回答を投稿できるようにします。

### データベース設計

回答のためのテーブルを作成します：

```ruby:db/migrate/YYYYMMDDHHMMSS_create_answers.rb
class CreateAnswers < ActiveRecord::Migration[5.2]
  def change
    create_table :answers do |t|
      t.references :user, foreign_key: true
      t.references :worry, foreign_key: true
      t.text :content, null: false
      t.boolean :anonymous, default: false
      t.timestamps
    end
  end
end
```

### モデルの実装

Answerモデルを実装します：

```ruby:models.rb
class Answer < ActiveRecord::Base
  belongs_to :user
  belongs_to :worry
  has_many :likes, dependent: :destroy

  validates :content, presence: true, length: { maximum: 1000 }
  validates :user_id, presence: true
  validates :worry_id, presence: true

  scope :recent, -> { order(created_at: :desc) }

  def liked_by?(user)
    likes.where(user_id: user.id).exists?
  end

  before_save :sanitize_content

  private

  def sanitize_content
    self.content = Sanitize.fragment(self.content)
  end
end
```

既存のWorryモデルとUserモデルにも関連を追加します：

```ruby:models.rb
class Worry < ActiveRecord::Base
  # 既存のコードに追加
  has_many :answers, dependent: :destroy
end

class User < ActiveRecord::Base
  # 既存のコードに追加
  has_many :answers
end
```

### コントローラーロジック

app.rbに回答機能のルーティングとロジックを追加します：

```ruby:app.rb
# 回答の作成
post '/worries/:worry_id/answers' do
  authenticate!
  @worry = Worry.find(params[:worry_id])
  @answer = @worry.answers.new(content: params[:content], user: current_user, anonymous: params[:anonymous])
  if @answer.save
    redirect "/worries/#{@worry.id}"
  else
    @errors = @answer.errors.full_messages
    erb :worry_show
  end
end

# 回答の削除
delete '/answers/:id' do
  authenticate!
  answer = current_user.answers.find(params[:id])
  worry_id = answer.worry_id
  answer.destroy
  redirect "/worries/#{worry_id}"
end
```

### ビューの更新

悩み詳細ページ（worry_show.erb）を更新して、回答の表示と投稿フォームを追加します：

```erb:views/worry_show.erb
<h2><%= h(@worry.title) %></h2>
<p><%= h(@worry.content) %></p>

<h3>回答</h3>
<% @worry.answers.recent.each do |answer| %>
  <div class="answer">
    <p><%= h(answer.content) %></p>
    <small>
      回答者: <%= answer.anonymous ? "匿名" : h(answer.user.username) %> 
      at <%= answer.created_at.strftime('%Y-%m-%d %H:%M') %>
    </small>
    <% if answer.user == current_user %>
      <form action="/answers/<%= answer.id %>" method="post" style="display: inline;">
        <input type="hidden" name="_method" value="delete">
        <input type="submit" value="削除" onclick="return confirm('本当に削除しますか？');">
      </form>
    <% end %>
  </div>
<% end %>

<h4>回答を投稿</h4>
<form action="/worries/<%= @worry.id %>/answers" method="post">
  <textarea name="content" required></textarea>
  <label>
    <input type="checkbox" name="anonymous" value="1"> 匿名で投稿する
  </label>
  <input type="submit" value="回答を投稿">
</form>
```

## 2. 「いいね」機能の実装

次に、回答に対する「いいね」機能を実装します。

### データベース設計

「いいね」のためのテーブルを作成します：

```ruby:db/migrate/YYYYMMDDHHMMSS_create_likes.rb
class CreateLikes < ActiveRecord::Migration[5.2]
  def change
    create_table :likes do |t|
      t.references :user, foreign_key: true
      t.references :answer, foreign_key: true
      t.timestamps
    end
    add_index :likes, [:user_id, :answer_id], unique: true
  end
end
```

### モデルの実装

Likeモデルを実装します：

```ruby:models.rb
class Like < ActiveRecord::Base
  belongs_to :user
  belongs_to :answer

  validates :user_id, uniqueness: { scope: :answer_id }
end
```

UserモデルとAnswerモデルにLikeの関連を追加します：

```ruby:models.rb
class User < ActiveRecord::Base
  # 既存のコードに追加
  has_many :likes
  has_many :liked_answers, through: :likes, source: :answer
end

class Answer < ActiveRecord::Base
  # 既存のコードに追加
  has_many :likes, dependent: :destroy
  has_many :liking_users, through: :likes, source: :user
end
```

### コントローラーロジック

app.rbに「いいね」機能のルーティングとロジックを追加します：

```ruby:app.rb
# いいねの追加
post '/answers/:answer_id/likes' do
  authenticate!
  answer = Answer.find(params[:answer_id])
  like = current_user.likes.new(answer: answer)
  if like.save
    redirect "/worries/#{answer.worry_id}"
  else
    # エラー処理
    redirect "/worries/#{answer.worry_id}"
  end
end

# いいねの削除
delete '/answers/:answer_id/likes' do
  authenticate!
  answer = Answer.find(params[:answer_id])
  like = current_user.likes.find_by(answer: answer)
  like.destroy if like
  redirect "/worries/#{answer.worry_id}"
end
```

### ビューの更新

worry_show.erbを更新して、「いいね」ボタンを追加します：

```erb:views/worry_show.erb
<% @worry.answers.recent.each do |answer| %>
  <div class="answer">
    <!-- 既存のコード -->
    <p>いいね数: <%= answer.likes.count %></p>
    <% if current_user %>
      <% if answer.liked_by?(current_user) %>
        <form action="/answers/<%= answer.id %>/likes" method="post" style="display: inline;">
          <input type="hidden" name="_method" value="delete">
          <input type="submit" value="いいねを取り消す">
        </form>
      <% else %>
        <form action="/answers/<%= answer.id %>/likes" method="post" style="display: inline;">
          <input type="submit" value="いいね">
        </form>
      <% end %>
    <% end %>
  </div>
<% end %>
```

## 3. 高度なセキュリティ対策

中高生を対象とするサービスであるため、セキュリティには特に注意を払う必要があります。

### CSRF対策

Sinatraには標準でCSRF対策が含まれていないため、`rack-protection` gemを使用して実装します。

```ruby:app.rb
require 'rack/protection'
use Rack::Protection::AuthenticityToken
```

そして、フォームに CSRF トークンを追加します：

```erb
<form action="/some_action" method="post">
  <%= csrf_token_tag %>
  <!-- フォームの内容 -->
</form>
```

`csrf_token_tag` メソッドを定義するために、以下のヘルパーを追加します：

```ruby:app.rb
helpers do
  def csrf_token_tag
    Rack::Protection::AuthenticityToken.token(session)
    Rack::Protection::AuthenticityToken.token(session)
    "<input type='hidden' name='authenticity_token' value='#{Rack::Protection::AuthenticityToken.token(session)}'>"
  end
end
```

### XSS対策

ユーザー入力をエスケープするために、`Rack::Utils.escape_html` メソッドを使用します。以下のヘルパーメソッドを追加します：

```ruby:app.rb
helpers do
  def h(text)
    Rack::Utils.escape_html(text)
  end
end
```

### セキュアなセッション設定

セッションをより安全にするために、以下の設定を追加します：

```ruby:app.rb
use Rack::Session::Cookie,
  key: 'rack.session',
  path: '/',
  expire_after: 2592000, # 30日
  secret: ENV['SESSION_SECRET'],
  same_site: :strict
```

`SESSION_SECRET` は環境変数から読み込むようにし、公開リポジトリにコミットしないようにします。

### 強力なパスワードポリシー

ユーザー登録時にパスワードの強度をチェックするバリデーションを追加します：

```ruby:models.rb
class User < ActiveRecord::Base
  # 既存のバリデーションに追加
  validate :password_complexity

  private

  def password_complexity
    return if password.blank? || password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/
    errors.add :password, 'パスワードは8文字以上で、大文字、小文字、数字、特殊文字をそれぞれ1つ以上含む必要があります。'
  end
end
```

## 4. コンテンツモデレーション

中高生のメンタルヘルスに配慮したコンテンツモデレーションを実装します。

### キーワードフィルタリング

特定のキーワードを含む投稿を自動的にフラグ付けまたはモデレーションキューに入れます。

```ruby:models.rb
class Worry < ActiveRecord::Base
  # 既存のコードに追加
  before_save :check_sensitive_content

  private

  def check_sensitive_content
    sensitive_words = ['自殺', '薬物', '虐待', # その他センシティブな単語]
    if sensitive_words.any? { |word| self.content.include?(word) }
      self.needs_moderation = true
    end
  end
end
```

### モデレーションシステム

フラグ付けされた投稿を管理者が確認できるようにします。

```ruby:app.rb
# 管理者用：モデレーションが必要な悩みの一覧
get '/admin/moderation' do
  authenticate_admin!
  @worries_for_moderation = Worry.where(needs_moderation: true)
  erb :admin_moderation
end

# 管理者用：悩みの承認または削除
post '/admin/moderation/:id' do
  authenticate_admin!
  worry = Worry.find(params[:id])
  if params[:approve]
    worry.update(needs_moderation: false)
  elsif params[:delete]
    worry.destroy
  end
  redirect '/admin/moderation'
end
```

### 専門家への紹介

深刻な悩みの場合、専門家への相談を促すメッセージを表示します。

```erb:views/worry_show.erb
<% if @worry.needs_expert_advice? %>
  <div class="alert">
    この悩みについては、専門家に相談することをおすすめします。
    <a href="/resources">相談窓口一覧はこちら</a>
  </div>
<% end %>
```

### ポジティブな環境作り

回答の内容が否定的または攻撃的でないかチェックし、ポジティブな表現を促します。

```ruby:models.rb
class Answer < ActiveRecord::Base
  # 既存のコードに追加
  validate :encourage_positivity

  private

  def encourage_positivity
    negative_words = ['バカ', 'ダメ', '無理', # その他ネガティブな単語]
    if negative_words.any? { |word| self.content.include?(word) }
      errors.add(:content, 'もう少し前向きな言葉で表現してみましょう。相手の気持ちを考えて回答することが大切です。')
    end
  end
end
```
## まとめ

Part 4では、Ballonの回答機能と「いいね」機能の実装、高度なセキュリティ対策、そしてコンテンツモデレーションについて詳しく解説しました。これらの機能と対策により、Ballonはより安全で使いやすい悩み相談プラットフォームとなります。

特に以下の点に注目しました：

1. 回答機能の実装：ユーザーが悩みに対して回答を投稿できるようになり、コミュニティでの支援が可能になりました。
2. 「いいね」機能：有益な回答を評価する仕組みを導入し、質の高い回答を促進します。
3. セキュリティ対策：CSRF対策、XSS対策、セキュアなセッション管理など、多層的なセキュリティ対策を実装しました。
4. コンテンツモデレーション：キーワードフィルタリング、管理者用モデレーションシステム、専門家への紹介機能など、中高生のメンタルヘルスに配慮した仕組みを導入しました。

## 中高生向けサービスとしての特別な配慮事項

Ballonは中高生を対象としているため、以下のような特別な配慮を行っています：

1. 年齢に適した言葉遣い：
   システムメッセージやエラーメッセージを、中高生にわかりやすい言葉で表現します。

   ```ruby:helpers.rb
   def user_friendly_message(key)
     messages = {
       welcome: "Ballonへようこそ！みんなで支え合う場所だよ。",
       password_error: "パスワードは8文字以上で、大文字、小文字、数字、記号をそれぞれ1つ以上含めてね。",
       content_too_long: "書いてくれてありがとう！でも少し長いかも。もう少し短くできるかな？",
       # 他のメッセージ...
     }
     messages[key] || "なにかうまくいかないみたい。もう一度試してみてね。"
   end
   ```

2. 利用時間の制限：
   健全な生活リズムを守るため、深夜の利用を控えめにするよう促します。

   ```ruby:app.rb
   before do
     @current_hour = Time.now.hour
     if @current_hour >= 22 || @current_hour < 6
       @late_night_warning = true
     end
   end
   ```

   ```erb:views/layout.erb
   <% if @late_night_warning %>
     <div class="alert">
       夜遅くまで起きているのかな？十分な睡眠は大切だよ。明日また来てね！
     </div>
   <% end %>
   ```

3. ポジティブな表現の奨励：
   否定的な表現を使用した場合、より前向きな表現を提案します。

   ```ruby:models.rb
   class Answer < ActiveRecord::Base
     # 既存のコードに追加
     before_save :suggest_positive_expression

     private

     def suggest_positive_expression
       negative_phrases = {
         "できない" => "まだ難しいかもしれない",
         "無理" => "チャレンジが必要",
         "だめ" => "改善の余地がある"
         # 他のフレーズ...
       }

       negative_phrases.each do |negative, positive|
         if self.content.include?(negative)
           self.content += "\n\n[アドバイス] 「#{negative}」という表現の代わりに、「#{positive}」というように言い換えてみるのはどうかな？ポジティブな表現を使うと、相手も前向きな気持ちになれるよ。"
         end
       end
     end
   end
   ```

4. 教育的要素の組み込み：
   悩みの解決過程を通じて、問題解決スキルや感情管理スキルを学べるような仕組みを提供します。

   ```ruby:app.rb
   get '/learn/problem_solving' do
     @steps = [
       {title: "問題を明確にする", description: "どんな状況で、何が困っているのか具体的に書き出してみよう。"},
       {title: "可能な解決策を考える", description: "思いつく限りのアイデアを出してみよう。この時点では実現可能かどうかは気にしなくていいよ。"},
       {title: "それぞれの解決策のメリット・デメリットを考える", description: "各アイデアについて、良い点と悪い点を考えてみよう。"},
       {title: "最適な解決策を選ぶ", description: "メリットとデメリットを比較して、一番良さそうな方法を選んでみよう。"},
       {title: "実行する", description: "選んだ方法を実際に試してみよう。うまくいかなくても大丈夫、それも大切な経験だよ。"},
       {title: "結果を評価する", description: "どうだった？うまくいったところ、改善が必要なところを振り返ってみよう。"}
     ]
     erb :problem_solving_guide
   end
   ```

   ```erb:views/problem_solving_guide.erb
   <h2>問題解決のステップ</h2>
   <p>困ったことがあったときは、以下のステップを試してみよう！</p>
   
   <% @steps.each_with_index do |step, index| %>
     <div class="step">
       <h3><%= index + 1 %>. <%= step[:title] %></h3>
       <p><%= step[:description] %></p>
     </div>
   <% end %>
   ```

これらの配慮により、Ballonは単なる悩み相談サービスではなく、中高生の成長と学びを支援するプラットフォームとして機能します。

## 次回予告

次回のPart 5では、以下の内容について解説する予定です：

1. パフォーマンス最適化：大規模なユーザーベースに対応するための戦略
2. アクセシビリティ対応：様々な利用者に配慮したデザインと実装
3. 多言語対応：国際的な展開を視野に入れた機能拡張
4. アナリティクスとレポーティング：サービス改善のためのデータ活用

また、Ballonの開発を通じて学んだ教訓や、中高生向けサービス開発における倫理的考慮事項についても議論する予定です。

引き続き、Ballonの開発過程を通じて、Sinatraを使用したWebアプリケーション開発の実践的なスキルと、若者向けサービス開発における重要な考慮事項を学んでいきましょう。

ご質問やフィードバックがありましたら、コメント欄にてお待ちしています！