操作
機能 #14
未完了Chatworkのルーム投稿するとニュースとして反映される
ステータス:
解決
優先度:
通常
担当者:
-
開始日:
2025-05-10
期日:
進捗率:
0%
予定工数:
説明
Chatworkのルーム投稿するとニュースとして反映される
参考ニュースを共有するチャットワークが指定のルームで投稿されると、
ニュースをRedmineに取り込み、カテゴリとタグを設定する。
カテゴリは2つまで、タグは5個まで登録できる。
チャットワークスのAPとWebhookと連携する。
Redmine Admin さんが4日前に更新
ChatworkのWebhookからRedmineニュースに連携するプラグイン開発¶
開発概要¶
Chatworkのメッセージを受け取り、Redmineのニュースとして自動登録するプラグインを開発しました。このプラグインを使うことで、Chatworkで共有された情報を、カテゴリとタグを付けてRedmineのニュースとして整理できます。
調査結果¶
-
既存のプラグイン調査
- Redmine-Chatworkの連携プラグインはいくつか存在するが、チケット通知が主な用途
- ニュース登録に特化したプラグインは見つからなかった
-
Redmine News API
- Redmineはニュース作成用のAPIを提供している(POST /projects/:project_id/news.json)
- カスタムフィールドを使ってカテゴリ設定が可能
-
タグ機能
- 複数のタグプラグインが存在、Additional Tagsが最も活発にメンテナンスされている
- タグ検索・フィルタリング機能を実装可能
-
Chatwork Webhook
- メッセージ作成イベントをWebhookとして受け取り可能
- 署名検証による安全なデータ受信が可能
実装機能¶
-
基本機能
- Chatworkメッセージを受信し、ニュースとして保存
- メッセージ形式: 最初の行をタイトル、残りを本文として処理
- カテゴリとタグの自動抽出(例:[カテゴリ:お知らせ] [タグ:重要])
-
設定機能
- Webhookトークン設定
- ルーム-プロジェクトマッピング
- デフォルトカテゴリ/タグ設定
-
UI/UX
- ニュース表示時のカテゴリ/タグ表示
- タグによるニュースのフィルタリング
プラグイン構成¶
ファイル構造¶
redmine_chatwork_news/
├── app/
│ ├── controllers/
│ │ ├── chatwork_webhook_controller.rb # Webhook受信処理
│ │ └── chatwork_news_settings_controller.rb # 設定画面
│ ├── models/
│ │ └── chatwork_news_room_mapping.rb # ルームマッピングモデル
│ └── views/
│ ├── chatwork_news_settings/ # 設定画面ビュー
│ ├── hooks/
│ │ └── redmine_chatwork_news/
│ │ └── _view_news_show_bottom.html.erb # ニュース表示拡張
│ └── settings/
│ └── _chatwork_news_settings.html.erb # プラグイン設定画面
├── assets/
│ ├── stylesheets/ # CSS
│ └── javascripts/ # JavaScript
├── config/
│ ├── locales/ # 翻訳ファイル
│ └── routes.rb # ルーティング
├── db/
│ └── migrate/ # マイグレーションファイル
├── lib/
│ ├── redmine_chatwork_news.rb # コア機能
│ └── redmine_chatwork_news/
│ └── hooks.rb # フック実装
├── test/ # テストファイル
├── init.rb # プラグイン初期化
└── README.md
データベース構造¶
- chatwork_news_room_mappings: ChatworkルームとRedmineプロジェクトの対応関係
- NewsCustomField: カテゴリ保存用のカスタムフィールド
主要コード¶
init.rb(プラグイン登録)¶
require 'redmine'
require_dependency 'redmine_chatwork_news'
Redmine::Plugin.register :redmine_chatwork_news do
name 'Redmine Chatwork News Plugin'
author 'Claude'
description 'A plugin that creates news from Chatwork messages via Webhook'
version '0.1.0'
url 'https://github.com/jdmnt1999/redmine_chatwork_news'
author_url 'https://github.com/jdmnt1999'
# 設定メニューを追加
settings default: {
webhook_token: '',
chatwork_room_ids: '',
default_project_id: '',
default_categories: '',
max_categories: 2,
max_tags: 5
}, partial: 'settings/chatwork_news_settings'
# プロジェクトモジュールとして追加
project_module :chatwork_news do
permission :manage_chatwork_news_settings, {
chatwork_news_settings: [:index, :edit, :update]
}, require: :member
end
# メニューに追加
menu :admin_menu, :chatwork_news,
{ controller: 'chatwork_news_settings', action: 'index' },
caption: :label_chatwork_news,
html: { class: 'icon icon-news' }
end
# 登録後の処理
Rails.application.config.to_prepare do
# モデルの拡張
News.send(:include, RedmineChatworkNews::NewsPatch)
# フックを登録
require_dependency 'redmine_chatwork_news/hooks'
end
chatwork_webhook_controller.rb(Webhook受信処理)¶
class ChatworkWebhookController < ApplicationController
skip_before_action :verify_authenticity_token, only: [:create]
accept_api_auth :create
# Chatworkからのwebhookを受け取るアクション
def create
# リクエストボディを取得
request_body = request.raw_post
signature = request.headers['X-ChatWorkWebhookSignature']
# 署名検証
unless RedmineChatworkNews.valid_signature?(signature, request_body, webhook_token)
logger.warn "Invalid webhook signature received from #{request.remote_ip}"
render json: { error: 'Invalid signature' }, status: :unauthorized
return
end
# JSONをパース
begin
data = JSON.parse(request_body)
rescue JSON::ParserError => e
logger.error "Failed to parse webhook JSON: #{e.message}"
render json: { error: 'Invalid JSON payload' }, status: :bad_request
return
end
# メッセージを処理
result = process_webhook(data)
if result[:success]
render json: { status: 'success', news_id: result[:news_id] }, status: :ok
else
render json: { error: result[:error] }, status: :unprocessable_entity
end
end
private
# Chatworkからのwebhookを処理
def process_webhook(data)
# メッセージをパース
message = RedmineChatworkNews.parse_message(data)
unless message
return { success: false, error: 'Invalid message format' }
end
# 対象ルームIDかチェック
room_id = message[:room_id].to_s
unless target_room_ids.include?(room_id)
return { success: false, error: 'Room not configured for news' }
end
# プロジェクトを特定
project = find_project_for_room(room_id)
unless project
return { success: false, error: 'Project not found' }
end
# 著者を特定(管理者をデフォルトにする)
author = User.where(admin: true).first
unless author
# 管理者が見つからない場合は最初のアクティブユーザー
author = User.active.first
end
# ニュースのタイトルと説明を抽出
title = RedmineChatworkNews.extract_title(message[:body])
description = RedmineChatworkNews.extract_description(message[:body])
# カテゴリとタグを抽出
categories = RedmineChatworkNews.extract_categories(
message[:body],
RedmineChatworkNews.settings['max_categories'].to_i
)
tags = RedmineChatworkNews.extract_tags(
message[:body],
RedmineChatworkNews.settings['max_tags'].to_i
)
# ニュースを作成
news = RedmineChatworkNews.create_news(
project, title, description, author, categories, tags
)
if news
return { success: true, news_id: news.id }
else
return { success: false, error: 'Failed to create news' }
end
end
# ルームIDに対応するプロジェクトを取得
def find_project_for_room(room_id)
# 設定からプロジェクトマッピングを取得
mapping = ChatworkNewsRoomMapping.find_by(chatwork_room_id: room_id)
if mapping && mapping.project_id.present?
return Project.find_by(id: mapping.project_id)
end
# マッピングがない場合はデフォルトプロジェクトを使用
default_project_id = RedmineChatworkNews.settings['default_project_id']
if default_project_id.present?
return Project.find_by(id: default_project_id)
end
nil
end
# Webhook検証用のトークンを取得
def webhook_token
RedmineChatworkNews.settings['webhook_token']
end
# 対象のChatworkルームID一覧を取得
def target_room_ids
room_ids = RedmineChatworkNews.settings['chatwork_room_ids']
return [] if room_ids.blank?
room_ids.split(',').map(&:strip)
end
end
コアモジュール(redmine_chatwork_news.rb)¶
module RedmineChatworkNews
class << self
def settings
Setting.plugin_redmine_chatwork_news
end
# Chatworkからのメッセージをパース
def parse_message(webhook_event)
return nil unless webhook_event && webhook_event['webhook_event_type'] == 'message_created'
message_data = webhook_event['webhook_event']
return nil unless message_data
{
room_id: message_data['room_id'],
message_id: message_data['message_id'],
account_id: message_data['account_id'],
body: message_data['body'],
send_time: Time.at(message_data['send_time'])
}
end
# メッセージからタイトル抽出(最初の行)
def extract_title(body)
return '' if body.blank?
body.lines.first.strip
end
# メッセージから本文を抽出(2行目以降)
def extract_description(body)
return '' if body.blank?
lines = body.lines
return '' if lines.size <= 1
lines[1..-1].join
end
# メッセージからカテゴリを抽出
def extract_categories(body, max_categories = 2)
categories = []
return categories if body.blank?
# カテゴリタグを探す(例: [カテゴリ:技術])
body.scan(/\[カテゴリ:([^\]]+)\]/).flatten.each do |category|
categories << category.strip
break if categories.size >= max_categories
end
categories
end
# メッセージからタグを抽出
def extract_tags(body, max_tags = 5)
tags = []
return tags if body.blank?
# タグを探す(例: [タグ:Ruby])
body.scan(/\[タグ:([^\]]+)\]/).flatten.each do |tag|
tags << tag.strip
break if tags.size >= max_tags
end
tags
end
# 検証用のメソッド(Chatworkからのリクエストが正当かチェック)
def valid_signature?(signature, body, token)
return false if signature.blank? || body.blank? || token.blank?
# HMACによる署名検証
require 'base64'
require 'openssl'
hmac = OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), Base64.decode64(token), body)
expected_signature = Base64.encode64(hmac).strip
ActiveSupport::SecurityUtils.secure_compare(expected_signature, signature)
end
# ニュース記事を作成
def create_news(project, title, description, author, categories = [], tags = [])
return nil if project.nil? || title.blank? || author.nil?
news = News.new(
project: project,
title: title,
description: description,
author: author
)
if news.save
# カテゴリを設定(カスタムフィールドとして)
set_news_categories(news, categories) if categories.any?
# タグを設定(AdditionalTagsプラグインが利用可能な場合)
set_news_tags(news, tags) if tags.any? && additional_tags_available?
return news
end
nil
end
# ニュースにカテゴリを設定
def set_news_categories(news, categories)
# カテゴリをカスタムフィールドとして設定
category_field = CustomField.where(type: 'NewsCustomField', name: 'Categories').first
if category_field
category_value = categories.join(', ')
custom_value = news.custom_values.where(custom_field_id: category_field.id).first_or_initialize
custom_value.value = category_value
custom_value.save
end
end
# ニュースにタグを設定
def set_news_tags(news, tags)
return unless additional_tags_available?
# AdditionalTagsプラグインのAPIを使用
if news.respond_to?(:tag_list=)
news.tag_list = tags
news.save
end
end
# AdditionalTagsプラグインが利用可能か確認
def additional_tags_available?
Redmine::Plugin.installed?(:additional_tags) &&
defined?(AdditionalTags) &&
News.included_modules.include?(AdditionalTags::NewsTagging)
end
end
end
実装のポイント¶
1. セキュリティ対策¶
- HMAC SHA-256署名検証でWebhookが正当なChatworkからの送信であることを検証
- クロスサイトリクエストフォージェリ(CSRF)対策
2. データ処理フロー¶
- Chatworkからのwebhookを受信
- メッセージをパースしてタイトル、本文、カテゴリ、タグを抽出
- 対応するプロジェクトを特定
- Redmineのニュースとして登録
- カテゴリとタグを設定
3. UX改善¶
- フロントエンドではニュース表示時にカテゴリとタグを視覚的に表示
- タグによるフィルタリング機能でニュースの整理を容易に
4. 拡張性¶
- 複数のルームとプロジェクトのマッピングに対応
- Additional Tagsプラグインとの連携(オプション)
- 将来的な拡張のための柔軟なアーキテクチャ設計
マイグレーション¶
class CreateChatworkNewsRoomMappings < ActiveRecord::Migration[5.2]
def change
create_table :chatwork_news_room_mappings do |t|
t.string :chatwork_room_id, null: false
t.integer :project_id, null: false
t.string :category1
t.string :category2
t.string :default_tags
t.timestamps
end
add_index :chatwork_news_room_mappings, :chatwork_room_id, unique: true
add_index :chatwork_news_room_mappings, :project_id
# ニュースカテゴリ用のカスタムフィールドを作成
unless CustomField.where(type: 'NewsCustomField', name: 'Categories').exists?
CustomField.create!(
type: 'NewsCustomField',
name: 'Categories',
field_format: 'string',
is_required: false,
is_for_all: true,
searchable: true
)
end
end
end
テスト¶
テストは以下の観点で実装:
- ユニットテスト: メッセージ解析、カテゴリ/タグ抽出、HMAC検証など
- コントローラーテスト: Webhook受信、エラー処理など
- 統合テスト: エンドツーエンドのフロー確認
今後の開発計画¶
短期計画(v0.2.0)¶
- メッセージフィルタリング機能
- 特定のプレフィックスが付いたメッセージのみをニュースとして登録
- 添付ファイル対応
- Chatworkの添付ファイルをニュースの添付ファイルとして登録
- メッセージ編集への対応
- メッセージ編集時にニュースも更新
中期計画(v0.3.0)¶
- 複数ルーム対応の強化
- ニュース作成通知機能
- 管理画面の機能強化
- アクセス権限の詳細設定
長期計画(v1.0.0)¶
- 他のチャットツールへの対応(Slack、Microsoft Teams)
- テンプレート機能
- 高度なフィルタリング機能
- APIの拡張
GitHub リポジトリ¶
操作