commit bd3eb883c7965150a89fed9331ad2cc54af0fadb Author: uttili Date: Mon Nov 10 15:20:41 2025 +0000 main.py diff --git a/main.py b/main.py new file mode 100644 index 0000000..8b12381 --- /dev/null +++ b/main.py @@ -0,0 +1,559 @@ +import discord +from discord.ext import commands, tasks +from discord import app_commands +from discord.ui import Button, View +import aiohttp +import asyncio +import platform +import psutil +import os +from datetime import datetime +import logging + +# ロギングの設定 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s', + handlers=[ + logging.FileHandler("bot.log"), + logging.StreamHandler() + ] +) + +# BOTの設定 +TOKEN = 'あなたのDiscord Bot Token' # BOTトークン +CHANNEL_ID = 000000000000000000 # チャンネルID +CHECK_INTERVAL = 30 # 間隔(秒) +LOG_FILE = 'ip_changes.log' # IPアドレス変更ログファイル + +last_ip = None +status_message = None +initialization_complete = False + +intents = discord.Intents.all() +intents.message_content = True +intents.reactions = True + +bot = commands.Bot(command_prefix='!', intents=intents) + +# グローバルIPを取得 +async def get_global_ip(): + try: + async with aiohttp.ClientSession() as session: + async with session.get('https://api.ipify.org/?format=json', timeout=10) as response: + if response.status == 200: + data = await response.json() + return data['ip'] + else: + logging.error(f"IPの取得に失敗しました: ステータスコード {response.status}") + return None + except Exception as e: + logging.error(f"IPの取得中にエラーが発生しました: {e}") + return None + +# ログにIPアドレス変更を記録 +def log_ip_change(old_ip, new_ip): + timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + log_entry = f"[{timestamp}] IP変更: {old_ip} -> {new_ip}\n" + + try: + with open(LOG_FILE, 'a', encoding='utf-8') as log_file: + log_file.write(log_entry) + except Exception as e: + logging.error(f"ログの書き込み中にエラーが発生しました: {e}") + +# サーバーのステータスを取得 +def get_server_status(): + try: + # CPU使用率 + cpu_percent = psutil.cpu_percent(interval=1) + + # メモリ使用率 + memory = psutil.virtual_memory() + memory_percent = memory.percent + memory_used = round(memory.used / (1024 * 1024 * 1024), 2) # GB + memory_total = round(memory.total / (1024 * 1024 * 1024), 2) # GB + + # ディスク使用率 + disk = psutil.disk_usage('/') + disk_percent = disk.percent + disk_used = round(disk.used / (1024 * 1024 * 1024), 2) # GB + disk_total = round(disk.total / (1024 * 1024 * 1024), 2) # GB + + # システム稼働時間 + boot_time = datetime.fromtimestamp(psutil.boot_time()) + uptime = datetime.now() - boot_time + days = uptime.days + hours, remainder = divmod(uptime.seconds, 3600) + minutes, seconds = divmod(remainder, 60) + uptime_str = f"{days}日 {hours}時間 {minutes}分 {seconds}秒" + + # OSバージョン + os_info = platform.platform() + + return { + "cpu_percent": cpu_percent, + "memory_percent": memory_percent, + "memory_used": memory_used, + "memory_total": memory_total, + "disk_percent": disk_percent, + "disk_used": disk_used, + "disk_total": disk_total, + "uptime": uptime_str, + "os_info": os_info + } + except Exception as e: + logging.error(f"サーバーステータスの取得中にエラーが発生しました: {e}") + return None + +# 埋め込みメッセージ +def create_ip_embed(ip, old_ip=None, color=discord.Color.green()): + if not ip: + return discord.Embed( + title="⚠️ エラー", + description="グローバルIPアドレスの取得に失敗しました", + color=discord.Color.red() + ) + + embed = discord.Embed( + title="🌐 グローバルIPアドレス", + description=f"**`{ip}`**", + color=color, + timestamp=datetime.now() + ) + + if old_ip and old_ip != ip: + embed.add_field(name="以前のIPアドレス", value=f"`{old_ip}`", inline=False) + embed.add_field(name="状態", value="⚠️ IPアドレスが変更されました", inline=False) + embed.color = discord.Color.orange() + + embed.set_footer(text="最終更新") + + return embed + +# サーバーステータスの埋め込みメッセージ +def create_status_embed(status_data): + if not status_data: + return discord.Embed( + title="⚠️ エラー", + description="サーバーステータスの取得に失敗しました", + color=discord.Color.red() + ) + + embed = discord.Embed( + title="🖥️ サーバーステータス", + color=discord.Color.blue(), + timestamp=datetime.now() + ) + + # CPU使用率のプログレスバー + cpu_bar = create_progress_bar(status_data["cpu_percent"]) + embed.add_field(name="CPU使用率", value=f"{cpu_bar} {status_data['cpu_percent']}%", inline=False) + + # メモリ使用率のプログレスバー + memory_bar = create_progress_bar(status_data["memory_percent"]) + embed.add_field( + name="メモリ使用率", + value=f"{memory_bar} {status_data['memory_percent']}%\n`{status_data['memory_used']}GB / {status_data['memory_total']}GB`", + inline=False + ) + + # ディスク使用率のプログレスバー + disk_bar = create_progress_bar(status_data["disk_percent"]) + embed.add_field( + name="ディスク使用率", + value=f"{disk_bar} {status_data['disk_percent']}%\n`{status_data['disk_used']}GB / {status_data['disk_total']}GB`", + inline=False + ) + + # システム情報 + embed.add_field(name="システム稼働時間", value=f"`{status_data['uptime']}`", inline=False) + embed.add_field(name="OS情報", value=f"`{status_data['os_info']}`", inline=False) + + embed.set_footer(text="最終更新") + + return embed + +# プログレスバーを作成 +def create_progress_bar(percent, bar_length=15): + filled_length = int(bar_length * percent / 100) + empty_length = bar_length - filled_length + + if percent < 60: + color = "🟢" # 緑 + elif percent < 80: + color = "🟡" # 黄 + else: + color = "🔴" # 赤 + + bar = "▰" * filled_length + "▱" * empty_length + return f"[{bar}] {color}" + +class IPControlView(View): + def __init__(self): + super().__init__(timeout=None) + + @discord.ui.button(label="今すぐ更新", style=discord.ButtonStyle.primary, custom_id="refresh_ip") + async def refresh_button(self, interaction: discord.Interaction, button: discord.ui.Button): + global last_ip + + await interaction.response.defer(ephemeral=True) + + current_ip = await get_global_ip() + + if current_ip is None: + await interaction.followup.send("IPアドレスの取得に失敗しました", ephemeral=True) + return + + if last_ip and last_ip != current_ip: + log_ip_change(last_ip, current_ip) + + old_ip = last_ip + last_ip = current_ip + + embed = create_ip_embed(current_ip, old_ip) + await interaction.message.edit(embed=embed, view=self) + + await interaction.followup.send("IPアドレス情報を更新しました", ephemeral=True) + + @discord.ui.button(label="サーバーステータス", style=discord.ButtonStyle.success, custom_id="server_status") + async def status_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + + status_data = get_server_status() + + if status_data is None: + await interaction.followup.send("サーバーステータスの取得に失敗しました", ephemeral=True) + return + + embed = create_status_embed(status_data) + + view = CloseView() + + await interaction.followup.send(embed=embed, view=view, ephemeral=False) + + @discord.ui.button(label="PROXMOX", style=discord.ButtonStyle.secondary, custom_id="proxmox_link") + async def proxmox_button(self, interaction: discord.Interaction, button: discord.ui.Button): + global last_ip + + await interaction.response.defer(ephemeral=True) + + if not last_ip: + await interaction.followup.send("IPアドレスが取得されていません", ephemeral=True) + return + + proxmox_url = f"https://{last_ip}:8006" + + await interaction.followup.send( + f"Proxmox管理画面にアクセス\n" + f"[Proxmox管理画面]({proxmox_url})\n\n" + f"または直接アクセス: `{proxmox_url}`", + ephemeral=True + ) + +class CloseView(View): + def __init__(self): + super().__init__(timeout=None) + + @discord.ui.button(label="閉じる", style=discord.ButtonStyle.danger, custom_id="close_status") + async def close_button(self, interaction: discord.Interaction, button: discord.ui.Button): + await interaction.response.defer(ephemeral=True) + await interaction.message.delete() + await interaction.followup.send("ステータス表示を終了しました", ephemeral=True) + +async def clear_channel_messages(): + try: + channel = bot.get_channel(CHANNEL_ID) + if channel is None: + logging.error(f"チャンネルが見つかりません: {CHANNEL_ID}") + return False + + deleted = 0 + logging.info("チャンネル内のメッセージを削除中") + + try: + logging.info("最近のメッセージを一削除中") + deleted_messages = await channel.purge(limit=100, bulk=True) + deleted += len(deleted_messages) + logging.info(f"{len(deleted_messages)}件のメッセージの削除完了") + + remaining = [] + async for message in channel.history(limit=10): + remaining.append(message) + + if remaining: + logging.info(f"残り{len(remaining)}件の古いメッセージを削除中") + for message in remaining: + await message.delete() + deleted += 1 + logging.info(f"メッセージを削除(合計: {deleted})") + await asyncio.sleep(0.5) # レート制限回避 + + except discord.Forbidden: + logging.error("Discordのメッセージ削除権限がありません") + return False + except discord.HTTPException as e: + logging.error(f"一括削除中にHTTPエラーが発生: {e}") + try: + async for message in channel.history(limit=100): + await message.delete() + deleted += 1 + logging.info(f"個別削除:メッセージを削除(合計: {deleted})") + await asyncio.sleep(0.5) + except Exception as e2: + logging.error(f"個別削除中にエラーが発生: {e2}") + + logging.info(f"{deleted}件のメッセージを削除") + return True + + except Exception as e: + logging.error(f"チャンネル内メッセージ削除中にエラーが発生: {e}") + return False + +@bot.event +async def on_ready(): + global initialization_complete + + logging.info(f'{bot.user}') + logging.info(f'BOT ID: {bot.user.id}') + logging.info(f'送信先チャンネルID: {CHANNEL_ID}') + logging.info(f'間隔: {CHECK_INTERVAL}秒 ({CHECK_INTERVAL/60}分)') + logging.info('------') + logging.info('グローバルIP検出・変更通知BOT') + logging.info('© 2025 uttili_external_system') + logging.info('https://uttili.com') + logging.info('------') + + if not initialization_complete: + logging.info("チャンネル内メッセージを削除中") + await clear_channel_messages() + + logging.info("メッセージを送信中") + await send_initial_message() + + if not check_ip_change.is_running(): + check_ip_change.start() + logging.info("IP検出開始") + + initialization_complete = True + logging.info("初期化が完了") + else: + if not check_ip_change.is_running(): + check_ip_change.start() + logging.info("IP検出再開") + +@tasks.loop(seconds=CHECK_INTERVAL) +async def check_ip_change(): + global last_ip, status_message + + try: + current_ip = await get_global_ip() + + if current_ip is None: + logging.warning("IPアドレスの取得に失敗") + return + + if last_ip is None: + last_ip = current_ip + logging.info(f"初期IPアドレスを確定: {current_ip}") + + if status_message is None: + await send_initial_message() + else: + await update_ip_message(current_ip) + + elif last_ip != current_ip: + old_ip = last_ip + last_ip = current_ip + + log_ip_change(old_ip, current_ip) + + await update_ip_message(current_ip, old_ip) + + logging.info(f"IPアドレスの変更検出: {old_ip} -> {current_ip}") + else: + await update_ip_message(current_ip) + logging.debug(f"IPアドレスに変更なし: {current_ip}") + + except Exception as e: + logging.error(f"IP検出中に予期しないエラーが発生: {e}") + +@check_ip_change.before_loop +async def before_check_ip_change(): + await bot.wait_until_ready() + logging.info("BOT起動") + +@check_ip_change.error +async def check_ip_change_error(error): + logging.error(f"IP検出中にエラーが発生: {error}") + if not check_ip_change.is_running(): + check_ip_change.restart() + logging.info("IP検出再開") + +async def send_initial_message(): + global last_ip, status_message + + try: + if status_message is not None: + logging.info("既にメッセージが存在するためスキップ") + return + + channel = bot.get_channel(CHANNEL_ID) + if channel is None: + logging.error(f"チャンネル不明: {CHANNEL_ID}") + return + + current_ip = await get_global_ip() + if current_ip is None: + logging.error("IPアドレスの取得に失敗しました") + return + + last_ip = current_ip + + embed = create_ip_embed(current_ip) + + view = IPControlView() + + status_message = await channel.send(embed=embed, view=view) + + logging.info(f"メッセージを送信 Message ID: {status_message.id}") + + except Exception as e: + logging.error(f"メッセージ送信中にエラー: {e}") + +async def update_ip_message(current_ip, old_ip=None): + global status_message + + try: + if status_message is None: + await send_initial_message() + return + + channel = bot.get_channel(CHANNEL_ID) + if channel is None: + logging.error(f"チャンネル不明: {CHANNEL_ID}") + return + + try: + message = await channel.fetch_message(status_message.id) + except discord.NotFound: + logging.warning("ステータスメッセージ不明。新規作成開始。") + status_message = None # status_messageリセット + await send_initial_message() + return + except Exception as e: + logging.error(f"メッセージ取得中にエラーが発生: {e}") + status_message = None + await send_initial_message() + return + + color = discord.Color.orange() if old_ip and old_ip != current_ip else discord.Color.green() + + embed = create_ip_embed(current_ip, old_ip, color) + + view = IPControlView() + + await message.edit(embed=embed, view=view) + + if old_ip and old_ip != current_ip: + logging.info(f"IPメッセージを更新: {old_ip} -> {current_ip}") + else: + logging.debug(f"IPメッセージのタイムスタンプを更新: {current_ip}") + + except discord.NotFound: + logging.warning("メッセージ不明。新規作成開始。") + status_message = None + await send_initial_message() + except discord.Forbidden: + logging.error("メッセージの編集権限がないため動作不可能") + except discord.HTTPException as e: + logging.error(f"メッセージ更新中にHTTPエラーが発生: {e}") + await asyncio.sleep(5) + try: + if status_message: + channel = bot.get_channel(CHANNEL_ID) + message = await channel.fetch_message(status_message.id) + embed = create_ip_embed(current_ip, old_ip, discord.Color.green()) + view = IPControlView() + await message.edit(embed=embed, view=view) + except Exception: + status_message = None + await send_initial_message() + except Exception as e: + logging.error(f"メッセージの更新中に予期しないエラーが発生: {e}") + status_message = None + await send_initial_message() + +@bot.command(name="ipreset") +async def ipreset(ctx): + global status_message, initialization_complete + + if not ctx.author.guild_permissions.administrator: + await ctx.send("❌ このコマンドは管理者権限が必要です", delete_after=5) + return + + await ctx.message.delete() + + await clear_channel_messages() + status_message = None + initialization_complete = False + + initialization_complete = True + await send_initial_message() + + if not check_ip_change.is_running(): + check_ip_change.start() + logging.info("IP検出を開始") + + await ctx.send("✅ チャンネル内を削除し、ステータスを表示しました", delete_after=5) + +@bot.command(name="ipclear") +async def ipclear(ctx): + global status_message + + if not ctx.author.guild_permissions.administrator: + await ctx.send("❌ このコマンドは管理者権限が必要です", delete_after=5) + return + + await ctx.message.delete() + + success = await clear_channel_messages() + + if success: + status_message = None + + await send_initial_message() + + await ctx.send("✅ チャンネルをクリアしました", delete_after=5) + else: + await ctx.send("❌ チャンネルのクリアに失敗しました", delete_after=5) + +@bot.command(name="ipstatus") +async def ipstatus(ctx): + await ctx.message.delete() + + task_status = "実行中" if check_ip_change.is_running() else "停止中" + + ip_status = f"`{last_ip}`" if last_ip else "未取得" + + status_msg = await ctx.send( + f"**IPチェック状態**\n" + f"タスク: {task_status}\n" + f"現在のIP: {ip_status}\n" + f"更新間隔: {CHECK_INTERVAL}秒", + delete_after=10 + ) + +if __name__ == "__main__": + logging.info("BOTを起動中") + logging.info("グローバルIPの監視開始") + + try: + import psutil + except ImportError: + logging.warning("psutilモジュールがインストールされていない") + logging.warning("サーバーステータス機能を使用するには、以下のコマンドでインストール:") + logging.warning("pip install psutil") + logging.warning("続行しますが、サーバーステータス機能は機能しない") + + bot.run(TOKEN) \ No newline at end of file