Cisco Webex
コミュニケーションツール
Cisco Webex
概要
Cisco Webexは、エンタープライズ向けの総合コラボレーションプラットフォームです。高品質なビデオ会議、チーム連携機能、企業レベルのセキュリティを統合し、最大100,000人まで対応可能な大規模会議機能を提供します。Webex SDK v3、強力なBot開発機能、豊富なWebhook統合により、カスタマイズされたエンタープライズソリューションの構築が可能です。
詳細
Cisco Webex(シスコ ウェベックス)は、1995年に設立されたWebExがCiscoに買収され、現在のWebexプラットフォームに発展したエンタープライズコミュニケーション製品です。世界中の大企業、政府機関、教育機関で広く採用され、特に高品質なビデオ会議とセキュリティ機能で知られています。
2024-2025年には、Webex SDK v3への大規模移行、AI Transcription API、Webex Assistant API、Real-time Media API、Advanced Spaces API、新しいWebhook Events、Contact Center APIの大幅拡張などが実装されました。また、Zero Trust Securityアーキテクチャの強化、Cloud-Native Infrastructure移行、WebRTC最適化が行われ、エンタープライズ環境での安定性と性能が大幅に向上しています。
Webex APIは、JavaScript SDK、Python SDK、.NET SDK、REST API、GraphQL APIを提供し、Bot開発、カスタムアプリケーション開発、システム統合が可能です。また、Adaptive Cards UIやWebex Devicesとの統合により、会議室システムからモバイルまで一貫したエクスペリエンスを提供します。
メリット・デメリット
メリット
- エンタープライズセキュリティ: Zero Trust Security、エンドツーエンド暗号化
- 大規模会議対応: 最大100,000人参加、ウェビナー・ブロードキャスト機能
- 豊富なAPI/SDK: JavaScript、Python、.NET SDK、REST API、GraphQL
- ハイブリッド環境対応: オンプレミス・クラウドのハイブリッド展開
- AI機能統合: リアルタイム転写、Webex Assistant、ノイズ除去
- デバイス統合: Webex Devices、会議室システムとの完全統合
- コンプライアンス: HIPAA、FedRAMP、SOC2等の認証取得
- グローバル展開: 多地域でのデータセンター、低レイテンシ
デメリット
- 高コスト: エンタープライズ向け価格設定、ライセンス体系の複雑さ
- 複雑性: 豊富な機能による設定・管理の複雑さ
- 学習コスト: 管理者・開発者の習得コスト
- Cisco依存: Ciscoエコシステムへの依存度
- 小規模企業向けでない: スタートアップや小規模チームには過剰
- カスタマイズ制限: UI/UXのカスタマイズ範囲制約
主要リンク
- Cisco Webex公式サイト
- Webex Developer Portal
- Webex JavaScript SDK
- Webex API Reference
- Webex Bot Framework
- Webex Webhooks
- Cisco DevNet
書き方の例
Webex JavaScript SDK v3 を使用したBot開発
// Webex JavaScript SDK v3 Bot実装
import { Webex } from '@webex/sdk';
class WebexBot {
constructor(accessToken) {
this.webex = Webex.init({
credentials: {
access_token: accessToken
},
config: {
logger: {
level: 'info'
},
meetings: {
enableExperimentalSupport: true
}
}
});
this.setupEventHandlers();
}
setupEventHandlers() {
// 新しいメッセージイベント
this.webex.messages.on('created', (message) => {
this.handleMessage(message);
});
// メンション通知
this.webex.messages.on('created', (message) => {
if (message.mentionedPeople && message.mentionedPeople.includes(this.webex.people.me.id)) {
this.handleMention(message);
}
});
// 会議イベント
this.webex.meetings.on('meeting:added', (meeting) => {
console.log('🎥 新しい会議が作成されました:', meeting.id);
});
// Space作成イベント
this.webex.rooms.on('created', (room) => {
console.log('🏠 新しいSpaceが作成されました:', room.title);
});
}
async handleMessage(message) {
// 自分のメッセージは無視
if (message.personId === this.webex.people.me.id) return;
console.log(`📩 メッセージ受信: ${message.text}`);
// コマンド処理
if (message.text && message.text.startsWith('/')) {
await this.processCommand(message);
}
// キーワード検出
if (message.text && message.text.toLowerCase().includes('help')) {
await this.sendHelpMessage(message.roomId);
}
// 会議スケジュール要求
if (message.text && message.text.toLowerCase().includes('schedule meeting')) {
await this.scheduleMeeting(message);
}
}
async processCommand(message) {
const [command, ...args] = message.text.slice(1).split(' ');
switch (command.toLowerCase()) {
case 'ping':
await this.sendMessage(message.roomId, '🏓 Pong!');
break;
case 'status':
await this.sendSystemStatus(message.roomId);
break;
case 'weather':
await this.sendWeatherInfo(message.roomId, args[0] || 'Tokyo');
break;
case 'meeting':
await this.handleMeetingCommand(message, args);
break;
case 'space':
await this.handleSpaceCommand(message, args);
break;
case 'people':
await this.listSpaceMembers(message.roomId);
break;
default:
await this.sendMessage(message.roomId, `❓ 不明なコマンド: ${command}. /helpと入力してください。`);
}
}
async sendMessage(roomId, text, attachments = null) {
try {
const messageData = {
roomId: roomId,
text: text
};
if (attachments) {
messageData.attachments = attachments;
}
const response = await this.webex.messages.create(messageData);
return response;
} catch (error) {
console.error('メッセージ送信エラー:', error);
}
}
async sendAdaptiveCard(roomId, cardData) {
try {
const attachments = [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: cardData
}];
await this.sendMessage(roomId, 'カード形式のメッセージ:', attachments);
} catch (error) {
console.error('Adaptive Card送信エラー:', error);
}
}
async sendHelpMessage(roomId) {
const helpCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: '🤖 Webex Bot Commands',
weight: 'Bolder',
size: 'Medium'
},
{
type: 'TextBlock',
text: '利用可能なコマンド:'
},
{
type: 'FactSet',
facts: [
{ title: '/ping', value: '接続テスト' },
{ title: '/status', value: 'システム状態表示' },
{ title: '/weather [city]', value: '天気情報取得' },
{ title: '/meeting create', value: '会議作成' },
{ title: '/space info', value: 'Space情報表示' },
{ title: '/people', value: 'メンバー一覧' }
]
}
],
actions: [
{
type: 'Action.Submit',
title: 'システム状態確認',
data: {
action: 'status'
}
}
]
};
await this.sendAdaptiveCard(roomId, helpCard);
}
async sendSystemStatus(roomId) {
const os = require('os');
const uptimeHours = Math.floor(os.uptime() / 3600);
const uptimeMinutes = Math.floor((os.uptime() % 3600) / 60);
const memoryUsage = Math.round((os.totalmem() - os.freemem()) / os.totalmem() * 100);
const statusCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: '📊 システム状態',
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: [
{ title: '稼働時間', value: `${uptimeHours}時間 ${uptimeMinutes}分` },
{ title: 'メモリ使用率', value: `${memoryUsage}%` },
{ title: 'プラットフォーム', value: `${os.platform()} ${os.arch()}` },
{ title: 'Node.js', value: process.version },
{ title: 'Webex SDK', value: '接続中 ✅' }
]
}
]
};
await this.sendAdaptiveCard(roomId, statusCard);
}
async sendWeatherInfo(roomId, city) {
try {
const response = await fetch(`https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${process.env.WEATHER_API_KEY}&units=metric&lang=ja`);
const weather = await response.json();
if (weather.cod === 200) {
const weatherCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: `🌤️ ${weather.name}の天気`,
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: [
{ title: '天気', value: weather.weather[0].description },
{ title: '気温', value: `${weather.main.temp}°C` },
{ title: '湿度', value: `${weather.main.humidity}%` },
{ title: '風速', value: `${weather.wind.speed} m/s` }
]
}
]
};
await this.sendAdaptiveCard(roomId, weatherCard);
} else {
await this.sendMessage(roomId, '❌ 天気情報の取得に失敗しました');
}
} catch (error) {
console.error('Weather API error:', error);
await this.sendMessage(roomId, '❌ 天気情報取得中にエラーが発生しました');
}
}
async handleMeetingCommand(message, args) {
const subCommand = args[0]?.toLowerCase();
switch (subCommand) {
case 'create':
await this.createMeeting(message.roomId, args.slice(1));
break;
case 'list':
await this.listMeetings(message.roomId);
break;
case 'join':
await this.joinMeeting(message.roomId, args[1]);
break;
default:
await this.sendMessage(message.roomId, '利用可能な会議コマンド: create, list, join');
}
}
async createMeeting(roomId, args) {
try {
const title = args.join(' ') || 'Bot Created Meeting';
const startTime = new Date(Date.now() + 10 * 60 * 1000); // 10分後
const endTime = new Date(startTime.getTime() + 60 * 60 * 1000); // 1時間
const meetingData = {
title: title,
start: startTime.toISOString(),
end: endTime.toISOString(),
timezone: 'Asia/Tokyo',
enabledAutoRecordMeeting: false,
allowAnyUserToBeCoHost: true
};
const meeting = await this.webex.meetings.create(meetingData);
const meetingCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: '🎥 会議が作成されました',
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: [
{ title: 'タイトル', value: meeting.title },
{ title: '開始時刻', value: new Date(meeting.start).toLocaleString('ja-JP') },
{ title: '会議ID', value: meeting.meetingNumber },
{ title: 'Web Link', value: meeting.webLink }
]
}
],
actions: [
{
type: 'Action.OpenUrl',
title: '会議に参加',
url: meeting.webLink
}
]
};
await this.sendAdaptiveCard(roomId, meetingCard);
} catch (error) {
console.error('会議作成エラー:', error);
await this.sendMessage(roomId, '❌ 会議の作成に失敗しました');
}
}
async listMeetings(roomId) {
try {
const meetings = await this.webex.meetings.list({
from: new Date().toISOString(),
to: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString() // 今後1週間
});
if (meetings.items && meetings.items.length > 0) {
const meetingList = meetings.items.slice(0, 5).map(meeting => ({
title: meeting.title,
value: `${new Date(meeting.start).toLocaleString('ja-JP')} - ${meeting.meetingNumber}`
}));
const listCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: '📅 今後の会議一覧',
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: meetingList
}
]
};
await this.sendAdaptiveCard(roomId, listCard);
} else {
await this.sendMessage(roomId, '📅 今後の会議はありません');
}
} catch (error) {
console.error('会議一覧取得エラー:', error);
await this.sendMessage(roomId, '❌ 会議一覧の取得に失敗しました');
}
}
async handleSpaceCommand(message, args) {
const subCommand = args[0]?.toLowerCase();
switch (subCommand) {
case 'info':
await this.getSpaceInfo(message.roomId);
break;
case 'create':
await this.createSpace(message.roomId, args.slice(1));
break;
default:
await this.sendMessage(message.roomId, '利用可能なSpaceコマンド: info, create');
}
}
async getSpaceInfo(roomId) {
try {
const room = await this.webex.rooms.get(roomId);
const spaceCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: '🏠 Space情報',
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: [
{ title: 'タイトル', value: room.title },
{ title: 'タイプ', value: room.type === 'group' ? 'グループ' : 'ダイレクト' },
{ title: '作成日', value: new Date(room.created).toLocaleDateString('ja-JP') },
{ title: 'Space ID', value: room.id }
]
}
]
};
await this.sendAdaptiveCard(roomId, spaceCard);
} catch (error) {
console.error('Space情報取得エラー:', error);
await this.sendMessage(roomId, '❌ Space情報の取得に失敗しました');
}
}
async listSpaceMembers(roomId) {
try {
const memberships = await this.webex.memberships.list({ roomId: roomId });
if (memberships.items && memberships.items.length > 0) {
let memberList = [];
for (const membership of memberships.items.slice(0, 10)) {
const person = await this.webex.people.get(membership.personId);
memberList.push({
title: person.displayName,
value: person.emails[0]
});
}
const membersCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: `👥 メンバー一覧 (${memberships.items.length}人)`,
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: memberList
}
]
};
await this.sendAdaptiveCard(roomId, membersCard);
}
} catch (error) {
console.error('メンバー一覧取得エラー:', error);
await this.sendMessage(roomId, '❌ メンバー一覧の取得に失敗しました');
}
}
async uploadFile(roomId, filePath, fileName) {
const fs = require('fs');
try {
const fileBuffer = fs.readFileSync(filePath);
const fileData = {
roomId: roomId,
files: [fileBuffer],
filename: fileName
};
const response = await this.webex.messages.create(fileData);
console.log(`📎 ファイルアップロード完了: ${fileName}`);
return response;
} catch (error) {
console.error('ファイルアップロードエラー:', error);
}
}
async start() {
try {
await this.webex.once('ready');
console.log('🚀 Webex Bot が起動しました');
// 自分の情報を取得
this.webex.people.me = await this.webex.people.get('me');
console.log(`Bot情報: ${this.webex.people.me.displayName} (${this.webex.people.me.emails[0]})`);
// Webhook登録(本番環境用)
if (process.env.WEBHOOK_URL) {
await this.registerWebhooks();
}
// 定期タスクの設定
this.setupPeriodicTasks();
} catch (error) {
console.error('Bot起動エラー:', error);
}
}
async registerWebhooks() {
try {
// メッセージWebhook
await this.webex.webhooks.create({
name: 'Bot Messages',
targetUrl: process.env.WEBHOOK_URL + '/messages',
resource: 'messages',
event: 'created'
});
// 会議Webhook
await this.webex.webhooks.create({
name: 'Bot Meetings',
targetUrl: process.env.WEBHOOK_URL + '/meetings',
resource: 'meetings',
event: 'created'
});
console.log('✅ Webhook登録完了');
} catch (error) {
console.error('Webhook登録エラー:', error);
}
}
setupPeriodicTasks() {
// 1時間ごとのヘルスチェック
setInterval(async () => {
console.log('⏰ 定期ヘルスチェック実行中...');
}, 3600000);
// 日次レポート(毎日9時)
const now = new Date();
const msUntil9AM = new Date(now.getFullYear(), now.getMonth(), now.getDate() + 1, 9, 0, 0, 0) - now;
setTimeout(() => {
setInterval(async () => {
console.log('📊 日次レポート生成中...');
// 管理SpaceにレポートBOOKS送信(実装例)
}, 24 * 60 * 60 * 1000);
}, msUntil9AM);
}
async stop() {
console.log('🛑 Webex Bot を停止します');
// クリーンアップ処理
}
}
// 使用例
async function main() {
const accessToken = process.env.WEBEX_ACCESS_TOKEN;
if (!accessToken) {
console.error('❌ WEBEX_ACCESS_TOKENが設定されていません');
process.exit(1);
}
const bot = new WebexBot(accessToken);
// グレースフルシャットダウン
process.on('SIGINT', async () => {
console.log('🔄 シャットダウン中...');
await bot.stop();
process.exit(0);
});
await bot.start();
}
main().catch(console.error);
Webex Webhook サーバー実装
// Express.js でのWebhook受信サーバー
const express = require('express');
const crypto = require('crypto');
const { Webex } = require('@webex/sdk');
const app = express();
app.use(express.json());
class WebexWebhookServer {
constructor(accessToken, webhookSecret) {
this.webex = Webex.init({
credentials: {
access_token: accessToken
}
});
this.webhookSecret = webhookSecret;
}
// Webhook署名検証
verifyWebhookSignature(req, res, next) {
const signature = req.headers['x-spark-signature'];
const body = JSON.stringify(req.body);
if (this.webhookSecret) {
const expectedSignature = crypto
.createHmac('sha1', this.webhookSecret)
.update(body)
.digest('hex');
if (signature !== expectedSignature) {
return res.status(401).send('不正な署名');
}
}
next();
}
// メッセージWebhook処理
async handleMessageWebhook(req, res) {
const webhookData = req.body;
try {
// 新しいメッセージの場合のみ処理
if (webhookData.event === 'created') {
const message = await this.webex.messages.get(webhookData.data.id);
// Bot自身のメッセージは無視
if (message.personId === (await this.webex.people.get('me')).id) {
return res.status(200).send('OK');
}
console.log(`📩 Webhook受信: ${message.text}`);
// Bot処理実行
await this.processMessage(message);
}
res.status(200).send('OK');
} catch (error) {
console.error('Webhook処理エラー:', error);
res.status(500).send('サーバーエラー');
}
}
// 会議Webhook処理
async handleMeetingWebhook(req, res) {
const webhookData = req.body;
try {
console.log('🎥 会議Webhook受信:', webhookData);
// 会議作成通知
if (webhookData.event === 'created') {
const meeting = webhookData.data;
await this.notifyMeetingCreated(meeting);
}
res.status(200).send('OK');
} catch (error) {
console.error('会議Webhook処理エラー:', error);
res.status(500).send('サーバーエラー');
}
}
async processMessage(message) {
// CI/CD通知処理
if (message.text && message.text.includes('deployment')) {
await this.handleDeploymentNotification(message);
}
// GitHub統合
if (message.text && message.text.includes('github')) {
await this.handleGitHubIntegration(message);
}
// アラート処理
if (message.text && message.text.toLowerCase().includes('alert')) {
await this.handleAlertMessage(message);
}
}
async handleDeploymentNotification(message) {
// デプロイメント状況をWebexに通知
const deployCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: '🚀 デプロイメント通知',
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: [
{ title: 'ステータス', value: '進行中' },
{ title: '環境', value: 'Production' },
{ title: '開始時刻', value: new Date().toLocaleString('ja-JP') }
]
}
],
actions: [
{
type: 'Action.OpenUrl',
title: 'ログを確認',
url: 'https://your-ci-cd-platform.com/logs'
}
]
};
await this.sendAdaptiveCard(message.roomId, deployCard);
}
async sendAdaptiveCard(roomId, cardData) {
const attachments = [{
contentType: 'application/vnd.microsoft.card.adaptive',
content: cardData
}];
await this.webex.messages.create({
roomId: roomId,
text: 'カード型メッセージ:',
attachments: attachments
});
}
// システム監視統合
async sendSystemAlert(roomId, alertData) {
const severity = alertData.severity || 'warning';
const colors = {
'critical': 'attention',
'warning': 'warning',
'info': 'good'
};
const alertCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: `🚨 システムアラート: ${alertData.title}`,
weight: 'Bolder',
size: 'Medium',
color: colors[severity]
},
{
type: 'TextBlock',
text: alertData.description,
wrap: true
},
{
type: 'FactSet',
facts: [
{ title: '重要度', value: severity.toUpperCase() },
{ title: 'サービス', value: alertData.service },
{ title: '発生時刻', value: new Date(alertData.timestamp).toLocaleString('ja-JP') }
]
}
],
actions: [
{
type: 'Action.OpenUrl',
title: 'ダッシュボードを確認',
url: alertData.dashboardUrl || 'https://your-monitoring-dashboard.com'
}
]
};
await this.sendAdaptiveCard(roomId, alertCard);
}
startServer(port = 3000) {
// Webhook エンドポイント設定
app.post('/webhooks/messages',
this.verifyWebhookSignature.bind(this),
this.handleMessageWebhook.bind(this)
);
app.post('/webhooks/meetings',
this.verifyWebhookSignature.bind(this),
this.handleMeetingWebhook.bind(this)
);
// 外部システム統合エンドポイント
app.post('/api/alert', async (req, res) => {
const { roomId, alertData } = req.body;
try {
await this.sendSystemAlert(roomId, alertData);
res.status(200).json({ success: true });
} catch (error) {
console.error('アラート送信エラー:', error);
res.status(500).json({ error: 'アラート送信に失敗しました' });
}
});
// GitHub Webhook
app.post('/api/github', async (req, res) => {
const githubEvent = req.headers['x-github-event'];
const payload = req.body;
try {
await this.handleGitHubEvent(githubEvent, payload);
res.status(200).send('OK');
} catch (error) {
console.error('GitHub Webhook処理エラー:', error);
res.status(500).send('エラー');
}
});
// ヘルスチェック
app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString()
});
});
app.listen(port, () => {
console.log(`🚀 Webex Webhook サーバーがポート${port}で起動しました`);
});
}
async handleGitHubEvent(eventType, payload) {
const roomId = process.env.GITHUB_NOTIFICATION_ROOM;
if (!roomId) return;
switch (eventType) {
case 'push':
await this.handleGitHubPush(roomId, payload);
break;
case 'pull_request':
await this.handleGitHubPR(roomId, payload);
break;
case 'issues':
await this.handleGitHubIssue(roomId, payload);
break;
}
}
async handleGitHubPush(roomId, payload) {
const pushCard = {
type: 'AdaptiveCard',
version: '1.4',
body: [
{
type: 'TextBlock',
text: `🚀 Push to ${payload.repository.full_name}`,
weight: 'Bolder',
size: 'Medium'
},
{
type: 'FactSet',
facts: [
{ title: 'コミット数', value: payload.commits.length.toString() },
{ title: 'ブランチ', value: payload.ref.split('/').pop() },
{ title: 'プッシュ者', value: payload.pusher.name }
]
},
{
type: 'TextBlock',
text: '**最新のコミット:**',
weight: 'Bolder'
}
]
};
// 最新コミットの詳細を追加
if (payload.commits.length > 0) {
const latestCommit = payload.commits[0];
pushCard.body.push({
type: 'FactSet',
facts: [
{ title: 'メッセージ', value: latestCommit.message },
{ title: 'コミットID', value: latestCommit.id.substring(0, 7) },
{ title: '作成者', value: latestCommit.author.name }
]
});
}
pushCard.actions = [
{
type: 'Action.OpenUrl',
title: '変更を確認',
url: payload.compare
}
];
await this.sendAdaptiveCard(roomId, pushCard);
}
}
// 使用例
async function startWebhookServer() {
const accessToken = process.env.WEBEX_ACCESS_TOKEN;
const webhookSecret = process.env.WEBEX_WEBHOOK_SECRET;
if (!accessToken) {
console.error('❌ WEBEX_ACCESS_TOKENが設定されていません');
process.exit(1);
}
const webhookServer = new WebexWebhookServer(accessToken, webhookSecret);
webhookServer.startServer(process.env.PORT || 3000);
}
startWebhookServer().catch(console.error);
Python SDK Bot開発
# Python Webex Bot実装 - webexteamssdk使用
import os
import asyncio
from webexteamssdk import WebexTeamsAPI
from flask import Flask, request, jsonify
import threading
class WebexPythonBot:
def __init__(self, access_token):
self.api = WebexTeamsAPI(access_token=access_token)
self.bot_id = self.api.people.me().id
def handle_message(self, message_data):
"""メッセージ処理"""
message = self.api.messages.get(message_data['id'])
# Bot自身のメッセージは無視
if message.personId == self.bot_id:
return
print(f"📩 メッセージ受信: {message.text}")
# コマンド処理
if message.text and message.text.startswith('/'):
self.process_command(message)
# キーワード検出
elif message.text and 'help' in message.text.lower():
self.send_help_message(message.roomId)
def process_command(self, message):
"""コマンド処理"""
parts = message.text[1:].split()
command = parts[0].lower()
args = parts[1:] if len(parts) > 1 else []
if command == 'ping':
self.send_message(message.roomId, '🏓 Pong!')
elif command == 'status':
self.send_system_status(message.roomId)
elif command == 'weather':
city = args[0] if args else 'Tokyo'
self.send_weather_info(message.roomId, city)
elif command == 'meeting':
if args and args[0] == 'create':
self.create_meeting(message.roomId, ' '.join(args[1:]))
else:
self.send_message(message.roomId, f"❓ 不明なコマンド: {command}")
def send_message(self, room_id, text, attachments=None):
"""メッセージ送信"""
try:
if attachments:
self.api.messages.create(
roomId=room_id,
text=text,
attachments=attachments
)
else:
self.api.messages.create(roomId=room_id, text=text)
except Exception as e:
print(f"メッセージ送信エラー: {e}")
def send_adaptive_card(self, room_id, card_data):
"""Adaptive Card送信"""
attachments = [{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card_data
}]
self.send_message(room_id, "カード形式のメッセージ:", attachments)
def send_help_message(self, room_id):
"""ヘルプメッセージ送信"""
help_card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "🤖 Webex Python Bot",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "FactSet",
"facts": [
{"title": "/ping", "value": "接続テスト"},
{"title": "/status", "value": "システム状態"},
{"title": "/weather [city]", "value": "天気情報"},
{"title": "/meeting create [title]", "value": "会議作成"}
]
}
]
}
self.send_adaptive_card(room_id, help_card)
def send_system_status(self, room_id):
"""システム状態送信"""
import psutil
import platform
cpu_percent = psutil.cpu_percent()
memory = psutil.virtual_memory()
status_card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "📊 システム状態",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "FactSet",
"facts": [
{"title": "CPU使用率", "value": f"{cpu_percent}%"},
{"title": "メモリ使用率", "value": f"{memory.percent}%"},
{"title": "プラットフォーム", "value": f"{platform.system()} {platform.machine()}"},
{"title": "Python", "value": platform.python_version()}
]
}
]
}
self.send_adaptive_card(room_id, status_card)
def create_meeting(self, room_id, title):
"""会議作成"""
try:
from datetime import datetime, timedelta
# 現在時刻から10分後に1時間の会議を作成
start_time = datetime.now() + timedelta(minutes=10)
end_time = start_time + timedelta(hours=1)
meeting_data = {
'title': title or 'Python Bot Created Meeting',
'start': start_time.isoformat(),
'end': end_time.isoformat(),
'timezone': 'Asia/Tokyo',
'enabledAutoRecordMeeting': False,
'allowAnyUserToBeCoHost': True
}
meeting = self.api.meetings.create(**meeting_data)
meeting_card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "🎥 会議が作成されました",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "FactSet",
"facts": [
{"title": "タイトル", "value": meeting.title},
{"title": "開始時刻", "value": start_time.strftime("%Y-%m-%d %H:%M")},
{"title": "会議番号", "value": meeting.meetingNumber},
{"title": "参加URL", "value": meeting.webLink}
]
}
],
"actions": [
{
"type": "Action.OpenUrl",
"title": "会議に参加",
"url": meeting.webLink
}
]
}
self.send_adaptive_card(room_id, meeting_card)
except Exception as e:
print(f"会議作成エラー: {e}")
self.send_message(room_id, "❌ 会議の作成に失敗しました")
# Flask Webhook サーバー
app = Flask(__name__)
bot = None
@app.route('/webhooks/messages', methods=['POST'])
def handle_message_webhook():
"""メッセージWebhook処理"""
webhook_data = request.json
if webhook_data.get('event') == 'created':
bot.handle_message(webhook_data['data'])
return jsonify({'status': 'success'})
@app.route('/api/alert', methods=['POST'])
def handle_alert():
"""外部システムアラート処理"""
alert_data = request.json
room_id = alert_data.get('roomId')
if not room_id:
return jsonify({'error': 'roomId が必要です'}), 400
try:
alert_card = {
"type": "AdaptiveCard",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": f"🚨 {alert_data.get('title', 'システムアラート')}",
"weight": "Bolder",
"size": "Medium"
},
{
"type": "TextBlock",
"text": alert_data.get('description', ''),
"wrap": True
},
{
"type": "FactSet",
"facts": [
{"title": "重要度", "value": alert_data.get('severity', 'Unknown')},
{"title": "サービス", "value": alert_data.get('service', 'Unknown')},
{"title": "発生時刻", "value": alert_data.get('timestamp', 'Unknown')}
]
}
]
}
bot.send_adaptive_card(room_id, alert_card)
return jsonify({'status': 'success'})
except Exception as e:
print(f"アラート送信エラー: {e}")
return jsonify({'error': 'アラート送信に失敗しました'}), 500
@app.route('/health', methods=['GET'])
def health_check():
"""ヘルスチェック"""
return jsonify({'status': 'healthy', 'timestamp': datetime.now().isoformat()})
def run_flask_app():
"""Flask アプリを別スレッドで実行"""
app.run(host='0.0.0.0', port=int(os.getenv('PORT', 3000)), debug=False)
if __name__ == '__main__':
access_token = os.getenv('WEBEX_ACCESS_TOKEN')
if not access_token:
print("❌ WEBEX_ACCESS_TOKEN が設定されていません")
exit(1)
# Bot初期化
bot = WebexPythonBot(access_token)
print("🚀 Webex Python Bot が起動しました")
# Flask サーバーを別スレッドで起動
flask_thread = threading.Thread(target=run_flask_app)
flask_thread.daemon = True
flask_thread.start()
# メインスレッドは生存し続ける
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
print("🛑 Bot を停止します")
環境変数設定
# .env ファイル
# Webex API設定
WEBEX_ACCESS_TOKEN=your-webex-access-token
WEBEX_WEBHOOK_SECRET=your-webhook-secret
# Webhook URL(本番環境)
WEBHOOK_URL=https://your-domain.com/webhooks
# GitHub統合
GITHUB_TOKEN=your-github-token
GITHUB_NOTIFICATION_ROOM=your-room-id
# 外部API連携
WEATHER_API_KEY=your-weather-api-key
# サーバー設定
PORT=3000
NODE_ENV=production
# データベース(任意)
DATABASE_URL=your-database-url
# Cisco Webex Device統合(任意)
WEBEX_DEVICE_USERNAME=your-device-username
WEBEX_DEVICE_PASSWORD=your-device-password
# Enterprise設定
WEBEX_ORG_ID=your-organization-id
[email protected]
Docker設定例
# Dockerfile
FROM node:18-alpine
WORKDIR /app
# 依存関係のインストール
COPY package*.json ./
RUN npm ci --only=production
# アプリケーションファイルのコピー
COPY . .
# 環境変数
ENV NODE_ENV=production
ENV PORT=3000
# ポート公開
EXPOSE 3000
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# アプリケーション起動
CMD ["npm", "start"]
Docker Compose設定
# docker-compose.yml
version: '3.8'
services:
webex-bot:
build: .
container_name: webex-bot
restart: unless-stopped
ports:
- "3000:3000"
environment:
- WEBEX_ACCESS_TOKEN=${WEBEX_ACCESS_TOKEN}
- WEBEX_WEBHOOK_SECRET=${WEBEX_WEBHOOK_SECRET}
- WEBHOOK_URL=${WEBHOOK_URL}
- WEATHER_API_KEY=${WEATHER_API_KEY}
- GITHUB_TOKEN=${GITHUB_TOKEN}
- PORT=3000
volumes:
- ./logs:/app/logs
networks:
- webex-network
# Optional: Redis for session management
redis:
image: redis:7-alpine
container_name: webex-redis
restart: unless-stopped
volumes:
- redis_data:/data
networks:
- webex-network
# Optional: PostgreSQL for data storage
postgres:
image: postgres:15-alpine
container_name: webex-postgres
restart: unless-stopped
environment:
POSTGRES_DB: webex_bot
POSTGRES_USER: webex
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- webex-network
volumes:
redis_data:
postgres_data:
networks:
webex-network:
driver: bridge
CI/CD パイプライン統合
# .github/workflows/webex-notification.yml
name: Webex Deployment Notification
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
notify-deployment:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Deploy to production
run: |
# デプロイメント処理
echo "Deploying to production..."
- name: Notify Webex on success
if: success()
run: |
curl -X POST "${{ secrets.WEBEX_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{
"roomId": "${{ secrets.WEBEX_ROOM_ID }}",
"alertData": {
"title": "デプロイメント成功",
"description": "本番環境へのデプロイが正常に完了しました",
"severity": "info",
"service": "GitHub Actions",
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"dashboardUrl": "https://github.com/${{ github.repository }}/actions"
}
}'
- name: Notify Webex on failure
if: failure()
run: |
curl -X POST "${{ secrets.WEBEX_WEBHOOK_URL }}" \
-H "Content-Type: application/json" \
-d '{
"roomId": "${{ secrets.WEBEX_ROOM_ID }}",
"alertData": {
"title": "デプロイメント失敗",
"description": "本番環境へのデプロイに失敗しました",
"severity": "critical",
"service": "GitHub Actions",
"timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
"dashboardUrl": "https://github.com/${{ github.repository }}/actions"
}
}'