📌 概要
GROWIで記事を保存する際、Slack通知のチェックボックスを常に自動的にONにし、Ctrl+Sでの保存時にもSlack通知を送信できるようにするカスタムスクリプトの実装方法。
そもそもUser trigger notificationでスラックに連携する先は選択できるのだが、投稿するときに必ず添付画像のチェックを入れないと連携されない仕様になっているため不便。
だから強制的にスラックに通知するようカスタムスクリプトを追記した。


🎯 解決した課題
課題1: チェックボックスの問題
- GROWIのSlack通知機能は、記事保存時に毎回チェックボックスをONにする必要がある
- チェックを忘れると、Slackに通知されない
- 要望: チェックボックスを常に自動的にONにしたい
課題2: Ctrl+Sでの保存時に通知されない
- 保存ボタンを押した場合は通知される
- Ctrl+Sでの保存時は通知されない
- これは意図的な動作ではなく、設定の問題だった
🔍 問題の調査プロセス
ステップ1: 初期の仮説と実装
仮説: チェックボックスの checked プロパティを true にすれば良い
const element = document.getElementById("idForEditorNavbarBottom");
element.checked = true;
結果: ❌ 画面上はチェックされるが、内部的に反映されない
- ReactアプリケーションではDOM操作だけでは不十分
- Reactの内部状態が更新されていない
ステップ2: Reactのイベントを発火
改善策: click() メソッドでReactの状態を更新
const element = document.getElementById("idForEditorNavbarBottom");
if (element && !element.checked) {
element.click(); // Reactのイベントハンドラーが呼ばれる
}
結果: ✅ 保存ボタンを押した時は動作
❌ Ctrl+Sでは通知されない
ステップ3: 通信の監視
仮説: Ctrl+Sと保存ボタンで異なるAPIが呼ばれている?
以下をインターセプトして調査:
// fetch API の監視
const originalFetch = window.fetch;
window.fetch = function(...args) {
console.log('fetch:', args);
return originalFetch.apply(this, args);
};
// XMLHttpRequest の監視
XMLHttpRequest.prototype.send = function(body) {
console.log('XHR send:', body);
return originalXHRSend.apply(this, arguments);
};
発見:
- GROWIは
XMLHttpRequestを使用(fetchではない) - 保存時に
POST /_api/pages.updateが呼ばれる
ステップ4: 送信データの解析
重要な発見:
保存ボタンを押した時(Slack通知あり):
{
"isSlackEnabled": true,
"slackChannels": "#general",
...
}
Ctrl+Sを押した時(Slack通知なし):
{
"isSlackEnabled": true,
"slackChannels": "", // ← 空文字列!
...
}
根本原因:
-
isSlackEnabledはチェックボックスの状態 slackChannelsは通知先チャンネル- Ctrl+Sでは
slackChannelsが空のため、通知が送信されない
💡 最終的な解決策
アプローチ: 3段階の防御
1. UI レベル: チェックボックスを常にON
const checkbox = document.getElementById("idForEditorNavbarBottom");
if (checkbox && !checkbox.checked) {
checkbox.click();
}
2. UI レベル: チャンネル入力欄にデフォルト値を設定
const channelInput = document.querySelector('input[name="slackChannels"]');
if (channelInput && !channelInput.value) {
channelInput.value = '#general';
// Reactの状態更新のためイベント発火
channelInput.dispatchEvent(new Event('input', { bubbles: true }));
channelInput.dispatchEvent(new Event('change', { bubbles: true }));
}
3. 送信レベル: APIリクエストを直接書き換え(最重要)
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
if (this._url && this._url.includes('pages.update')) {
if (body && typeof body === 'string') {
const data = JSON.parse(body);
// slackChannelsが空の場合、強制的にデフォルト値を設定
if (data.isSlackEnabled === true && !data.slackChannels) {
data.slackChannels = '#general';
body = JSON.stringify(data);
}
}
}
return originalXHRSend.call(this, body);
};
ポイント: UIで設定できなくても、送信直前にデータを書き換えることで確実に動作
📦 完成したスクリプト
window.addEventListener('load', () => {
// ========================================
// 設定: デフォルトのSlackチャンネル
// ========================================
const DEFAULT_SLACK_CHANNEL = '#general';
// チェックボックスとSlackチャンネルを自動設定
const ensureSlackSettings = () => {
// 1. チェックボックスをON
const checkbox = document.getElementById("idForEditorNavbarBottom");
if (checkbox && !checkbox.checked) {
checkbox.click();
}
// 2. チャンネル入力欄を設定
const channelSelectors = [
'input[name="slackChannels"]',
'input[placeholder*="slack"]',
'input[placeholder*="channel"]',
];
for (const selector of channelSelectors) {
const channelInput = document.querySelector(selector);
if (channelInput && !channelInput.value) {
channelInput.value = DEFAULT_SLACK_CHANNEL;
channelInput.dispatchEvent(new Event('input', { bubbles: true }));
channelInput.dispatchEvent(new Event('change', { bubbles: true }));
break;
}
}
};
// 初期設定
setTimeout(() => ensureSlackSettings(), 1000);
// MutationObserverで監視
const observer = new MutationObserver(() => ensureSlackSettings());
observer.observe(document.body, { childList: true, subtree: true });
// 定期チェック
setInterval(() => ensureSlackSettings(), 100);
// 3. 送信直前にデータを書き換え(最重要)
const originalXHRSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.send = function(body) {
if (this._url && this._url.includes('pages.update')) {
if (body && typeof body === 'string') {
try {
const data = JSON.parse(body);
if (data.isSlackEnabled === true && !data.slackChannels) {
data.slackChannels = DEFAULT_SLACK_CHANNEL;
body = JSON.stringify(data);
}
} catch (e) {
console.error('Parse error:', e);
}
}
}
return originalXHRSend.call(this, body);
};
const originalXHROpen = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, ...args) {
this._url = url;
return originalXHROpen.apply(this, [method, url, ...args]);
};
});
🛠️ 使用した技術・手法
1. MutationObserver API
- DOMの変化を監視
- 動的に追加される要素にも対応
2. イベントインターセプト
- XMLHttpRequest のフック
- 送信前にデータを操作
3. React との相互運用
-
click()メソッドでReactの状態を更新 -
input/changeイベントを発火してReactに通知
4. デバッグ手法
-
console.logによるロギング - ネットワークトラフィックの監視
- リクエストボディの解析
📊 トラブルシューティング履歴
| 問題 | 原因 | 解決策 |
|---|---|---|
| チェックボックスが反映されない | DOM操作だけではReactの状態が更新されない |
element.click() を使用 |
| Ctrl+Sで通知されない |
slackChannels が空文字列 |
XMLHttpRequestをインターセプトして書き換え |
| fetch APIで検知できない | GROWIはXMLHttpRequestを使用 | XMLHttpRequestをフック |
| UIで入力欄が見つからない | セレクタが間違っている | 複数のセレクタで試行 |
⚠️ 注意点
1. パフォーマンス
-
setIntervalで100msごとにチェック - 負荷が気になる場合は間隔を調整(例: 500ms)
2. GROWIのバージョン依存
- API URLやDOM構造が変わる可能性がある
- GROWIのアップデート時に動作確認が必要
3. エラーハンドリング
- JSONパースエラーに対応
-
try-catchで安全に処理
🎓 学んだこと
1. Reactアプリケーションのスクリプト注入
- 単純なDOM操作では不十分
- Reactの状態管理を理解する必要がある
- イベントを適切に発火させることが重要
2. ネットワーク層でのインターセプト
- UIでの操作が難しい場合、送信データを直接操作
- XMLHttpRequestやfetch APIのフックが有効
3. 段階的なデバッグ
- 仮説を立てる
- ログで検証
- 問題を特定
- 解決策を実装
- テスト
🔗 参考資料
📝 まとめ
成果
- ✅ チェックボックスが常に自動的にON
- ✅ Ctrl+Sでの保存時にもSlack通知
- ✅ デフォルトチャンネルの自動設定
- ✅ ユーザーの手間を削減
キーポイント
- Reactアプリでは
click()を使う - UIで解決できない場合はネットワーク層で対処
- 送信データを直接書き換えるのが最も確実
一言
ぜひgrowiの標準機能といて持ってい置いてほしい。
これができることでスラックで検索すればgrowiの記事も基本的にはヒットするはずなので。