From d043a3d0305d734d1340665c4f56f2416a244f03 Mon Sep 17 00:00:00 2001 From: xrh0905 <1014930533@qq.com> Date: Fri, 22 Jul 2022 16:14:05 +0800 Subject: [PATCH 01/46] Use `SO_REUSEADDR` during server port binding (#27) * Add IRC Bot Support for SocketReuse to prevent crash after reboot * IRC Support & SocketReuse * Delete irctest.py * Remove IRC_BOT Entry * Delete chatbridge/impl/irc directory * Add back a empty line * Always Enable SocketReuse * reverted unrelated changes * formatting Co-authored-by: Fallen_Breath --- chatbridge/core/server.py | 1 + 1 file changed, 1 insertion(+) diff --git a/chatbridge/core/server.py b/chatbridge/core/server.py index 7685d58..49f9efc 100644 --- a/chatbridge/core/server.py +++ b/chatbridge/core/server.py @@ -111,6 +111,7 @@ def is_running(self) -> bool: def _main_loop(self): self.__sock = socket.socket() + self.__sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) try: self.__sock.bind(self.server_address) except: From 929d48ffdec18853741f44d3d70a34eafeb464fe Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 23 Aug 2022 01:11:59 +0800 Subject: [PATCH 02/46] less status log spam in cqhttp bot --- chatbridge/impl/cqhttp/entry.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index c46db46..2eb94ec 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -51,10 +51,9 @@ def on_message(self, _, message: str): if chatClient is None: return data = json.loads(message) - if 'status' in data: - self.logger.info('CoolQ return status {}'.format(data['status'])) - elif data['post_type'] == 'message' and data['message_type'] == 'group': + if data['post_type'] == 'message' and data['message_type'] == 'group': if data['anonymous'] is None and data['group_id'] == self.config.react_group_id: + self.logger.info('QQ chat message: {}'.format(data)) args = data['raw_message'].split(' ') if len(args) == 1 and args[0] == '!!help': From c0a2144c47bc7638ba7a8c276b9ed034c5260cb0 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 23 Aug 2022 01:36:41 +0800 Subject: [PATCH 03/46] Use mermaid to draw the flowchart image --- README.md | 18 +++++++++++++++++- topomap.png | Bin 42362 -> 0 bytes 2 files changed, 17 insertions(+), 1 deletion(-) delete mode 100644 topomap.png diff --git a/README.md b/README.md index 04b70fe..fcf807a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,23 @@ See [here](https://github.com/TISUnion/ChatBridge/tree/v1) for chatbridge v1. **NOT compatible with Chatbridge v1** -![topomap](https://raw.githubusercontent.com/TISUnion/ChatBridge/master/topomap.png) +```mermaid +flowchart LR + subgraph Minecraft Host + mcdr1("ChatBridge Client (MCDR plugin)")<--MCDR-->smp[Minecraft Surival Server] + mcdr2("ChatBridge Client (MCDR plugin)")<--MCDR-->cmp[Minecraft Creative Server] + mcdr3("ChatBridge Client (MCDR plugin)")<--MCDR-->smpc[Minecraft Mirror Server] + end + + server(["ChatBridge Server"]) + server<-->mcdr1 + server<-->mcdr2 + server<-->mcdr3 + server<-->cli_client("CLI Client")<--->user[/User/] + server<-->cq_client("ChatBridge CQHttp Client")<-->cqhttp[CQ Http bot]-.-QQ + server<-->khl_client("ChatBridge Khl Client")<-->khl["Kaiheila (Kook)"] + server<-->discord_client("ChatBridge Discord Client")<-->Discord +``` ## Disclaimer diff --git a/topomap.png b/topomap.png deleted file mode 100644 index 95f85c4ce8a52a8dbf1d056680be1090efff3e75..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42362 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%0Ae8uQa_=EWxCXsi7Kd@%HBfUN8e}D>t8A$ z3rwiejytvPJKklaEZw|8;Hmbn4=OClU3cWtF1F0EZZpizQW5fwdeXWp-v3g#7q(O! zHXEpd0z+9VkJQMGR6F#aL~eJcLukZOP#}{d3ZXFx6k>i$!@ZSEH*T_6nE1319q{4yNKzseTP zqRiX3%e+r=n%dWM5a&J|`9yKeLtkHG1s5HKP$R!3%qZ0bl&N1jEA)y>Z-!JTGV>6= zD3A|`f=O|mBNFcSqD7RaM_e>G(D!^%=@ezJ6;?TAqV40#O9v9jc}4WZVDSjoqEOsq zLKyZO`}OJqH}3!D9H8&>pL511VtXa#+lGb?%UycGzD@m+|2v7LL6)?%n~zUlf32p$ z!JU@8)OxXo3y)&Y@&$|4E0mj)w68VW95BgC+GHduC3=J2~wnS=X>!=nNI>vMl?c-!d%LtLg9m z{Q7@J~hlIn(jYXkVxB95DPp3-5oQh4;TE7#is@Ffa&c)rH;f z78ehBz7FXdr<$Dskr<|~tv#zk^-W5*VCo8vj*e!s4J9JmY4T-MsNMa zm#%cH%5-&flF6UpTrhX3bZ--54Q~Z6J2Hu|DWa}i^o_eGkRoD_M&Z}qPS6lW|le*W zua6=pgQ42Tfe`7OdAp4DcWuyUjNeExQq0Zf_li1RwJ+EF@NJnux{5Q4uD>!a3>2ej zQMc|r>qNRKfnBhzPPIF5nzNs&X*Q0P5UaHws_y&^huIwWyJ_qB$Hj8$8*1xw#B z9OeIj5U21J0#|+xPB9dxIR!lYib}X5z;X z*bzP3oJ|X?c9=H*8u}-H52S0v;_I(m1Pf;ZqVr)6NGAT>lfitz=*kv>Lcy*_p6W^I>bKu~U1+Ma zg%Y~G$b9gA+G!ebT|jzEP*2fWmfZWR*S^JJC}}osFfU56`Ha*pcG zOVKWdpThS1_b;zD$8~D`tl}@6;oY!YvU#;Woy=XF`LJUaUI8bKO6lX3q?MW!A-y;Q zDZyX!%$YdUnSGVV`EVYJMuLA$o`bEn(`e09Qh)1vpkq55l*Qe+fbQKpDugSdl zRfg05{(b&K%2Di`KcdSx+C6lNkj%kX-!1@bl zsKLfTjvoPAHnHgpY&%U4!S*Sl5_%BrB1-^SR@%m9@R$#RY z(UO@(uS7!E-md3~LYA$3SP)EYRx)>;b%$Mev~O4dm>GB*_GNf>=MoQoGO=o*Pn@U- z%U4KG*IdOasQ*dM^=i(MX7t3s$>A6`@1%se5%^n@G}7f@FH5yszG4MwQHL~kSVFL_ z_R@-q$CFXNHjT`+3l}bUS}+v}kMV6;IVbu_Xn~AVav!fHQ;~t8VIw?WH5w+SI6Wl{ zTmt!DQf%zgS`AH{wLt8Z=%^Vge6o4-=0+Ix_iHLDo=r;6h>U#6i$mI2(3bS71O0g= z{VTfR4El7Nt$ zbM6g&pE}ypY(Twz8dtuZEf;XBY~{L#(b4Tks9rjW2A+2L%*AxBJF}7x&C31!5OoiqUuZ5fIQv63T3kyAox@;YDUaW+NR @0#mCVy2XhIs3e3!p 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 D{&JSv From 20326211e5e4f0919754d3211fab1462a962b025 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 23 Aug 2022 01:41:13 +0800 Subject: [PATCH 04/46] online command client in flowchart image --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index fcf807a..6e1f2a7 100644 --- a/README.md +++ b/README.md @@ -10,13 +10,12 @@ flowchart LR mcdr1("ChatBridge Client (MCDR plugin)")<--MCDR-->smp[Minecraft Surival Server] mcdr2("ChatBridge Client (MCDR plugin)")<--MCDR-->cmp[Minecraft Creative Server] mcdr3("ChatBridge Client (MCDR plugin)")<--MCDR-->smpc[Minecraft Mirror Server] + online("ChatBridge Online Command Client")<--RCON-->bc[Bungeecord Server] end server(["ChatBridge Server"]) - server<-->mcdr1 - server<-->mcdr2 - server<-->mcdr3 - server<-->cli_client("CLI Client")<--->user[/User/] + server<-->mcdr1 & mcdr2 & mcdr3 & online + server<-->cli_client("CLI Client")<-.->user[/User/] server<-->cq_client("ChatBridge CQHttp Client")<-->cqhttp[CQ Http bot]-.-QQ server<-->khl_client("ChatBridge Khl Client")<-->khl["Kaiheila (Kook)"] server<-->discord_client("ChatBridge Discord Client")<-->Discord From ce564738c88c2f547d80fd858b94969dbaac993e Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 10 Sep 2022 18:01:35 +0800 Subject: [PATCH 05/46] update discord.py now `discord.py>=2.0.0` is required, and message content intent in the discord application dashboard too --- README.md | 2 +- chatbridge/impl/discord/bot.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6e1f2a7..6c8ad13 100644 --- a/README.md +++ b/README.md @@ -119,7 +119,7 @@ Extra configure fields (compared to [CLI client](#cli-client)) Extra requirements (also listed in `/chatbridge/impl/discord/requirements.txt`): ``` -discord.py +discord.py>=2.0.0 ``` Extra configure fields (compared to [CLI client](#cli-client)) diff --git a/chatbridge/impl/discord/bot.py b/chatbridge/impl/discord/bot.py index 9f2102b..4071e99 100644 --- a/chatbridge/impl/discord/bot.py +++ b/chatbridge/impl/discord/bot.py @@ -151,7 +151,10 @@ def format_message_text(msg): def create_bot() -> DiscordBot: config = stored.config - bot = DiscordBot(config.command_prefix) + + intents = discord.Intents.default() + intents.message_content = True + bot = DiscordBot(config.command_prefix, intents=intents) # noinspection PyShadowingBuiltins @bot.command() From ef5433e8d5aee6360c0e0fa2076783419f52d8bf Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 10 Sep 2022 18:02:17 +0800 Subject: [PATCH 06/46] v2.3.0 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index c80d15d..7532343 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.2.0", + "version": "2.3.0", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From bb318d26748d31e0436f24e7bb914cba1d89b4b9 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Wed, 19 Oct 2022 01:28:37 +0800 Subject: [PATCH 07/46] zh_cn Disclaimer --- README.md | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c8ad13..e321e95 100644 --- a/README.md +++ b/README.md @@ -30,10 +30,10 @@ ChatBridge is mainly for custom use of TIS server, especially the bot/command co - Kaiheila client - Online command client -Therefore for these bot and related clients: +Therefore, for these bot and related clients: - Expect hardcoded constants in codes and lack of document/usage/support -- PRs for features will not be accepted, related issues will probably be ignored +- PRs for features will not be accepted, issues complaining something don't work will probably be ignored. No after-sales support - If you want more features, fork this repository and implement them yourself But the basic chatbridge components are within the support range, including: @@ -42,6 +42,27 @@ But the basic chatbridge components are within the support range, including: - CLI server - MCDR plugin +## 免责声明 + +ChatBridge 是一个为 TIS 服务器定制使用的工具,尤其是 bot/指令相关的组件: + +- CQHttp 客户端 +- Discord 客户端 +- Kaiheila 客户端 +- Online 指令客户端 + +因此,对于这些 bot 及相关的客户端: + +- 代码中将会包含若干硬编码常量,缺乏相关的文档/用法等支持 +- 功能方面的 PR 不会被接受,相关的 issue 大概率会被忽略,没有售后 +- 如果你想要更多的功能,建议你去 fork 这个仓库,然后自己实现 + +但基本的 ChatBridge 组件都是在支持范围内,这包括: + +- CLI 客户端 +- CLI 服务端 +- MCDR 插件 + ## Usage Enter `python ChatBridge.pyz` in command line to see possible helps From 0a5b599ff7fd9cc3525464f88a0d29beb470d5f7 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 4 Feb 2023 00:26:53 +0800 Subject: [PATCH 08/46] `enable` switch in mcdr plugin config --- README.md | 1 + chatbridge/impl/mcdr/config.py | 1 + chatbridge/impl/mcdr/mcdr_entry.py | 6 ++++++ 3 files changed, 8 insertions(+) diff --git a/README.md b/README.md index e321e95..18ff92e 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,7 @@ Just put the `.pyz` file into the plugin folder Extra configure fields (compared to [CLI client](#cli-client)) ```json5 + "enable": true, // for switching the functionality of the chatbridge plugin "debug": false, // for switching debug logging on ``` diff --git a/chatbridge/impl/mcdr/config.py b/chatbridge/impl/mcdr/config.py index 605bbb4..1c7cbf0 100644 --- a/chatbridge/impl/mcdr/config.py +++ b/chatbridge/impl/mcdr/config.py @@ -2,4 +2,5 @@ class MCDRClientConfig(ClientConfig): + enable: bool = True debug: bool = False diff --git a/chatbridge/impl/mcdr/mcdr_entry.py b/chatbridge/impl/mcdr/mcdr_entry.py index fcd0b28..58b0831 100644 --- a/chatbridge/impl/mcdr/mcdr_entry.py +++ b/chatbridge/impl/mcdr/mcdr_entry.py @@ -81,6 +81,12 @@ def on_load(server: PluginServerInterface, old_module): except: server.logger.exception('Failed to read the config file! ChatBridge might not work properly') server.logger.error('Fix the configure file and then reload the plugin') + config.enable = False + + if not config.enable: + server.logger.info('ChatBridge is disabled') + return + client = ChatBridgeMCDRClient(config, server) if config.debug: client.logger.set_debug_all(True) From b1b2ea5ec275b8e6663cdede88b3e18168b1f615 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Wed, 15 Feb 2023 01:01:26 +0800 Subject: [PATCH 09/46] v2.4.0 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 7532343..9349513 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.3.0", + "version": "2.4.0", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From 9c3c4a3342c206e95127a40d2b56cfadb8cbf47a Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 28 Feb 2023 02:16:44 +0800 Subject: [PATCH 10/46] fixed cli entry typo close #34 --- chatbridge/cli_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatbridge/cli_entry.py b/chatbridge/cli_entry.py index c0f572a..4631614 100644 --- a/chatbridge/cli_entry.py +++ b/chatbridge/cli_entry.py @@ -50,5 +50,5 @@ def main(): print('{} discord_bot: Start a Discord bot as client'.format(prefix)) print('{} cqhttp_bot: Start a CQ-Http bot as client'.format(prefix)) print('{} kaiheila_bot: Start a Kaiheila bot as client'.format(prefix)) - print('{} online_command: Start a CQ-Http bot as client'.format(prefix)) + print('{} online_command: Start a OnlineCommand bot as client'.format(prefix)) From 4f4915bb9f0f94b25bbfe155c742324602f31fa7 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 28 Feb 2023 02:17:45 +0800 Subject: [PATCH 11/46] CQ bot client json data reading fail-proof fixed #35 --- chatbridge/impl/cqhttp/entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index 2eb94ec..a6cb5e0 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -51,7 +51,7 @@ def on_message(self, _, message: str): if chatClient is None: return data = json.loads(message) - if data['post_type'] == 'message' and data['message_type'] == 'group': + if data.get('post_type') == 'message' and data.get('message_type') == 'group': if data['anonymous'] is None and data['group_id'] == self.config.react_group_id: self.logger.info('QQ chat message: {}'.format(data)) args = data['raw_message'].split(' ') From 24b1cba952b354202e9929f368f206aacd394054 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 28 Feb 2023 02:18:07 +0800 Subject: [PATCH 12/46] v2.4.1 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 9349513..38263d6 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.4.0", + "version": "2.4.1", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From 20728232fc324d630b458dee07755462b611d118 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 14 Mar 2023 00:19:31 +0800 Subject: [PATCH 13/46] custom packet type, and splits broadcast_chat / send_chat --- chatbridge/common/serializer.py | 7 ++++- chatbridge/core/client.py | 45 ++++++++++++++++++++++------- chatbridge/core/network/protocol.py | 5 ++++ chatbridge/impl/cli/cli_client.py | 2 +- chatbridge/impl/cqhttp/entry.py | 2 +- chatbridge/impl/discord/bot.py | 2 +- chatbridge/impl/kaiheila/entry.py | 4 +-- chatbridge/impl/mcdr/mcdr_entry.py | 2 +- 8 files changed, 52 insertions(+), 17 deletions(-) diff --git a/chatbridge/common/serializer.py b/chatbridge/common/serializer.py index 8a86e6c..33f83ef 100644 --- a/chatbridge/common/serializer.py +++ b/chatbridge/common/serializer.py @@ -1,10 +1,15 @@ +from typing import TypeVar, Type + from mcdreforged.api.utils.serializer import Serializable +Self = TypeVar('Self', bound='NoMissingFieldSerializable') + class NoMissingFieldSerializable(Serializable): @classmethod - def deserialize(cls, data: dict, **kwargs): + def deserialize(cls: Type[Self], data: dict, **kwargs) -> Self: kwargs.setdefault('error_at_missing', True) + # noinspection PyTypeChecker return super().deserialize(data, **kwargs) @classmethod diff --git a/chatbridge/core/client.py b/chatbridge/core/client.py index f585606..527df68 100644 --- a/chatbridge/core/client.py +++ b/chatbridge/core/client.py @@ -15,7 +15,7 @@ from chatbridge.core.network import net_util from chatbridge.core.network.basic import ChatBridgeBase, Address from chatbridge.core.network.protocol import ChatBridgePacket, PacketType, AbstractPacket, ChatPayload, \ - KeepAlivePayload, AbstractPayload, CommandPayload + KeepAlivePayload, AbstractPayload, CommandPayload, CustomPayload from chatbridge.core.network.protocol import LoginPacket, LoginResultPacket @@ -234,9 +234,9 @@ def _on_stopped(self): self.__thread_keep_alive.join() self.logger.debug('Joined keep alive thread') - # ---------------- - # Packet Logic - # ---------------- + # --------------------- + # Packet core logic + # --------------------- def _send_packet(self, packet: AbstractPacket): if self._is_connected(): @@ -279,13 +279,22 @@ def send_to(self, type_: str, clients: Union[str, Iterable[str]], payload: Abstr def send_to_all(self, type_: str, payload: AbstractPayload): self.__build_and_send_packet(type_, [], payload, is_broadcast=True) + # ------------------------- + # Packet handlers + # ------------------------- + def _on_packet(self, packet: ChatBridgePacket): + """ + A dispatcher that dispatch the packet based on packet type + """ if packet.type == PacketType.keep_alive: self._on_keep_alive(packet.sender, KeepAlivePayload.deserialize(packet.payload)) - if packet.type == PacketType.chat: + elif packet.type == PacketType.chat: self.on_chat(packet.sender, ChatPayload.deserialize(packet.payload)) - if packet.type == PacketType.command: + elif packet.type == PacketType.command: self.on_command(packet.sender, CommandPayload.deserialize(packet.payload)) + elif packet.type == PacketType.custom: + self.on_custom(packet.sender, CustomPayload.deserialize(packet.payload)) def _on_keep_alive(self, sender: str, payload: KeepAlivePayload): if payload.is_ping(): @@ -301,10 +310,20 @@ def on_chat(self, sender: str, payload: ChatPayload): def on_command(self, sender: str, payload: CommandPayload): pass + def on_custom(self, sender: str, payload: CustomPayload): + pass + + # ------------------------- + # Send packet shortcuts + # ------------------------- + def _send_keep_alive_ping(self): self.send_to(PacketType.keep_alive, self._keep_alive_target(), KeepAlivePayload.ping()) - def send_chat(self, message: str, author: str = ''): + def send_chat(self, target: str, message: str, author: str = ''): + self.send_to(PacketType.chat, target, ChatPayload(author=author, message=message)) + + def broadcast_chat(self, message: str, author: str = ''): self.send_to_all(PacketType.chat, ChatPayload(author=author, message=message)) def send_command(self, target: str, command: str, params: Optional[Union[Serializable, dict]] = None): @@ -313,9 +332,15 @@ def send_command(self, target: str, command: str, params: Optional[Union[Seriali def reply_command(self, target: str, asker_payload: 'CommandPayload', result: Union[Serializable, dict]): self.send_to(PacketType.command, target, CommandPayload.answer(asker_payload, result)) - # -------------- - # Keep Alive - # -------------- + def send_custom(self, target: str, data: dict): + self.send_to(PacketType.chat, target, CustomPayload(data=data)) + + def broadcast_custom(self, data: dict): + self.send_to_all(PacketType.chat, CustomPayload(data=data)) + + # ------------------- + # Keep Alive Impl + # ------------------- def _get_keep_alive_thread_name(self): return 'KeepAlive' diff --git a/chatbridge/core/network/protocol.py b/chatbridge/core/network/protocol.py index 7c0d2e4..cbf5f99 100644 --- a/chatbridge/core/network/protocol.py +++ b/chatbridge/core/network/protocol.py @@ -29,6 +29,7 @@ class PacketType: keep_alive = 'chatbridge.keep_alive' chat = 'chatbridge.chat' command = 'chatbridge.command' + custom = 'chatbridge.custom' class ChatBridgePacket(AbstractPacket): @@ -111,3 +112,7 @@ def answer(cls, asker_payload: 'CommandPayload', result: Union[Serializable, dic params=asker_payload.params, result=result, ) + + +class CustomPayload(AbstractPayload): + data: dict diff --git a/chatbridge/impl/cli/cli_client.py b/chatbridge/impl/cli/cli_client.py index b61687c..618adea 100644 --- a/chatbridge/impl/cli/cli_client.py +++ b/chatbridge/impl/cli/cli_client.py @@ -33,7 +33,7 @@ def console_loop(self): self.logger.info('restart: restart the client') self.logger.info('ping: display ping') else: - self.send_chat(text) + self.broadcast_chat(text) def main(): diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index a6cb5e0..903a0a8 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -70,7 +70,7 @@ def on_message(self, _, message: str): if len(sender) == 0: sender = data['sender']['nickname'] text = html.unescape(data['raw_message'].split(' ', 1)[1]) - chatClient.send_chat(text, sender) + chatClient.broadcast_chat(text, sender) if len(args) == 1 and args[0] == '!!online': self.logger.info('!!online command triggered') diff --git a/chatbridge/impl/discord/bot.py b/chatbridge/impl/discord/bot.py index 4071e99..e99fdf1 100644 --- a/chatbridge/impl/discord/bot.py +++ b/chatbridge/impl/discord/bot.py @@ -104,7 +104,7 @@ async def on_message(self, message: Message): # Chat if message.channel.id == self.config.channel_for_chat: self.logger.info('Chat: {}'.format(msg_debug)) - stored.client.send_chat(message.content, author=message.author.name) + stored.client.broadcast_chat(message.content, author=message.author.name) def add_message(self, data, channel_id, t): self.messages.put(MessageData(data=data, channel=channel_id, type=t)) diff --git a/chatbridge/impl/kaiheila/entry.py b/chatbridge/impl/kaiheila/entry.py index fd72703..cec13e4 100644 --- a/chatbridge/impl/kaiheila/entry.py +++ b/chatbridge/impl/kaiheila/entry.py @@ -117,7 +117,7 @@ async def on_message(self, message: Msg): if channel_id == self.config.channel_for_chat: global chatClient if not message.content.startswith(self.config.command_prefix): - chatClient.send_chat(message.content, author=author) + chatClient.broadcast_chat(message.content, author=author) def add_message(self, data, channel_id, t): self.messages.put(MessageData(data=data, channel=channel_id, type=t)) @@ -222,7 +222,7 @@ def on_command(self, sender: str, payload: CommandPayload): message = '错误代码:{}'.format(result.error_code) khlBot.add_message(message, channel_id, MessageDataType.TEXT) elif payload.command == '!!online': - result = OnlineQueryResult.deserialize(payload.result) + result: OnlineQueryResult = OnlineQueryResult.deserialize(payload.result) khlBot.add_embed('{} online players'.format(config.server_display_name), '\n'.join(result.data), channel_id) diff --git a/chatbridge/impl/mcdr/mcdr_entry.py b/chatbridge/impl/mcdr/mcdr_entry.py index 58b0831..b22eeda 100644 --- a/chatbridge/impl/mcdr/mcdr_entry.py +++ b/chatbridge/impl/mcdr/mcdr_entry.py @@ -58,7 +58,7 @@ def send_chat(message: str, *, author: str = ''): if not client.is_running(): client.start() if client.is_online(): - client.send_chat(message, author) + client.broadcast_chat(message, author) def on_load(server: PluginServerInterface, old_module): From 1b0f1094099245d06abdf35bbf6eff5351600ad7 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 14 Mar 2023 00:20:24 +0800 Subject: [PATCH 14/46] fixed khl bot cannot broadcast !!qq message --- chatbridge/impl/kaiheila/entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatbridge/impl/kaiheila/entry.py b/chatbridge/impl/kaiheila/entry.py index cec13e4..7de8c18 100644 --- a/chatbridge/impl/kaiheila/entry.py +++ b/chatbridge/impl/kaiheila/entry.py @@ -116,7 +116,7 @@ async def on_message(self, message: Msg): self.logger.info(f"{channel_id}: {author}: {message.content}") if channel_id == self.config.channel_for_chat: global chatClient - if not message.content.startswith(self.config.command_prefix): + if message.content.startswith('!!qq ') or not message.content.startswith(self.config.command_prefix): chatClient.broadcast_chat(message.content, author=author) def add_message(self, data, channel_id, t): From 251bb12694fabf441b191dd4eb2616c99f041796 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 14 Mar 2023 00:26:34 +0800 Subject: [PATCH 15/46] cqhttp client support sending any text via custom payload --- chatbridge/impl/cqhttp/entry.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index 903a0a8..e2e3630 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -6,7 +6,7 @@ from chatbridge.common.logger import ChatBridgeLogger from chatbridge.core.client import ChatBridgeClient -from chatbridge.core.network.protocol import ChatPayload, CommandPayload +from chatbridge.core.network.protocol import ChatPayload, CommandPayload, CustomPayload from chatbridge.impl import utils from chatbridge.impl.cqhttp.config import CqHttpConfig from chatbridge.impl.tis.protocol import StatsQueryResult, OnlineQueryResult @@ -162,6 +162,22 @@ def on_command(self, sender: str, payload: CommandPayload): result = OnlineQueryResult.deserialize(payload.result) cq_bot.send_text('====== 玩家列表 ======\n{}'.format('\n'.join(result.data))) + def on_custom(self, sender: str, payload: CustomPayload): + global cq_bot + if cq_bot is None: + return + try: + __example_data = { + 'cqhttp_client.action': 'send_text', + 'text': 'the message you want to send' + } + if payload.data.get('cqhttp_client.action') == 'send_text': + text = payload.data.get('text') + self.logger.info('Triggered custom text, sending message {} to qq'.format(text)) + cq_bot.send_text(text) + except: + self.logger.exception('Error in on_custom()') + def main(): global chatClient, cq_bot From a0131d495c7ceec92aee4ee07a75dcfb4cf9759c Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 14 Mar 2023 00:26:48 +0800 Subject: [PATCH 16/46] v2.5.0 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 38263d6..354ab01 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.4.1", + "version": "2.5.0", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From 6ed1eb2d174832fa725acd126ba1b46295ae40de Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 4 Apr 2023 23:26:04 +0800 Subject: [PATCH 17/46] fix using undocumented api --- .gitignore | 4 +++- chatbridge/impl/utils.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index a4e5bf9..6ffbf03 100644 --- a/.gitignore +++ b/.gitignore @@ -4,11 +4,13 @@ test.py *.log *.pyc -/__pycache__ +__pycache__ StatsHelper.py /stats_helper /more_apis *.zip *.mcdr *.pyz +*.bat +*.sh ChatBridge_*.json diff --git a/chatbridge/impl/utils.py b/chatbridge/impl/utils.py index a77c58c..29cf935 100644 --- a/chatbridge/impl/utils.py +++ b/chatbridge/impl/utils.py @@ -20,7 +20,7 @@ def load_config(config_path: str, config_class: Type[T]) -> T: raise FileNotFoundError(config_path) else: with open(config_path, encoding='utf8') as file: - config.update_from(json.load(file)) + vars(config).update(vars(config_class.deserialize(json.load(file)))) with open(config_path, 'w', encoding='utf8') as file: json.dump(config.serialize(), file, ensure_ascii=False, indent=4) return config From e60f859328cc03bc543d013bab9739339b528003 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Tue, 4 Apr 2023 23:26:17 +0800 Subject: [PATCH 18/46] v2.5.1 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 354ab01..fac8828 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.5.0", + "version": "2.5.1", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From 5e6e55f5de85a047de38cda801dd7c6abe20335d Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 29 Oct 2023 14:26:08 +0800 Subject: [PATCH 19/46] up-to-date online client bungee command impl --- chatbridge/impl/online/entry.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/chatbridge/impl/online/entry.py b/chatbridge/impl/online/entry.py index 2b3fe7d..34751b3 100644 --- a/chatbridge/impl/online/entry.py +++ b/chatbridge/impl/online/entry.py @@ -3,6 +3,7 @@ """ import collections import functools +import re import traceback from concurrent.futures.thread import ThreadPoolExecutor from threading import Lock @@ -62,11 +63,10 @@ def handle_minecraft(updater: Callable[[str, Collection[str]], Any], server: Rco @staticmethod def handle_bungee(updater: Callable[[str, Collection[str]], Any], respond: str): for line in respond.splitlines(): - if not line.startswith('Total players online:'): - server_name = line.split('] (', 1)[0][1:] - player_list = set(line.split('): ')[-1].split(', ')) - if '' in player_list: - player_list.remove('') + matched = re.fullmatch(r'\[([^]]+)] \(\d+\): (.*)', line) + if matched: + server_name = matched.group(1) + player_list = set(filter(None, matched.group(2).split(', '))) updater(server_name, player_list) @staticmethod @@ -98,7 +98,7 @@ def updater(name: str, players: Collection[str]): for server in config.server_list: pool.submit(self.query_server, server, 'list', lambda data, svr=server: self.handle_minecraft(updater, svr, data)) for server in config.bungeecord_list: - pool.submit(self.query_server, server, 'glist', lambda data: self.handle_bungee(updater, data)) + pool.submit(self.query_server, server, 'glist all', lambda data: self.handle_bungee(updater, data)) counter_sorted = sorted([(key, value) for key, value in counter.items()], key=functools.cmp_to_key(self.server_comparator)) player_set_all = set() From 01d5b37dbc4dc5cad0747a095e3376512c1b8ad1 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 29 Oct 2023 14:47:50 +0800 Subject: [PATCH 20/46] !!online command on mcdr client --- chatbridge/impl/mcdr/client.py | 18 +++++++++++++++--- chatbridge/impl/mcdr/config.py | 3 +++ chatbridge/impl/mcdr/mcdr_entry.py | 12 ++++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/chatbridge/impl/mcdr/client.py b/chatbridge/impl/mcdr/client.py index a8819f5..ba96ecd 100644 --- a/chatbridge/impl/mcdr/client.py +++ b/chatbridge/impl/mcdr/client.py @@ -5,7 +5,7 @@ from chatbridge.core.client import ChatBridgeClient from chatbridge.core.network.protocol import ChatPayload, CommandPayload from chatbridge.impl.mcdr.config import MCDRClientConfig -from chatbridge.impl.tis.protocol import StatsQueryResult +from chatbridge.impl.tis.protocol import StatsQueryResult, OnlineQueryResult class ChatBridgeMCDRClient(ChatBridgeClient): @@ -38,9 +38,10 @@ def on_chat(self, sender: str, payload: ChatPayload): self.server.say(RText('[{}] {}'.format(sender, payload.formatted_str()), RColor.gray)) def on_command(self, sender: str, payload: CommandPayload): + is_ask = not payload.responded command = payload.command result: Optional[Serializable] = None - if command.startswith('!!stats '): + if command.startswith('!!stats '): # !!stats request try: import stats_helper except (ImportError, ModuleNotFoundError): @@ -69,6 +70,17 @@ def on_command(self, sender: str, payload: CommandPayload): result = StatsQueryResult.create(stats_name, lines[1:-1], total) else: result = StatsQueryResult.unknown_stat() + elif command == '!!online': # !!online response + player = payload.params.get('player') + if player is None: + self.logger.warning('No player in params, params {}'.format(payload.params)) + else: + result: OnlineQueryResult = OnlineQueryResult.deserialize(payload.result) + for line in result.data: + self.server.tell(player, line) - if result is not None: + if is_ask and result is not None: self.reply_command(sender, payload, result) + + def query_online(self, client_to_query_online: str, player: str): + self.send_command(client_to_query_online, '!!online', params={'player': player}) diff --git a/chatbridge/impl/mcdr/config.py b/chatbridge/impl/mcdr/config.py index 1c7cbf0..271346f 100644 --- a/chatbridge/impl/mcdr/config.py +++ b/chatbridge/impl/mcdr/config.py @@ -1,6 +1,9 @@ +from typing import Optional + from chatbridge.core.config import ClientConfig class MCDRClientConfig(ClientConfig): enable: bool = True debug: bool = False + client_to_query_online: Optional[str] = None diff --git a/chatbridge/impl/mcdr/mcdr_entry.py b/chatbridge/impl/mcdr/mcdr_entry.py index b22eeda..1f5f695 100644 --- a/chatbridge/impl/mcdr/mcdr_entry.py +++ b/chatbridge/impl/mcdr/mcdr_entry.py @@ -33,6 +33,17 @@ def display_status(source: CommandSource): source.reply(tr('status.info', client.is_online(), client.get_ping_text())) +def query_online(source: CommandSource): + if config.client_to_query_online is None: + source.reply('client_to_query_online unset') + return + + if client is not None: + client.query_online(config.client_to_query_online, source.player) + else: + source.reply(tr('status.not_init')) + + @new_thread('ChatBridge-restart') def restart_client(source: CommandSource): with cb_lock: @@ -98,6 +109,7 @@ def on_load(server: PluginServerInterface, old_module): then(Literal('status').runs(display_status)). then(Literal('restart').runs(restart_client)) ) + server.register_command(Literal('!!online').runs(query_online)) @new_thread('ChatBridge-start') def start(): From 6441c3df8838ad87b645d26d63c2bb5515e9047a Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 29 Oct 2023 14:26:20 +0800 Subject: [PATCH 21/46] v2.5.2 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index fac8828..e8ad98d 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.5.1", + "version": "2.5.2", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From baded43607e29a42093cc79b91cf9d8bb9767d4f Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Thu, 7 Dec 2023 15:46:44 +0800 Subject: [PATCH 22/46] fixed cqhttp bot `anonymous` message data thing fixed #40, closed #39 --- chatbridge/impl/cqhttp/entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index e2e3630..b76b6d4 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -52,7 +52,7 @@ def on_message(self, _, message: str): return data = json.loads(message) if data.get('post_type') == 'message' and data.get('message_type') == 'group': - if data['anonymous'] is None and data['group_id'] == self.config.react_group_id: + if data.get('anonymous') is None and data['group_id'] == self.config.react_group_id: self.logger.info('QQ chat message: {}'.format(data)) args = data['raw_message'].split(' ') From 51d72eb875cd2c90b98f491248f0d87ee89e5958 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 23 Dec 2023 02:45:43 +0800 Subject: [PATCH 23/46] docker image support we use python3.9 cuz the khl bot lib does not work in 3.10+ due to the `asyncio.Queue` change --- .dockerignore | 2 ++ .github/workflows/image.yml | 49 +++++++++++++++++++++++++++++++ README.md | 17 +++++++++++ chatbridge/impl/cli/cli_server.py | 8 ++++- chatbridge/impl/cqhttp/entry.py | 1 + chatbridge/impl/discord/entry.py | 3 +- chatbridge/impl/kaiheila/entry.py | 1 + chatbridge/impl/online/entry.py | 7 ++++- chatbridge/impl/utils.py | 20 +++++++++++++ docker/Dockerfile | 26 ++++++++++++++++ docker/docker-compose.yml | 19 ++++++++++++ 11 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 .dockerignore create mode 100644 .github/workflows/image.yml create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a295864 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +*.pyc +__pycache__ diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml new file mode 100644 index 0000000..b05e4c6 --- /dev/null +++ b/.github/workflows/image.yml @@ -0,0 +1,49 @@ +name: Docker Image + +on: + push: + paths: + - ".github/workflows/image.yml" + - "chatbridge/**" + - "lang/**" + - "__main__.py" + - "LICENSE" + - "mcdreforged.plugin.json" + - "**/requirements.txt" + +jobs: + image: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to Docker Hub + uses: docker/login-action@v2 + with: + username: fallenbreath + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Docker meta + id: meta + uses: docker/metadata-action@v4 + with: + images: | + fallenbreath/chatbridge + tags: | + type=ref,event=branch + type=semver,pattern={{version}} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 + file: ./docker/Dockerfile + context: . + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} diff --git a/README.md b/README.md index 18ff92e..f276d3e 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,23 @@ Enter `python ChatBridge.pyz` in command line to see possible helps At launch, if the configure file is missing, chatbridge will automatically generate a default one and exit +## Docker Image + +[![Docker](https://img.shields.io/docker/v/fallenbreath/chatbridge/latest)](https://hub.docker.com/r/fallenbreath/chatbridge) + +Docker Hub image: [`fallenbreath/chatbridge`](https://hub.docker.com/r/fallenbreath/chatbridge) + +Image name examples: + +- `fallenbreath/chatbridge:latest` +- `fallenbreath/chatbridge:v2.5.3` + +Working directory: `/app` + +Example usages: `docker run --rm fallenbreath/chatbridge:latest server` + +See the [./docker](docker) directory in the repository for more details and docker compose example + ## Requirement Python 3.6+ required diff --git a/chatbridge/impl/cli/cli_server.py b/chatbridge/impl/cli/cli_server.py index 893a0f1..bbc263c 100644 --- a/chatbridge/impl/cli/cli_server.py +++ b/chatbridge/impl/cli/cli_server.py @@ -1,3 +1,4 @@ +import sys import threading import time import traceback @@ -87,7 +88,12 @@ def main(): print('- Client #{}: name = {}, password = {}'.format(i + 1, client_info.name, client_info.password)) server.add_client(client_info) server.start() - server.console_loop() + + if sys.stdin.isatty(): + server.console_loop() + else: + utils.wait_until_terminate() + server.stop() if __name__ == '__main__': diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index b76b6d4..a43a7dd 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -184,6 +184,7 @@ def main(): config = utils.load_config(ConfigFile, CqHttpConfig) chatClient = CqHttpChatBridgeClient.create(config) utils.start_guardian(chatClient) + utils.register_exit_on_termination() print('Starting CQ Bot') cq_bot = CQBot(config) cq_bot.start() diff --git a/chatbridge/impl/discord/entry.py b/chatbridge/impl/discord/entry.py index adce48a..e139c57 100644 --- a/chatbridge/impl/discord/entry.py +++ b/chatbridge/impl/discord/entry.py @@ -13,12 +13,13 @@ def main(): stored.client = DiscordChatClient.create(stored.config) stored.bot = bot.create_bot() utils.start_guardian(stored.client) + utils.register_exit_on_termination() try: stored.bot.start_running() except (KeyboardInterrupt, SystemExit): stored.client.stop() - except: + except Exception: print(traceback.format_exc()) print('Bye~') diff --git a/chatbridge/impl/kaiheila/entry.py b/chatbridge/impl/kaiheila/entry.py index 7de8c18..3142fd0 100644 --- a/chatbridge/impl/kaiheila/entry.py +++ b/chatbridge/impl/kaiheila/entry.py @@ -231,6 +231,7 @@ def main(): config = utils.load_config(ConfigFile, KaiHeiLaConfig) chatClient = KhlChatBridgeClient.create(config) utils.start_guardian(chatClient) + utils.register_exit_on_termination() print('Starting KHL Bot') khlBot = createKaiHeiLaBot() khlBot.startRunning() diff --git a/chatbridge/impl/online/entry.py b/chatbridge/impl/online/entry.py index 34751b3..e1b8560 100644 --- a/chatbridge/impl/online/entry.py +++ b/chatbridge/impl/online/entry.py @@ -4,6 +4,7 @@ import collections import functools import re +import sys import traceback from concurrent.futures.thread import ThreadPoolExecutor from threading import Lock @@ -135,7 +136,11 @@ def main(): config = utils.load_config(ClientConfigFile, OnlineConfig) chatClient = OnlineChatClient.create(config) utils.start_guardian(chatClient) - console_input_loop() + if sys.stdin.isatty(): + console_input_loop() + else: + utils.wait_until_terminate() + chatClient.stop() if __name__ == '__main__': diff --git a/chatbridge/impl/utils.py b/chatbridge/impl/utils.py index 29cf935..e8881f4 100644 --- a/chatbridge/impl/utils.py +++ b/chatbridge/impl/utils.py @@ -1,5 +1,8 @@ import json import os +import queue +import signal +import sys import time from threading import Thread from typing import Type, TypeVar, Callable @@ -40,3 +43,20 @@ def loop(): thread = Thread(name='ChatBridge Guardian', target=loop, daemon=True) thread.start() return thread + + +def wait_until_terminate(): + q = queue.Queue() + signal.signal(signal.SIGINT, lambda s, _: q.put(s)) + signal.signal(signal.SIGTERM, lambda s, _: q.put(s)) + sig = q.get() + print('Interrupted with {} ({})'.format(signal.Signals(sig).name, sig)) + + +def register_exit_on_termination(): + def callback(sig, _): + print('Interrupted with {} ({}), exiting'.format(signal.Signals(sig).name, sig)) + sys.exit(0) + + signal.signal(signal.SIGINT, callback) + signal.signal(signal.SIGTERM, callback) diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..f3addd3 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,26 @@ +ARG PYTHON_VERSION=3.9 + +FROM python:${PYTHON_VERSION} as builder + +RUN pip3 install mcdreforged + +COPY chatbridge /build/chatbridge +COPY lang /build/lang +COPY __main__.py LICENSE mcdreforged.plugin.json requirements.txt /build/ +RUN cd /build \ + && mcdreforged pack \ + && find . -name "requirements.txt" -exec cat '{}' \; > requirements.all.txt + +FROM python:${PYTHON_VERSION}-slim + +COPY --from=builder /build/requirements.all.txt /app/ +RUN apt-get update \ + && apt-get install -y --no-install-recommends gcc build-essential libffi-dev \ + && rm -rf /var/lib/apt/lists/* \ + && pip3 install -r /app/requirements.all.txt \ + && pip3 cache purge \ + && apt-get purge -y --auto-remove gcc build-essential libffi-dev + +COPY --from=builder /build/ChatBridge.pyz /app/ +WORKDIR /app +ENTRYPOINT ["python3", "ChatBridge.pyz"] diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..3336580 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3' +services: + server: + container_name: chatbridge_server + restart: unless-stopped + image: fallenbreath/chatbridge:latest + command: server + volumes: + - ./ChatBridge_server.json:/app/ChatBridge_server.json + ports: + - '30001:30001' + + khl_bot: + container_name: chatbridge_khl_bot + restart: unless-stopped + image: fallenbreath/chatbridge:latest + command: kaiheila_bot + volumes: + - ./ChatBridge_kaiheila.json:/app/ChatBridge_kaiheila.json From d3585488ac64b15b70b3e47961997fefe1cc8ac8 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 23 Dec 2023 15:55:21 +0800 Subject: [PATCH 24/46] v2.5.3 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index e8ad98d..133ad87 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.5.2", + "version": "2.5.3", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From bc89fc609c1a59506f6bed0f4f4fb6d03845d6c4 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 04:21:45 +0800 Subject: [PATCH 25/46] add satori bot support --- chatbridge/cli_entry.py | 6 + chatbridge/impl/satori/__init__.py | 0 chatbridge/impl/satori/config.py | 12 ++ chatbridge/impl/satori/entry.py | 206 ++++++++++++++++++++++++ chatbridge/impl/satori/requirements.txt | 1 + chatbridge/impl/utils.py | 6 +- 6 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 chatbridge/impl/satori/__init__.py create mode 100644 chatbridge/impl/satori/config.py create mode 100644 chatbridge/impl/satori/entry.py create mode 100644 chatbridge/impl/satori/requirements.txt diff --git a/chatbridge/cli_entry.py b/chatbridge/cli_entry.py index 4631614..5ad540f 100644 --- a/chatbridge/cli_entry.py +++ b/chatbridge/cli_entry.py @@ -25,6 +25,11 @@ def cqhttp_bot(): entry.main() +def satori_bot(): + from chatbridge.impl.satori import entry + entry.main() + + def online_command(): from chatbridge.impl.online import entry entry.main() @@ -49,6 +54,7 @@ def main(): print('{} server: Start the ChatBridge server'.format(prefix)) print('{} discord_bot: Start a Discord bot as client'.format(prefix)) print('{} cqhttp_bot: Start a CQ-Http bot as client'.format(prefix)) + print('{} satori_bot: Start a Satori bot as client'.format(prefix)) print('{} kaiheila_bot: Start a Kaiheila bot as client'.format(prefix)) print('{} online_command: Start a OnlineCommand bot as client'.format(prefix)) diff --git a/chatbridge/impl/satori/__init__.py b/chatbridge/impl/satori/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/chatbridge/impl/satori/config.py b/chatbridge/impl/satori/config.py new file mode 100644 index 0000000..bd54117 --- /dev/null +++ b/chatbridge/impl/satori/config.py @@ -0,0 +1,12 @@ +from chatbridge.core.config import ClientConfig + + +class SatoriConfig(ClientConfig): + ws_address: str = '127.0.0.1' + ws_port: int = 6700 + ws_path: str = '' + satori_token: str = '' + react_channel_id: int = 12345 + chatbridge_message_prefix: str = '!!qq' + client_to_query_stats: str = 'MyClient1' + client_to_query_online: str = 'MyClient2' diff --git a/chatbridge/impl/satori/entry.py b/chatbridge/impl/satori/entry.py new file mode 100644 index 0000000..74f3485 --- /dev/null +++ b/chatbridge/impl/satori/entry.py @@ -0,0 +1,206 @@ +import asyncio +from typing import Optional + +from satori import WebsocketsInfo, Event, EventType +from satori.client import App, Account, ApiInfo + +from chatbridge.common.logger import ChatBridgeLogger +from chatbridge.core.client import ChatBridgeClient +from chatbridge.core.network.protocol import ChatPayload, CommandPayload, CustomPayload +from chatbridge.impl import utils +from chatbridge.impl.cqhttp.entry import CQHelpMessage, StatsHelpMessage +from chatbridge.impl.satori.config import SatoriConfig +from chatbridge.impl.tis.protocol import StatsQueryResult, OnlineQueryResult + +ConfigFile = 'ChatBridge_satori.json' + +config: SatoriConfig +cb_client: Optional['SatoriChatBridgeClient'] = None +satori_client: Optional['SatoriClient'] = None + + +class SatoriClient: + def __init__(self): + self.app = App(WebsocketsInfo( + host=config.ws_address, + port=config.ws_port, + path=config.ws_path, + token=config.satori_token, + )) + self.logger = ChatBridgeLogger('Satori', file_handler=cb_client.logger.file_handler) + self.register_satori_hooks() + + self.__message_queue: asyncio.Queue[Optional[str]] = asyncio.Queue() + self.__loop: Optional[asyncio.AbstractEventLoop] = None + + def register_satori_hooks(self): + self.logger.info('Registering satori hooks') + + @self.app.register_on(EventType.MESSAGE_CREATED) + async def listen(account: Account, event: Event): + self.logger.debug('Satori MESSAGE_CREATED account={} event={}'.format(account, event)) + if event.channel is None or event.message is None or event.user is None: + return + if event.channel.id != str(config.react_channel_id): + return + self.logger.info('Satori chat message event: {}'.format(event)) + + msg = event.message.content + args = msg.split(' ') + + async def send_text(s: str): + await account.send_message(event.channel.id, s) + + if len(args) == 1 and args[0] == '!!help': + self.logger.info('!!help command triggered') + await send_text(CQHelpMessage) + + if len(args) == 1 and args[0] == '!!ping': + self.logger.info('!!ping command triggered') + await send_text('pong!!') + + if len(args) >= 2 and args[0] == '!!mc': + self.logger.info('!!mc command triggered') + sender = event.user.nick or event.user.name + cb_client.broadcast_chat(msg.split(' ', 1)[-1], sender) + + if len(args) == 1 and args[0] == '!!online': + self.logger.info('!!online command triggered') + if cb_client.is_online(): + command = args[0] + client = config.client_to_query_online + self.logger.info('Sending command "{}" to client {}'.format(command, client)) + cb_client.send_command(client, command) + else: + await send_text('ChatBridge 客户端离线') + + if len(args) >= 1 and args[0] == '!!stats': + self.logger.info('!!stats command triggered') + command = '!!stats rank ' + ' '.join(args[1:]) + if len(args) == 0 or len(args) - int(command.find('-bot') != -1) != 3: + await send_text(StatsHelpMessage) + return + if cb_client.is_online: + client = config.client_to_query_stats + self.logger.info('Sending command "{}" to client {}'.format(command, client)) + cb_client.send_command(client, command) + else: + await send_text('ChatBridge 客户端离线') + + async def __send_text_one(self, text: str): + account = Account('', 'chatbridge', ApiInfo( + host=config.ws_address, + port=config.ws_port, + path=config.ws_path, + token=config.satori_token, + )) + await account.send_message(str(config.react_channel_id), text) + + async def __send_text_long(self, text: str): + msg = '' + length = 0 + lines = text.rstrip().splitlines(keepends=True) + for i in range(len(lines)): + msg += lines[i] + length += len(lines[i]) + if i == len(lines) - 1 or length + len(lines[i + 1]) > 500: + await self.__send_text_one(msg) + msg = '' + length = 0 + + async def __messanger_loop(self): + while True: + msg = await self.__message_queue.get() + if msg is None: + break + try: + await self.__send_text_long(msg) + except Exception: + self.logger.exception('messanger loop error') + + async def main(self): + self.__loop = asyncio.get_event_loop() + t = asyncio.create_task(self.__messanger_loop()) + await self.app.run_async() + await t + + def submit_text(self, s: str): + if self.__loop is None: + self.logger.warning('submit_message is called when event loop does not exists yet') + return + + asyncio.run_coroutine_threadsafe(self.__message_queue.put(s), self.__loop) + + def shutdown(self): + if self.__loop is not None: + asyncio.run_coroutine_threadsafe(self.__message_queue.put(None), self.__loop) + + +class SatoriChatBridgeClient(ChatBridgeClient): + def on_chat(self, sender: str, payload: ChatPayload): + if satori_client is None: + return + + parts = payload.message.split(' ', 1) + if len(parts) != 2 or (len(config.chatbridge_message_prefix) > 0 and parts[0] != config.chatbridge_message_prefix): + return + + payload.message = parts[1] + self.logger.info('Triggered command, sending message {} to satori'.format(payload.formatted_str())) + satori_client.submit_text('[{}] {}'.format(sender, payload.formatted_str())) + + def on_command(self, sender: str, payload: CommandPayload): + if satori_client is None: + return + if not payload.responded: + return + if payload.command.startswith('!!stats '): + result = StatsQueryResult.deserialize(payload.result) + if result.success: + messages = ['====== {} ======'.format(result.stats_name)] + messages.extend(result.data) + messages.append('总数:{}'.format(result.total)) + satori_client.submit_text('\n'.join(messages)) + elif result.error_code == 1: + satori_client.submit_text('统计信息未找到') + elif result.error_code == 2: + satori_client.submit_text('StatsHelper 插件未加载') + elif payload.command == '!!online': + result = OnlineQueryResult.deserialize(payload.result) + satori_client.submit_text('====== 玩家列表 ======\n{}'.format('\n'.join(result.data))) + + def on_custom(self, sender: str, payload: CustomPayload): + if satori_client is None: + return + if payload.data.get('cqhttp_client.action') == 'send_text': + text = payload.data.get('text') + self.logger.info('Triggered custom text, sending message {} to satori'.format(text)) + satori_client.submit_text(text) + + +queue = asyncio.Queue() + + +def main(): + global config, cb_client, satori_client + config = utils.load_config(ConfigFile, SatoriConfig) + + cb_client = SatoriChatBridgeClient.create(config) + satori_client = SatoriClient() + + def exit_callback(): + satori_client.shutdown() + cb_client.stop() + + utils.start_guardian(cb_client) + utils.register_exit_on_termination(exit_callback) + + print('Starting Satori Bot') + # cannot use asyncio.run() to create a new one + # or some "got Future attached to a different loop" error will raise + asyncio.get_event_loop().run_until_complete(satori_client.main()) + print('Bye~') + + +if __name__ == '__main__': + main() diff --git a/chatbridge/impl/satori/requirements.txt b/chatbridge/impl/satori/requirements.txt new file mode 100644 index 0000000..bed9f5e --- /dev/null +++ b/chatbridge/impl/satori/requirements.txt @@ -0,0 +1 @@ +satori-python diff --git a/chatbridge/impl/utils.py b/chatbridge/impl/utils.py index e8881f4..94fd172 100644 --- a/chatbridge/impl/utils.py +++ b/chatbridge/impl/utils.py @@ -5,7 +5,7 @@ import sys import time from threading import Thread -from typing import Type, TypeVar, Callable +from typing import Type, TypeVar, Callable, Optional, Any from chatbridge.core.client import ChatBridgeClient from chatbridge.core.config import BasicConfig @@ -53,9 +53,11 @@ def wait_until_terminate(): print('Interrupted with {} ({})'.format(signal.Signals(sig).name, sig)) -def register_exit_on_termination(): +def register_exit_on_termination(exit_callback: Optional[Callable[[], Any]] = None): def callback(sig, _): print('Interrupted with {} ({}), exiting'.format(signal.Signals(sig).name, sig)) + if exit_callback: + exit_callback() sys.exit(0) signal.signal(signal.SIGINT, callback) From ab36f311119886871062ace2da2d0eabba5617e5 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:25:54 +0800 Subject: [PATCH 26/46] satori correct nickname support, use regular queue for simpleness, update docker compose --- chatbridge/impl/satori/entry.py | 46 ++++++++++++++++++++------------- docker/docker-compose.yml | 8 ++++++ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/chatbridge/impl/satori/entry.py b/chatbridge/impl/satori/entry.py index 74f3485..7a812ee 100644 --- a/chatbridge/impl/satori/entry.py +++ b/chatbridge/impl/satori/entry.py @@ -1,5 +1,6 @@ import asyncio -from typing import Optional +import queue +from typing import Optional, List from satori import WebsocketsInfo, Event, EventType from satori.client import App, Account, ApiInfo @@ -30,7 +31,7 @@ def __init__(self): self.logger = ChatBridgeLogger('Satori', file_handler=cb_client.logger.file_handler) self.register_satori_hooks() - self.__message_queue: asyncio.Queue[Optional[str]] = asyncio.Queue() + self.__message_queue: queue.Queue[Optional[str]] = queue.Queue() self.__loop: Optional[asyncio.AbstractEventLoop] = None def register_satori_hooks(self): @@ -39,14 +40,15 @@ def register_satori_hooks(self): @self.app.register_on(EventType.MESSAGE_CREATED) async def listen(account: Account, event: Event): self.logger.debug('Satori MESSAGE_CREATED account={} event={}'.format(account, event)) - if event.channel is None or event.message is None or event.user is None: + if event.channel is None or event.message is None or event.user is None or event.message is None: return if event.channel.id != str(config.react_channel_id): return self.logger.info('Satori chat message event: {}'.format(event)) - msg = event.message.content - args = msg.split(' ') + msg_str = event.message.content + msg_comp = event.message.message + args = msg_str.split(' ') async def send_text(s: str): await account.send_message(event.channel.id, s) @@ -61,8 +63,20 @@ async def send_text(s: str): if len(args) >= 2 and args[0] == '!!mc': self.logger.info('!!mc command triggered') - sender = event.user.nick or event.user.name - cb_client.broadcast_chat(msg.split(' ', 1)[-1], sender) + sender = event.user.nick or event.user.name or event.member.nick or event.member.name + assert sender is not None + + from satori import Text + new_elements: List[str] = [] + for el in msg_comp: + if isinstance(el, Text): + new_elements.append(el.text) + else: + new_elements.append(f'<{el.tag}>') + processed_msg = ''.join(new_elements) + if processed_msg.startswith('!!mc '): + processed_msg = processed_msg.split(' ', 1)[-1] + cb_client.broadcast_chat(processed_msg, sender) if len(args) == 1 and args[0] == '!!online': self.logger.info('!!online command triggered') @@ -110,7 +124,11 @@ async def __send_text_long(self, text: str): async def __messanger_loop(self): while True: - msg = await self.__message_queue.get() + try: + msg = self.__message_queue.get(block=False) + except queue.Empty: + await asyncio.sleep(0.05) + continue if msg is None: break try: @@ -125,15 +143,10 @@ async def main(self): await t def submit_text(self, s: str): - if self.__loop is None: - self.logger.warning('submit_message is called when event loop does not exists yet') - return - - asyncio.run_coroutine_threadsafe(self.__message_queue.put(s), self.__loop) + self.__message_queue.put(s) def shutdown(self): - if self.__loop is not None: - asyncio.run_coroutine_threadsafe(self.__message_queue.put(None), self.__loop) + self.__message_queue.put(None) class SatoriChatBridgeClient(ChatBridgeClient): @@ -178,9 +191,6 @@ def on_custom(self, sender: str, payload: CustomPayload): satori_client.submit_text(text) -queue = asyncio.Queue() - - def main(): global config, cb_client, satori_client config = utils.load_config(ConfigFile, SatoriConfig) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 3336580..41d9f85 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -17,3 +17,11 @@ services: command: kaiheila_bot volumes: - ./ChatBridge_kaiheila.json:/app/ChatBridge_kaiheila.json + + satori_bot: + container_name: chatbridge_satori_bot + restart: unless-stopped + image: fallenbreath/chatbridge:latest + command: satori_bot + volumes: + - ./ChatBridge_satori.json:/app/ChatBridge_satori.json From c768c2917fb7786d30c8b6a3b6783f334088094a Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:26:08 +0800 Subject: [PATCH 27/46] khl bot update khl.py --- chatbridge/impl/kaiheila/entry.py | 143 +++++++++++----------- chatbridge/impl/kaiheila/requirements.txt | 2 +- 2 files changed, 71 insertions(+), 74 deletions(-) diff --git a/chatbridge/impl/kaiheila/entry.py b/chatbridge/impl/kaiheila/entry.py index 3142fd0..49ed4f7 100644 --- a/chatbridge/impl/kaiheila/entry.py +++ b/chatbridge/impl/kaiheila/entry.py @@ -1,12 +1,12 @@ import asyncio import collections -import json import logging import queue from typing import Optional, List -from khl import Bot, Cert, Msg +from khl import Bot, Message, MessageTypes, PublicMessage +from chatbridge.common.logger import ChatBridgeLogger from chatbridge.core.client import ChatBridgeClient from chatbridge.core.config import ClientConfig from chatbridge.core.network.protocol import ChatPayload, CommandPayload @@ -22,8 +22,6 @@ class KaiHeiLaConfig(ClientConfig): - client_id: str = '' - client_secret: str = '' token: str = '' channels_for_command: List[str] = [ '123321', @@ -50,24 +48,62 @@ class MessageDataType: chatClient: Optional['KhlChatBridgeClient'] = None -class KaiHeiLaBot(Bot): +class KaiHeiLaBot: def __init__(self, config: KaiHeiLaConfig): self.config = config - cert = Cert( - client_id=self.config.client_id, - client_secret=self.config.client_secret, - token=self.config.token - ) - super().__init__(cert=cert, cmd_prefix=[self.config.command_prefix]) + self.loop = asyncio.get_event_loop() + self.bot = Bot(token=self.config.token) + self.__register_bot_callbacks() + self.logger = ChatBridgeLogger('khl', file_handler=chatClient.logger.file_handler) self.messages = queue.Queue() - self.event_loop = asyncio.get_event_loop() - self._setup_event_loop(self.event_loop) def startRunning(self): self.logger.info('Starting the bot') - self.on_text_msg(self.on_message) - asyncio.ensure_future(self.on_ready(), loop=self.event_loop) - self.run() + self.bot.run() + + def __register_bot_callbacks(self): + self.bot.on_message()(self.__on_message) + self.bot.on_startup(self.__on_ready) + + def bot_command(): + return self.bot.command(prefixes=[self.config.command_prefix]) + + @bot_command() + async def help(msg: Message): + if msg.ctx.channel.id in self.config.channels_for_command: + if msg.ctx.channel.id == self.config.channel_for_chat: + text = CommandHelpMessageAll + else: + text = CommandHelpMessage + await msg.reply(text) + + @bot_command() + async def ping(msg: Message): + if msg.ctx.channel.id in self.config.channels_for_command: + await self.bot.client.send(msg.ctx.channel, 'pong!!') + + async def send_chatbridge_command(target_client: str, command: str, msg: Message): + if chatClient.is_online(): + self.logger.info('Sending command "{}" to client {}'.format(command, target_client)) + chatClient.send_command(target_client, command, params={'from_channel': msg.ctx.channel.id}) + else: + await msg.reply('ChatBridge client is offline') + + @bot_command() + async def online(msg: Message): + if msg.ctx.channel.id == config.channel_for_chat: # chat channel only + await send_chatbridge_command(config.client_to_query_online, '!!online', msg) + + @bot_command() + async def stats(msg: Message, *args): + args = list(args) + if len(args) >= 1 and args[0] == 'rank': + args.pop(0) + command = '!!stats rank ' + ' '.join(args) + if len(args) == 0 or len(args) - int(command.find('-bot') != -1) - int(command.find('-all') != -1) != 2: + await msg.reply(StatsCommandHelpMessage) + else: + await send_chatbridge_command(config.client_to_query_stats, command, msg) async def listeningMessage(self): self.logger.info('Message listening looping...') @@ -83,34 +119,38 @@ async def listeningMessage(self): assert isinstance(data, tuple) sender: str = data[0] payload: ChatPayload = data[1] - await self.send(self.config.channel_for_chat, self.formatMessageToKaiHeiLa('[{}] {}'.format(sender, payload.formatted_str()))) + ch = await self.bot.client.fetch_public_channel(self.config.channel_for_chat) + await self.bot.client.send(ch, self.formatMessageToKaiHeiLa('[{}] {}'.format(sender, payload.formatted_str()))) elif message_data.type == MessageDataType.CARD: # embed assert isinstance(data, list) - await self.send(message_data.channel, json.dumps([ + ch = await self.bot.client.fetch_public_channel(message_data.channel) + await self.bot.client.send(ch, [ { "type": "card", "theme": "secondary", "size": "lg", "modules": data } - ]), type=Msg.Types.CARD) + ], type=MessageTypes.CARD) elif message_data.type == MessageDataType.TEXT: - await self.send(message_data.channel, self.formatMessageToKaiHeiLa(str(data))) + await self.bot.client.send(message_data.channel, self.formatMessageToKaiHeiLa(str(data))) else: self.logger.debug('Unknown messageData type {}'.format(message_data.data)) - except: + except Exception: self.logger.exception('Error looping khl bot') - async def on_ready(self): - id_ = await self.id() - self.logger.info(f'Logged in with id {id_}') - await self.listeningMessage() + async def __on_ready(self, b: Bot): + me = await b.client.fetch_me() + self.logger.info(f'Logged in as {me.username} with id {me.id}') + _ = asyncio.create_task(self.listeningMessage()) - async def on_message(self, message: Msg): - if message.author_id == await self.id(): + async def __on_message(self, message: Message): + if message.author_id == (await self.bot.client.fetch_me()).id: + return + if not isinstance(message, PublicMessage): return - channel_id = message.ctx.channel.id - author = message.ctx.author.username + channel_id = message.channel.id + author = message.author.username self.logger.debug('channel id = {}'.format(channel_id)) if channel_id in self.config.channels_for_command or channel_id == self.config.channel_for_chat: self.logger.info(f"{channel_id}: {author}: {message.content}") @@ -156,49 +196,6 @@ def formatMessageToKaiHeiLa(self, message: str) -> str: return message -def createKaiHeiLaBot() -> KaiHeiLaBot: - bot = KaiHeiLaBot(config) - - @bot.command() - async def help(msg: Msg): - if msg.ctx.channel.id in bot.config.channels_for_command: - if msg.ctx.channel.id == bot.config.channel_for_chat: - text = CommandHelpMessageAll - else: - text = CommandHelpMessage - await msg.reply(text) - - @bot.command() - async def ping(msg: Msg): - if msg.ctx.channel.id in bot.config.channels_for_command: - await bot.send(msg.ctx.channel.id, 'pong!!') - - async def send_chatbridge_command(target_client: str, command: str, msg: Msg): - if chatClient.is_online(): - bot.logger.info('Sending command "{}" to client {}'.format(command, target_client)) - chatClient.send_command(target_client, command, params={'from_channel': msg.ctx.channel.id}) - else: - await msg.reply('ChatBridge client is offline') - - @bot.command() - async def online(msg: Msg): - if msg.ctx.channel.id == config.channel_for_chat: # chat channel only - await send_chatbridge_command(config.client_to_query_online, '!!online', msg) - - @bot.command() - async def stats(msg: Msg, *args): - args = list(args) - if len(args) >= 1 and args[0] == 'rank': - args.pop(0) - command = '!!stats rank ' + ' '.join(args) - if len(args) == 0 or len(args) - int(command.find('-bot') != -1) - int(command.find('-all') != -1) != 2: - await msg.reply(StatsCommandHelpMessage) - else: - await send_chatbridge_command(config.client_to_query_stats, command, msg) - - return bot - - class KhlChatBridgeClient(ChatBridgeClient): def on_chat(self, sender: str, payload: ChatPayload): khlBot.add_message((sender, payload), None, MessageDataType.CHAT) @@ -233,7 +230,7 @@ def main(): utils.start_guardian(chatClient) utils.register_exit_on_termination() print('Starting KHL Bot') - khlBot = createKaiHeiLaBot() + khlBot = KaiHeiLaBot(config) khlBot.startRunning() print('Bye~') diff --git a/chatbridge/impl/kaiheila/requirements.txt b/chatbridge/impl/kaiheila/requirements.txt index 73ac0c3..7edbe2e 100644 --- a/chatbridge/impl/kaiheila/requirements.txt +++ b/chatbridge/impl/kaiheila/requirements.txt @@ -1 +1 @@ -khl.py==0.0.10 +khl.py~=0.3.17 From 0121204766dbc2d2d125eba2a8db81a949d6d1f4 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:35:17 +0800 Subject: [PATCH 28/46] action step update and image build linux/amd64 only for dev builds --- .github/workflows/image.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index b05e4c6..eb1c627 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -15,22 +15,22 @@ jobs: image: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Login to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: fallenbreath password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Docker meta id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | fallenbreath/chatbridge @@ -39,9 +39,9 @@ jobs: type=semver,pattern={{version}} - name: Build and push - uses: docker/build-push-action@v4 + uses: docker/build-push-action@v5 with: - platforms: linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 + platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64' || 'linux/amd64' }} file: ./docker/Dockerfile context: . push: ${{ github.event_name != 'pull_request' }} From f5cb1f416a718a756b9276077b4cb4faa7572d30 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:44:27 +0800 Subject: [PATCH 29/46] cqhttp client don't install websocket --- chatbridge/impl/cqhttp/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/chatbridge/impl/cqhttp/requirements.txt b/chatbridge/impl/cqhttp/requirements.txt index 6a28e81..3190b31 100644 --- a/chatbridge/impl/cqhttp/requirements.txt +++ b/chatbridge/impl/cqhttp/requirements.txt @@ -1,2 +1 @@ -websocket>=0.2.1 websocket-client>=1.2.1 From 9e0191f8bfa1607150a44985f1747f3fa571b28b Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:50:03 +0800 Subject: [PATCH 30/46] readme update and better satori-python requirement version range --- README.md | 37 +++++++++++++++++++++---- chatbridge/impl/satori/requirements.txt | 2 +- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f276d3e..42cad42 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ ChatBridge is mainly for custom use of TIS server, especially the bot/command co - Discord client - Kaiheila client - Online command client +- ... Therefore, for these bot and related clients: @@ -50,6 +51,7 @@ ChatBridge 是一个为 TIS 服务器定制使用的工具,尤其是 bot/指 - Discord 客户端 - Kaiheila 客户端 - Online 指令客户端 +- ... 因此,对于这些 bot 及相关的客户端: @@ -190,7 +192,6 @@ python ChatBridge.pyz cqhttp_bot Extra requirements (also listed in `/chatbridge/impl/cqhttp/requirements.txt`): ``` -websocket>=0.2.1 websocket-client>=1.2.1 ``` @@ -217,21 +218,47 @@ Extra configure fields (compared to [CLI client](#cli-client)) "server_display_name": "TIS" // The name of the server, used for display in some places ``` +## Client as a Satori client + +``` +python ChatBridge.pyz cqhttp_bot +``` + +Extra requirements (also listed in `/chatbridge/impl/satori/requirements.txt`): + +``` +satori-python>=0.11 +``` + +Just like the CoolqHttp client, but it uses satori protocol + +Extra configure fields (compared to [CLI client](#cli-client)) + +```json5 + "ws_address": "127.0.0.1", // satori server address + "ws_port": 5500, // satori server port + "ws_path": "", // satori server optional path prefix + "satori_token": "xxxxx", // satori access token + "react_channel_id": 12345, // the target channel id (for QQ, it's the group id) + "chatbridge_message_prefix": "!!qq", + "client_to_query_stats": "MyClient1", // it should be a client as an MCDR plugin, with stats_helper plugin installed in the MCDR + "client_to_query_online": "MyClient2", // a client described in the following section "Client to respond online command" + "server_display_name": "TIS" // The name of the server, used for display in some places +``` + ## Kaiheila bot client -`python ChatBridge.pyz kaiheila_bot` +`python ChatBridge.pyz satori_bot` Extra requirements (also listed in `/chatbridge/impl/kaiheila/requirements.txt`): ``` -khl.py==0.0.10 +khl.py~=0.3.17 ``` Extra configure fields (compared to [CLI client](#cli-client)) ```json5 - "client_id": "", // kaiheila client id - "client_secret": "", // kaiheila client secret "token": "", // kaiheila token "channels_for_command": [ // a list of channels, public commands can be used here. use string "123400000000000000", diff --git a/chatbridge/impl/satori/requirements.txt b/chatbridge/impl/satori/requirements.txt index bed9f5e..8e81a79 100644 --- a/chatbridge/impl/satori/requirements.txt +++ b/chatbridge/impl/satori/requirements.txt @@ -1 +1 @@ -satori-python +satori-python>=0.11 From 4b673b55bbc6bd37034e6e639be9b01985896d33 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:52:32 +0800 Subject: [PATCH 31/46] extract cqhttp bot copy-writings into a separated file --- chatbridge/impl/cqhttp/copywritings.py | 15 +++++++++++++++ chatbridge/impl/cqhttp/entry.py | 16 +--------------- chatbridge/impl/satori/entry.py | 2 +- 3 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 chatbridge/impl/cqhttp/copywritings.py diff --git a/chatbridge/impl/cqhttp/copywritings.py b/chatbridge/impl/cqhttp/copywritings.py new file mode 100644 index 0000000..ffaf847 --- /dev/null +++ b/chatbridge/impl/cqhttp/copywritings.py @@ -0,0 +1,15 @@ +CQHelpMessage = ''' +!!help: 显示本条帮助信息 +!!ping: pong!! +!!mc <消息>: 向 MC 中发送聊天信息 <消息> +!!online: 显示正版通道在线列表 +!!stats <类别> <内容> [<-bot>]: 查询统计信息 <类别>.<内容> 的排名 +'''.strip() + +StatsHelpMessage = ''' +!!stats <类别> <内容> [<-bot>] +添加 `-bot` 来列出 bot +例子: +!!stats used diamond_pickaxe +!!stats custom time_since_rest -bot +'''.strip() diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index a43a7dd..9151b94 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -9,27 +9,13 @@ from chatbridge.core.network.protocol import ChatPayload, CommandPayload, CustomPayload from chatbridge.impl import utils from chatbridge.impl.cqhttp.config import CqHttpConfig +from chatbridge.impl.cqhttp.copywritings import CQHelpMessage, StatsHelpMessage from chatbridge.impl.tis.protocol import StatsQueryResult, OnlineQueryResult ConfigFile = 'ChatBridge_CQHttp.json' cq_bot: Optional['CQBot'] = None chatClient: Optional['CqHttpChatBridgeClient'] = None -CQHelpMessage = ''' -!!help: 显示本条帮助信息 -!!ping: pong!! -!!mc <消息>: 向 MC 中发送聊天信息 <消息> -!!online: 显示正版通道在线列表 -!!stats <类别> <内容> [<-bot>]: 查询统计信息 <类别>.<内容> 的排名 -'''.strip() -StatsHelpMessage = ''' -!!stats <类别> <内容> [<-bot>] -添加 `-bot` 来列出 bot -例子: -!!stats used diamond_pickaxe -!!stats custom time_since_rest -bot -'''.strip() - class CQBot(websocket.WebSocketApp): def __init__(self, config: CqHttpConfig): diff --git a/chatbridge/impl/satori/entry.py b/chatbridge/impl/satori/entry.py index 7a812ee..e1ef17d 100644 --- a/chatbridge/impl/satori/entry.py +++ b/chatbridge/impl/satori/entry.py @@ -9,7 +9,7 @@ from chatbridge.core.client import ChatBridgeClient from chatbridge.core.network.protocol import ChatPayload, CommandPayload, CustomPayload from chatbridge.impl import utils -from chatbridge.impl.cqhttp.entry import CQHelpMessage, StatsHelpMessage +from chatbridge.impl.cqhttp.copywritings import CQHelpMessage, StatsHelpMessage from chatbridge.impl.satori.config import SatoriConfig from chatbridge.impl.tis.protocol import StatsQueryResult, OnlineQueryResult From 56b9e1abdb12be000cbf1dddf4148188b027ff3b Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:56:06 +0800 Subject: [PATCH 32/46] satori stop fast --- chatbridge/impl/satori/entry.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chatbridge/impl/satori/entry.py b/chatbridge/impl/satori/entry.py index e1ef17d..3c70cac 100644 --- a/chatbridge/impl/satori/entry.py +++ b/chatbridge/impl/satori/entry.py @@ -138,9 +138,8 @@ async def __messanger_loop(self): async def main(self): self.__loop = asyncio.get_event_loop() - t = asyncio.create_task(self.__messanger_loop()) - await self.app.run_async() - await t + _ = asyncio.create_task(self.__messanger_loop()) + await self.app.run_async(stop_signal=[]) def submit_text(self, s: str): self.__message_queue.put(s) From 7e22d018d9c078fbad2d592782817ec9c2650cbc Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:58:07 +0800 Subject: [PATCH 33/46] make the docker build simple cuz everything has pre-built wheel --- docker/Dockerfile | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index f3addd3..a85de00 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -15,11 +15,8 @@ FROM python:${PYTHON_VERSION}-slim COPY --from=builder /build/requirements.all.txt /app/ RUN apt-get update \ - && apt-get install -y --no-install-recommends gcc build-essential libffi-dev \ - && rm -rf /var/lib/apt/lists/* \ && pip3 install -r /app/requirements.all.txt \ - && pip3 cache purge \ - && apt-get purge -y --auto-remove gcc build-essential libffi-dev + && pip3 cache purge COPY --from=builder /build/ChatBridge.pyz /app/ WORKDIR /app From 4e0f4bd7be4dcd2cc56e892fe1977c4e112f219d Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 14:59:13 +0800 Subject: [PATCH 34/46] detect this --- .github/workflows/image.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index eb1c627..1671e43 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -4,6 +4,7 @@ on: push: paths: - ".github/workflows/image.yml" + - "docker/Dockerfile" - "chatbridge/**" - "lang/**" - "__main__.py" From 0f07aeb016a7f52307059de0db2f4d0270a6e451 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 15:00:34 +0800 Subject: [PATCH 35/46] simple --- docker/Dockerfile | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docker/Dockerfile b/docker/Dockerfile index a85de00..a1c0f46 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -14,9 +14,7 @@ RUN cd /build \ FROM python:${PYTHON_VERSION}-slim COPY --from=builder /build/requirements.all.txt /app/ -RUN apt-get update \ - && pip3 install -r /app/requirements.all.txt \ - && pip3 cache purge +RUN pip3 install -r /app/requirements.all.txt && pip3 cache purge COPY --from=builder /build/ChatBridge.pyz /app/ WORKDIR /app From b0068ed3510fd67bc62b3700d7a51227e81c5680 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sat, 25 May 2024 15:17:16 +0800 Subject: [PATCH 36/46] more logs and escape special chars for satori --- chatbridge/impl/kaiheila/entry.py | 5 ++++- chatbridge/impl/satori/entry.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/chatbridge/impl/kaiheila/entry.py b/chatbridge/impl/kaiheila/entry.py index 49ed4f7..b3ffc85 100644 --- a/chatbridge/impl/kaiheila/entry.py +++ b/chatbridge/impl/kaiheila/entry.py @@ -80,7 +80,8 @@ async def help(msg: Message): @bot_command() async def ping(msg: Message): if msg.ctx.channel.id in self.config.channels_for_command: - await self.bot.client.send(msg.ctx.channel, 'pong!!') + self.logger.info('!!ping command triggered') + await msg.reply('pong!!') async def send_chatbridge_command(target_client: str, command: str, msg: Message): if chatClient.is_online(): @@ -92,10 +93,12 @@ async def send_chatbridge_command(target_client: str, command: str, msg: Message @bot_command() async def online(msg: Message): if msg.ctx.channel.id == config.channel_for_chat: # chat channel only + self.logger.info('!!online command triggered') await send_chatbridge_command(config.client_to_query_online, '!!online', msg) @bot_command() async def stats(msg: Message, *args): + self.logger.info('!!stats command triggered, args: {}'.format(args)) args = list(args) if len(args) >= 1 and args[0] == 'rank': args.pop(0) diff --git a/chatbridge/impl/satori/entry.py b/chatbridge/impl/satori/entry.py index 3c70cac..ee0393c 100644 --- a/chatbridge/impl/satori/entry.py +++ b/chatbridge/impl/satori/entry.py @@ -108,6 +108,11 @@ async def __send_text_one(self, text: str): path=config.ws_path, token=config.satori_token, )) + # https://satori.js.org/zh-CN/protocol/message.html + text = text.replace('&', '&') + text = text.replace('"', '"') + text = text.replace('<', '<') + text = text.replace('>', '>') await account.send_message(str(config.react_channel_id), text) async def __send_text_long(self, text: str): @@ -158,8 +163,9 @@ def on_chat(self, sender: str, payload: ChatPayload): return payload.message = parts[1] - self.logger.info('Triggered command, sending message {} to satori'.format(payload.formatted_str())) - satori_client.submit_text('[{}] {}'.format(sender, payload.formatted_str())) + msg_to_send = '[{}] {}'.format(sender, payload.formatted_str()) + self.logger.info('Triggered command, sending message {!r} to satori'.format(msg_to_send)) + satori_client.submit_text(msg_to_send) def on_command(self, sender: str, payload: CommandPayload): if satori_client is None: From 02def9b7a5d1735dc632adc703f67df8af72c266 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 16 Jun 2024 03:41:17 +0800 Subject: [PATCH 37/46] ci: add package.yml --- .github/workflows/package.yml | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/package.yml diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml new file mode 100644 index 0000000..b8328ea --- /dev/null +++ b/.github/workflows/package.yml @@ -0,0 +1,43 @@ +name: CI for MCDR Plugin + +on: + push: + pull_request: + +jobs: + package: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + - name: Setup python + uses: actions/setup-python@v5 + with: + python-version: 3.9 + + - uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-pip- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Pack Plugin + run: | + python -m mcdreforged pack -o ./package + + - uses: actions/upload-artifact@v4 + with: + name: Chatbridge distribution for ${{ github.sha }} + path: package/ + + - name: Publish distribution to release + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') + uses: softprops/action-gh-release@v2 + with: + files: package/*.pyz From d232a8765e0b2b8b597dac0789dddc28791fe663 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 16 Jun 2024 03:41:41 +0800 Subject: [PATCH 38/46] v2.6.0 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 133ad87..64178dc 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.5.3", + "version": "2.6.0", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From 615e24f34a36553526682296e7c6b569ba5a3304 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 16 Jun 2024 03:46:40 +0800 Subject: [PATCH 39/46] ci: only use linux/amd64 for docker image build --- .github/workflows/image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/image.yml b/.github/workflows/image.yml index 1671e43..afd6dd7 100644 --- a/.github/workflows/image.yml +++ b/.github/workflows/image.yml @@ -42,7 +42,7 @@ jobs: - name: Build and push uses: docker/build-push-action@v5 with: - platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/386,linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64' || 'linux/amd64' }} + platforms: 'linux/amd64' file: ./docker/Dockerfile context: . push: ${{ github.event_name != 'pull_request' }} From 57708a7f133ac182733761d89d93ecf3c5ba176d Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 16 Jun 2024 03:47:12 +0800 Subject: [PATCH 40/46] -it in docker-compose.yml for server --- docker/docker-compose.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 41d9f85..c1cccf5 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -5,6 +5,8 @@ services: restart: unless-stopped image: fallenbreath/chatbridge:latest command: server + stdin_open: true + tty: true volumes: - ./ChatBridge_server.json:/app/ChatBridge_server.json ports: From cf3b9db73b8d720e64023ca9b8ff73afcae57b7e Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 16 Jun 2024 03:48:52 +0800 Subject: [PATCH 41/46] cli client, server, online cmd, ignore empty input --- chatbridge/impl/cli/cli_client.py | 3 +++ chatbridge/impl/cli/cli_server.py | 3 +++ chatbridge/impl/online/entry.py | 3 +++ 3 files changed, 9 insertions(+) diff --git a/chatbridge/impl/cli/cli_client.py b/chatbridge/impl/cli/cli_client.py index 618adea..a336e3e 100644 --- a/chatbridge/impl/cli/cli_client.py +++ b/chatbridge/impl/cli/cli_client.py @@ -17,6 +17,9 @@ def on_chat(self, sender: str, payload: ChatPayload): def console_loop(self): while True: text = input() + if len(text) == 0: + continue + self.logger.info('Processing user input "{}"'.format(text)) if text == 'start': self.start() diff --git a/chatbridge/impl/cli/cli_server.py b/chatbridge/impl/cli/cli_server.py index bbc263c..dfdb87c 100644 --- a/chatbridge/impl/cli/cli_server.py +++ b/chatbridge/impl/cli/cli_server.py @@ -47,6 +47,9 @@ def on_chat(self, sender: str, content: ChatPayload): def console_loop(self): while self.is_running(): text = input() + if len(text) == 0: + continue + self.logger.info('Processing user input "{}"'.format(text)) if text == 'stop': self.stop() diff --git a/chatbridge/impl/online/entry.py b/chatbridge/impl/online/entry.py index e1b8560..b8e9790 100644 --- a/chatbridge/impl/online/entry.py +++ b/chatbridge/impl/online/entry.py @@ -116,6 +116,9 @@ def console_input_loop(): while True: try: text = input() + if len(text) == 0: + continue + if text in ['!!online', 'online']: print('\n'.join(chatClient.query())) elif text == 'stop': From e1d59ccd42e9d4d3f2c1e221085187dd696ec4cc Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Sun, 16 Jun 2024 03:49:44 +0800 Subject: [PATCH 42/46] v2.6.1 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 64178dc..1cd8e2e 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.6.0", + "version": "2.6.1", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From 51cb4f27e332ed256ae22fac7cfcd88f66d3180a Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Fri, 19 Jul 2024 22:57:47 +0800 Subject: [PATCH 43/46] ping `satori-python` version --- chatbridge/impl/satori/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chatbridge/impl/satori/requirements.txt b/chatbridge/impl/satori/requirements.txt index 8e81a79..bf6bd8e 100644 --- a/chatbridge/impl/satori/requirements.txt +++ b/chatbridge/impl/satori/requirements.txt @@ -1 +1 @@ -satori-python>=0.11 +satori-python==0.11.5 From a689ea05804e3c03d7095c62abb0a5a90c6fcd4d Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Fri, 19 Jul 2024 23:01:03 +0800 Subject: [PATCH 44/46] cqhttp,satori: support disable !!stats or !!online command --- chatbridge/impl/cqhttp/entry.py | 10 ++++++++++ chatbridge/impl/satori/entry.py | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/chatbridge/impl/cqhttp/entry.py b/chatbridge/impl/cqhttp/entry.py index 9151b94..303bc91 100644 --- a/chatbridge/impl/cqhttp/entry.py +++ b/chatbridge/impl/cqhttp/entry.py @@ -60,6 +60,11 @@ def on_message(self, _, message: str): if len(args) == 1 and args[0] == '!!online': self.logger.info('!!online command triggered') + if not self.config.client_to_query_online: + self.logger.info('!!online command is not enabled') + self.send_text('!!online 指令未启用') + return + if chatClient.is_online(): command = args[0] client = self.config.client_to_query_online @@ -70,6 +75,11 @@ def on_message(self, _, message: str): if len(args) >= 1 and args[0] == '!!stats': self.logger.info('!!stats command triggered') + if not self.config.client_to_query_stats: + self.logger.info('!!stats command is not enabled') + self.send_text('!!stats 指令未启用') + return + command = '!!stats rank ' + ' '.join(args[1:]) if len(args) == 0 or len(args) - int(command.find('-bot') != -1) != 3: self.send_text(StatsHelpMessage) diff --git a/chatbridge/impl/satori/entry.py b/chatbridge/impl/satori/entry.py index ee0393c..6b5695f 100644 --- a/chatbridge/impl/satori/entry.py +++ b/chatbridge/impl/satori/entry.py @@ -80,6 +80,11 @@ async def send_text(s: str): if len(args) == 1 and args[0] == '!!online': self.logger.info('!!online command triggered') + if not config.client_to_query_online: + self.logger.info('!!online command is not enabled') + await send_text('!!online 指令未启用') + return + if cb_client.is_online(): command = args[0] client = config.client_to_query_online @@ -90,6 +95,11 @@ async def send_text(s: str): if len(args) >= 1 and args[0] == '!!stats': self.logger.info('!!stats command triggered') + if not config.client_to_query_stats: + self.logger.info('!!stats command is not enabled') + await send_text('!!stats 指令未启用') + return + command = '!!stats rank ' + ' '.join(args[1:]) if len(args) == 0 or len(args) - int(command.find('-bot') != -1) != 3: await send_text(StatsHelpMessage) From 0acafebd490ed4b08c7b0f9a9418640ee260d991 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Fri, 19 Jul 2024 23:01:19 +0800 Subject: [PATCH 45/46] v2.6.2 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index 1cd8e2e..a384790 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.6.1", + "version": "2.6.2", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more", From 98db5189cf69f5d358ee26cf1390750602453a68 Mon Sep 17 00:00:00 2001 From: Fallen_Breath Date: Fri, 19 Jul 2024 23:10:16 +0800 Subject: [PATCH 46/46] v2.6.3 --- mcdreforged.plugin.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mcdreforged.plugin.json b/mcdreforged.plugin.json index a384790..89f4cf1 100644 --- a/mcdreforged.plugin.json +++ b/mcdreforged.plugin.json @@ -1,6 +1,6 @@ { "id": "chatbridge", - "version": "2.6.2", + "version": "2.6.3", "name": "ChatBridge v2 for MCDR", "description": { "en_us": "Broadcast chats between Minecraft servers and more",