RailsでCirclebackのWebhookを受け取って会議データを保存する
はじめに
Circlebackは、オンライン会議の録音・転写・要約を自動化してくれるAIサービスです。会議終了後に議事録やアクションアイテムを自動生成してくれるため、多くの企業で導入が進んでいます。
この記事では、CirclebackのWebhook機能を使って、Railsアプリケーションで会議データを受け取り、データベースに保存する実装方法を解説します。
Circlebackとは
Circlebackは、Zoom、Google Meet、Microsoft TeamsなどのWeb会議ツールと連携し、以下の機能を提供します:
- 自動録音・転写: 会議内容の自動録音と音声のテキスト化
- AI要約: 会議内容の要点をAIが自動で要約
- アクションアイテム抽出: 会議中に決まったタスクを自動抽出
- インサイト生成: カスタマイズ可能な項目での情報抽出
Webhook機能を使うことで、会議終了後にこれらのデータを自動的に外部システムに送信できます。
テーブル設計
Circlebackから送信されるWebhookデータを保存するためのテーブル設計を考えてみましょう。
meetings テーブル
# 会議の基本情報を保存
create_table :meetings do |t|
t.bigint :circleback_id, null: false, index: { unique: true }
t.string :name, null: false
t.datetime :meeting_created_at, null: false
t.integer :duration_seconds
t.string :meeting_url
t.string :recording_url
t.string :ical_uid
t.text :notes
t.json :raw_webhook_data # 元のWebhookデータを念のため保存
t.timestamps
end
meeting_attendees テーブル
# 参加者情報を保存
create_table :meeting_attendees do |t|
t.references :meeting, null: false, foreign_key: true
t.string :name
t.string :email
t.timestamps
end
meeting_tags テーブル
# タグ情報を保存(多対多の関係)
create_table :meeting_tags do |t|
t.string :name, null: false, index: { unique: true }
t.timestamps
end
create_table :meeting_taggings do |t|
t.references :meeting, null: false, foreign_key: true
t.references :meeting_tag, null: false, foreign_key: true
t.timestamps
end
action_items テーブル
# アクションアイテムを保存
create_table :action_items do |t|
t.references :meeting, null: false, foreign_key: true
t.bigint :circleback_id, null: false
t.string :title, null: false
t.text :description
t.string :assignee_name
t.string :assignee_email
t.string :status, default: 'PENDING'
t.timestamps
end
transcript_segments テーブル
# 転写データを保存
create_table :transcript_segments do |t|
t.references :meeting, null: false, foreign_key: true
t.string :speaker, null: false
t.text :text, null: false
t.decimal :timestamp_seconds, precision: 10, scale: 2
t.timestamps
end
meeting_insights テーブル
# インサイトデータを保存
create_table :meeting_insights do |t|
t.references :meeting, null: false, foreign_key: true
t.string :insight_name, null: false
t.json :insight_data
t.string :speaker
t.decimal :timestamp_seconds, precision: 10, scale: 2
t.timestamps
end
CirclebackでのWebhook設定手順
RailsアプリケーションでWebhookを受け取る準備ができたら、Circleback側でWebhookの設定を行います。
設定手順
-
Automationsページを開く: https://app.circleback.ai/automations にアクセスします
-
Webhook設定を追加: 既存のAutomationにWebhookステップを追加するか、新しいAutomationを作成してWebhookステップを追加します
-
エンドポイントURLの設定:
- Webhook endpoint URLに、先ほど作成したRailsアプリケーションのエンドポイントを入力します
- 例:
https://yourdomain.com/api/v1/circleback/webhook
- Webhook送信に含めたくないデータがある場合は、該当項目をオフに切り替えます
-
設定の保存: Done をクリックしてから Save をクリックしてAutomationを保存します
注意事項
- HTTPS必須: CirclebackのWebhookはHTTPS接続が必要です
- 署名検証: 本番環境では必ず署名検証を有効にしてください
- タイムアウト設定: Webhookエンドポイントは30秒以内にレスポンスを返す必要があります
Railsアプリケーションでの実装
モデルの定義
まず、各モデルのリレーションを定義します:
# app/models/meeting.rb
class Meeting < ApplicationRecord
has_many :meeting_attendees, dependent: :destroy
has_many :meeting_taggings, dependent: :destroy
has_many :meeting_tags, through: :meeting_taggings
has_many :action_items, dependent: :destroy
has_many :transcript_segments, dependent: :destroy
has_many :meeting_insights, dependent: :destroy
validates :circleback_id, presence: true, uniqueness: true
validates :name, presence: true
end
# app/models/meeting_attendee.rb
class MeetingAttendee < ApplicationRecord
belongs_to :meeting
end
# app/models/meeting_tag.rb
class MeetingTag < ApplicationRecord
has_many :meeting_taggings, dependent: :destroy
has_many :meetings, through: :meeting_taggings
validates :name, presence: true, uniqueness: true
end
# app/models/meeting_tagging.rb
class MeetingTagging < ApplicationRecord
belongs_to :meeting
belongs_to :meeting_tag
end
# app/models/action_item.rb
class ActionItem < ApplicationRecord
belongs_to :meeting
validates :circleback_id, presence: true
validates :title, presence: true
validates :status, inclusion: { in: %w[PENDING DONE] }
end
# app/models/transcript_segment.rb
class TranscriptSegment < ApplicationRecord
belongs_to :meeting
validates :speaker, presence: true
validates :text, presence: true
end
# app/models/meeting_insight.rb
class MeetingInsight < ApplicationRecord
belongs_to :meeting
validates :insight_name, presence: true
end
Webhookコントローラーの実装
# app/controllers/api/v1/circleback_webhooks_controller.rb
class Api::V1::CirclebackWebhooksController < ApplicationController
protect_from_forgery with: :null_session
before_action :verify_webhook_signature
def create
webhook_service = CirclebackWebhookService.new(webhook_params)
result = webhook_service.process
if result[:success]
Rails.logger.info "Circleback webhook processed successfully for meeting ID: #{result[:meeting].circleback_id}"
render json: { status: 'success' }, status: :ok
else
Rails.logger.error "Failed to process Circleback webhook: #{result[:error]}"
render json: { error: result[:error] }, status: :unprocessable_entity
end
rescue => e
Rails.logger.error "Unexpected error processing Circleback webhook: #{e.message}"
Rails.logger.error e.backtrace.join("\n")
render json: { error: 'Internal server error' }, status: :internal_server_error
end
private
def webhook_params
params.permit!.to_h
end
def verify_webhook_signature
return unless Rails.env.production? # 開発環境では署名検証をスキップ
signing_secret = Rails.application.credentials.circleback_signing_secret
signature = request.headers['X-Signature']
request_body = request.raw_post
unless CirclebackSignatureVerifier.verify(request_body, signature, signing_secret)
Rails.logger.warn "Invalid Circleback webhook signature"
render json: { error: 'Invalid signature' }, status: :unauthorized
end
end
end
Webhookデータ処理サービスの実装
# app/services/circleback_webhook_service.rb
class CirclebackWebhookService
def initialize(webhook_data)
@webhook_data = webhook_data
end
def process
ActiveRecord::Base.transaction do
meeting = create_or_update_meeting
create_attendees(meeting)
create_tags(meeting)
create_action_items(meeting)
create_transcript_segments(meeting)
create_insights(meeting)
{ success: true, meeting: meeting }
end
rescue => e
Rails.logger.error "Error processing Circleback webhook: #{e.message}"
{ success: false, error: e.message }
end
private
def create_or_update_meeting
meeting = Meeting.find_or_initialize_by(circleback_id: @webhook_data['id'])
meeting.assign_attributes(
name: @webhook_data['name'],
meeting_created_at: Time.parse(@webhook_data['createdAt']),
duration_seconds: @webhook_data['duration']&.to_i,
meeting_url: @webhook_data['url'],
recording_url: @webhook_data['recordingUrl'],
ical_uid: @webhook_data['icalUid'],
notes: @webhook_data['notes'],
raw_webhook_data: @webhook_data
)
meeting.save!
meeting
end
def create_attendees(meeting)
# 既存の参加者を削除してから再作成
meeting.meeting_attendees.destroy_all
return unless @webhook_data['attendees'].present?
@webhook_data['attendees'].each do |attendee_data|
meeting.meeting_attendees.create!(
name: attendee_data['name'],
email: attendee_data['email']
)
end
end
def create_tags(meeting)
# 既存のタグ関連付けを削除
meeting.meeting_taggings.destroy_all
return unless @webhook_data['tags'].present?
@webhook_data['tags'].each do |tag_name|
tag = MeetingTag.find_or_create_by(name: tag_name)
meeting.meeting_taggings.create!(meeting_tag: tag)
end
end
def create_action_items(meeting)
# 既存のアクションアイテムを削除してから再作成
meeting.action_items.destroy_all
return unless @webhook_data['actionItems'].present?
@webhook_data['actionItems'].each do |action_item_data|
assignee = action_item_data['assignee']
meeting.action_items.create!(
circleback_id: action_item_data['id'],
title: action_item_data['title'],
description: action_item_data['description'],
assignee_name: assignee&.dig('name'),
assignee_email: assignee&.dig('email'),
status: action_item_data['status']
)
end
end
def create_transcript_segments(meeting)
# 既存の転写データを削除してから再作成
meeting.transcript_segments.destroy_all
return unless @webhook_data['transcript'].present?
@webhook_data['transcript'].each do |segment_data|
meeting.transcript_segments.create!(
speaker: segment_data['speaker'],
text: segment_data['text'],
timestamp_seconds: segment_data['timestamp']
)
end
end
def create_insights(meeting)
# 既存のインサイトを削除してから再作成
meeting.meeting_insights.destroy_all
return unless @webhook_data['insights'].present?
@webhook_data['insights'].each do |insight_name, insight_data_array|
next unless insight_data_array.is_a?(Array)
insight_data_array.each do |insight_item|
meeting.meeting_insights.create!(
insight_name: insight_name,
insight_data: insight_item['insight'],
speaker: insight_item['speaker'],
timestamp_seconds: insight_item['timestamp']
)
end
end
end
end
署名検証の実装
セキュリティのため、Webhookの署名検証を実装します:
# app/services/circleback_signature_verifier.rb
require 'openssl'
class CirclebackSignatureVerifier
def self.verify(request_body, signature, signing_secret)
return false if signature.blank? || signing_secret.blank?
expected_signature = OpenSSL::HMAC.hexdigest(
OpenSSL::Digest.new('sha256'),
signing_secret,
request_body
)
# タイミング攻撃を防ぐためのセキュアな比較
ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
end
end
エラーハンドリングとロギング
カスタムログフォーマットの設定
# config/application.rb
class Application < Rails::Application
# Webhookのログ設定
config.webhook_logger = ActiveSupport::Logger.new(Rails.root.join('log', 'webhooks.log'))
config.webhook_logger.formatter = proc do |severity, datetime, progname, msg|
"[#{datetime}] #{severity}: #{msg}\n"
end
end
ログ記録用のサービス追加
# app/services/webhook_logger_service.rb
class WebhookLoggerService
def self.log_webhook_received(webhook_data)
Rails.application.config.webhook_logger.info(
"Circleback webhook received - Meeting ID: #{webhook_data['id']}, Name: #{webhook_data['name']}"
)
end
def self.log_processing_error(error, webhook_data)
Rails.application.config.webhook_logger.error(
"Circleback webhook processing failed - Meeting ID: #{webhook_data&.dig('id')}, Error: #{error.message}"
)
end
def self.log_signature_verification_failed(request_headers)
Rails.application.config.webhook_logger.warn(
"Circleback webhook signature verification failed - User-Agent: #{request_headers['User-Agent']}"
)
end
end
改良されたコントローラー
# app/controllers/api/v1/circleback_webhooks_controller.rb(改良版)
class Api::V1::CirclebackWebhooksController < ApplicationController
protect_from_forgery with: :null_session
before_action :verify_webhook_signature
def create
WebhookLoggerService.log_webhook_received(webhook_params)
webhook_service = CirclebackWebhookService.new(webhook_params)
result = webhook_service.process
if result[:success]
Rails.logger.info "Circleback webhook processed successfully for meeting ID: #{result[:meeting].circleback_id}"
render json: { status: 'success', meeting_id: result[:meeting].id }, status: :ok
else
WebhookLoggerService.log_processing_error(StandardError.new(result[:error]), webhook_params)
render json: { error: result[:error] }, status: :unprocessable_entity
end
rescue => e
WebhookLoggerService.log_processing_error(e, webhook_params)
render json: { error: 'Internal server error' }, status: :internal_server_error
end
private
def webhook_params
params.permit!.to_h
end
def verify_webhook_signature
return unless Rails.env.production?
signing_secret = Rails.application.credentials.circleback_signing_secret
signature = request.headers['X-Signature']
request_body = request.raw_post
unless CirclebackSignatureVerifier.verify(request_body, signature, signing_secret)
WebhookLoggerService.log_signature_verification_failed(request.headers)
render json: { error: 'Invalid signature' }, status: :unauthorized
end
end
end
ルーティング設定
# config/routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1 do
post 'circleback/webhook', to: 'circleback_webhooks#create'
end
end
end
設定ファイルの追加
credentials.yml.enc の設定
rails credentials:edit
# config/credentials.yml.enc
circleback_signing_secret: your_webhook_signing_secret_here
環境変数の設定例
# config/environments/production.rb
Rails.application.configure do
# Webhook用のログレベル設定
config.log_level = :info
# セキュリティ設定
config.force_ssl = true
end
まとめ
この記事では、RailsアプリケーションでCirclebackのWebhookを受け取り、会議データを効率的に保存する実装方法を解説しました。
実装のポイント
- 正規化されたテーブル設計: 会議データの各要素を適切なテーブルに分割して保存
- 署名検証: セキュリティを確保するためのWebhook署名検証
- トランザクション: データの整合性を保つためのトランザクション処理
- エラーハンドリング: 適切なログ記録と例外処理
- 冪等性: 同じWebhookが複数回送信されても安全な処理
今後の拡張案
- 非同期処理: Sidekiqなどを使ったバックグラウンド処理
- 重複排除: Webhook IDを使った重複処理の防止
- リトライ機能: 失敗時の自動リトライ機能
- Webhook配信履歴: 配信状況の記録と監視機能
Circlebackとの連携により、会議の生産性向上と情報の一元管理が実現できます。ぜひこの実装を参考に、自社のワークフローに合わせてカスタマイズしてみてください。
Discussion