diff --git a/konabot/plugins/mc_count_player/__init__.py b/konabot/plugins/mc_count_player/__init__.py deleted file mode 100644 index 178789e..0000000 --- a/konabot/plugins/mc_count_player/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -import asyncio -import mcstatus - -from nonebot import on_command -from nonebot.adapters import Event -from nonebot_plugin_alconna import UniMessage -from mcstatus.responses import JavaStatusResponse - -from konabot.common.permsys import require_permission - - -cmd = on_command( - "宾几人", - aliases=set(("宾人数", "mcbingo")), - rule=require_permission("minecraft.bingo.check"), -) - - -def parse_status(motd: str) -> str: - if "[PRE-GAME]" in motd: - return "[✨ 空闲]" - if "[IN-GAME]" in motd: - return "[🕜 游戏中]" - if "[POST-GAME]" in motd: - return "[🕜 游戏中]" - return "[✨ 开放]" - - -def dump_server_status(name: str, status: JavaStatusResponse | BaseException) -> str: - if isinstance(status, JavaStatusResponse): - motd = status.motd.to_plain() - # Bingo Status: [PRE-GAME], [IN-GAME], [POST-GAME] - st = parse_status(motd) - players_sample = status.players.sample or [] - players_sample_suffix = "" - if len(players_sample) > 0: - player_list = [s.name for s in players_sample] - players_sample_suffix = " (" + ", ".join(player_list) + ")" - return f"{name}: {st} {status.players.online} 人在线{players_sample_suffix}" - else: - return f"{name}: 好像没开" - - -@cmd.handle() -async def _(evt: Event): - servers = ( - (mcstatus.JavaServer("play.simpfun.cn", 11495), "小帕 Bingo"), - (mcstatus.JavaServer("bingo.mujica.tech"), "坏枪 Bingo"), - (mcstatus.JavaServer("mc.mujica.tech", 11456), "齿轮盛宴"), - ) - - responses = await asyncio.gather( - *map(lambda s: s[0].async_status(), servers), - return_exceptions=True, - ) - messages = "\n".join( - ( - dump_server_status(n, r) - for n, r in zip(map(lambda s: s[1], servers), responses) - ) - ) - - await UniMessage.text(messages).finish(evt, at_sender=False) diff --git a/konabot/plugins/minecraft_servers/__init__.py b/konabot/plugins/minecraft_servers/__init__.py new file mode 100644 index 0000000..fd1f10e --- /dev/null +++ b/konabot/plugins/minecraft_servers/__init__.py @@ -0,0 +1,131 @@ +import asyncio +import datetime +from typing import Literal +import mcstatus + +from nonebot import on_command +from nonebot.adapters import Event +from nonebot_plugin_alconna import Alconna, Args, UniMessage, on_alconna +from mcstatus.responses import JavaStatusResponse +from nonebot_plugin_apscheduler import scheduler + +from konabot.common.permsys import DepPermManager, require_permission +from konabot.plugins.minecraft_servers.simpfun_server import SimpfunServer + + +cmd = on_command( + "宾几人", + aliases=set(("宾人数", "mcbingo")), + rule=require_permission("minecraft.bingo.check"), +) + + +def parse_status(motd: str) -> str: + if "[PRE-GAME]" in motd: + return "[✨ 空闲]" + if "[IN-GAME]" in motd: + return "[🕜 游戏中]" + if "[POST-GAME]" in motd: + return "[🕜 游戏中]" + return "[✨ 开放]" + + +def dump_server_status(name: str, status: JavaStatusResponse | BaseException) -> str: + if isinstance(status, JavaStatusResponse): + motd = status.motd.to_plain() + # Bingo Status: [PRE-GAME], [IN-GAME], [POST-GAME] + st = parse_status(motd) + players_sample = status.players.sample or [] + players_sample_suffix = "" + if len(players_sample) > 0: + player_list = [s.name for s in players_sample] + players_sample_suffix = " (" + ", ".join(player_list) + ")" + return f"{name}: {st} {status.players.online} 人在线{players_sample_suffix}" + else: + return f"{name}: 好像没开" + + +@cmd.handle() +async def _(evt: Event, pm: DepPermManager): + servers = ( + (mcstatus.JavaServer("play.simpfun.cn", 11495), "小帕 Bingo"), + (mcstatus.JavaServer("bingo.mujica.tech"), "坏枪 Bingo"), + (mcstatus.JavaServer("mc.mujica.tech", 11456), "齿轮盛宴"), + ) + + responses = await asyncio.gather( + *map(lambda s: s[0].async_status(), servers), + return_exceptions=True, + ) + messages = "\n".join( + ( + dump_server_status(n, r) + for n, r in zip(map(lambda s: s[1], servers), responses) + ) + ) + + if await pm.check_has_permission(evt, "minecraft.bingo.manipulate"): + messages += "\n\n---\n\n你可以使用 bingoman start 开启小帕的 bingo 服,用 bingoman stop 关闭小帕的 bingo 服" + + await UniMessage.text(messages).finish(evt, at_sender=False) + + +cmd_bingo_manipulate = on_alconna( + Alconna("bingoman", Args["action", str]), + aliases=("宾服务器", "bingo服"), + rule=require_permission("minecraft.bingo.manipulate"), +) + +actions: dict[str, Literal["start", "stop", "restart", "kill"]] = { + "up": "start", + "down": "stop", + "start": "start", + "stop": "stop", + "开机": "start", + "关机": "stop", + "restart": "restart", + "kill": "kill", + "重启": "restart", +} + + +@cmd_bingo_manipulate.handle() +async def _(action: str, event: Event): + server = SimpfunServer.new() # 使用默认配置管理服务器 + a = actions.get(action.lower().strip()) + if a is None: + await UniMessage.text(f"操作 {action} 不存在").send(event, at_sender=True) + return + resp = await server.power(a) + if resp.code == 200: + await UniMessage.text("好了").send(event, at_sender=True) + else: + await UniMessage.text(f"不好:{resp}").send(event, at_sender=True) + + +@scheduler.scheduled_job("cron", hour="4,23") +async def _(): + server = SimpfunServer.new() + today = datetime.datetime.now() + + # 获取服务器当前状态,重试多次以保证不会误判服务器未开启 + server_up = False + server_players = 0 + for _ in range(3): + mcs = mcstatus.JavaServer("play.simpfun.cn", 11495) + try: + resp = await mcs.async_status() + server_up = True + server_players = resp.players.online + except Exception: + pass + + if today.weekday() == 5 and today.hour < 12: + # 每周六开机一天,保证可以让服务器不被自动销毁 + if not server_up: + await server.power("start") + else: + # 每用一个自然日都会计费,所以要赶在这一天结束之前关服 + # 平时如果没人,也自动关上 + if server_up and server_players == 0: + await server.power("stop") diff --git a/konabot/plugins/minecraft_servers/simpfun_server.py b/konabot/plugins/minecraft_servers/simpfun_server.py new file mode 100644 index 0000000..123d1d6 --- /dev/null +++ b/konabot/plugins/minecraft_servers/simpfun_server.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass +import datetime +from typing import Literal + +import aiohttp +from pydantic import BaseModel + + +class SimpfunServerConfig(BaseModel): + plugin_simpfun_api_key: str = "" + plugin_simpfun_base_url: str = "https://api.simpfun.cn" + plugin_simpfun_instance_id: int = 0 + + +def get_config(): + from nonebot import get_plugin_config + + return get_plugin_config(SimpfunServerConfig) + + +class PowerManageResult(BaseModel): + code: int + status: bool + msg: str + + +class SimpfunServerDetailUtilization(BaseModel): + memory_bytes: int + cpu_absolute: float + disk_bytes: int + network_rx_bytes: int + network_tx_bytes: int + uptime: float + disk_last_check_time: datetime.datetime + + +class SimpfunServerDetailData(BaseModel): + id: int + name: str + is_pro: bool + + status: str + "运行中的话,是 running" + + is_suspended: bool + utilization: SimpfunServerDetailUtilization + + +class SimpfunServerDetailResp(BaseModel): + code: int + data: SimpfunServerDetailData + + +@dataclass +class SimpfunServer: + instance_id: int + api_key: str + base_url: str + + async def power( + self, action: Literal["start", "stop", "restart", "kill"] + ) -> PowerManageResult: + url = f"{self.base_url}/api/ins/{self.instance_id}/power" + + async with aiohttp.ClientSession( + headers={"Authorization": self.api_key} + ) as session: + async with session.get(url, params={"action": action}) as resp: + resp.raise_for_status() + return PowerManageResult.model_validate_json(await resp.read()) + + async def detail(self) -> SimpfunServerDetailResp: + url = f"{self.base_url}/api/ins/{self.instance_id}/power" + + async with aiohttp.ClientSession( + headers={"Authorization": self.api_key} + ) as session: + async with session.get(url) as resp: + resp.raise_for_status() + return SimpfunServerDetailResp.model_validate_json(await resp.read()) + + @staticmethod + def new(config: SimpfunServerConfig | None = None): + if config is None: + config = get_config() + return SimpfunServer( + instance_id=config.plugin_simpfun_instance_id, + api_key=config.plugin_simpfun_api_key, + base_url=config.plugin_simpfun_base_url, + )