├── FloraBot.py ├── LICENSE ├── PluginTemplate ├── Plugin.json └── PluginTemplate.py ├── README.md └── requirements.txt /FloraBot.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | from websocket_server import WebsocketServer, WebSocketHandler 3 | import logging 4 | from socketserver import TCPServer 5 | import requests 6 | import importlib.util 7 | import os 8 | import json 9 | import threading 10 | import sys 11 | import subprocess 12 | import platform 13 | import time 14 | import random 15 | import re 16 | import zipfile 17 | import shutil 18 | from colorama import init as colorama_init 19 | 20 | 21 | def supports_color(): # 检测当前终端是否支持颜色输出 22 | if "PYCHARM_HOSTED" in os.environ: 23 | return True 24 | if sys.platform == "win32": 25 | return "ANSICON" in os.environ or "WT_SESSION" in os.environ or "TERM_PROGRAM" in os.environ 26 | return hasattr(sys.stdout, "isatty") and sys.stdout.isatty() 27 | 28 | 29 | colorama_init() 30 | flora_logo = "\x1b[38;2;37;197;127m█\x1b[38;2;38;197;127m█\x1b[38;2;39;197;128m█\x1b[38;2;40;197;129m█\x1b[38;2;41;197;130m█\x1b[38;2;42;197;131m█\x1b[38;2;43;197;132m█\x1b[38;2;45;197;133m╗\x1b[38;2;46;197;134m \x1b[38;2;47;197;135m█\x1b[38;2;48;197;136m█\x1b[38;2;49;197;137m╗\x1b[38;2;50;197;138m \x1b[38;2;51;197;139m \x1b[38;2;53;197;140m \x1b[38;2;54;197;141m \x1b[38;2;55;197;142m \x1b[38;2;56;197;143m \x1b[38;2;57;197;144m \x1b[38;2;58;197;145m \x1b[38;2;59;197;146m \x1b[38;2;61;197;147m \x1b[38;2;62;197;148m \x1b[38;2;63;197;149m \x1b[38;2;64;197;150m \x1b[38;2;65;197;151m \x1b[38;2;66;197;152m \x1b[38;2;67;197;153m \x1b[38;2;69;197;154m \x1b[38;2;70;197;155m \x1b[38;2;71;197;156m \x1b[38;2;72;197;157m \x1b[38;2;73;197;158m \x1b[38;2;74;197;159m \x1b[38;2;76;197;160m \x1b[38;2;77;197;161m \x1b[38;2;78;197;162m \x1b[38;2;79;197;163m \x1b[38;2;80;197;164m█\x1b[38;2;81;197;165m█\x1b[38;2;82;197;166m█\x1b[38;2;84;197;167m█\x1b[38;2;85;197;168m█\x1b[38;2;86;197;169m█\x1b[38;2;87;197;170m╗\x1b[38;2;88;197;171m \x1b[38;2;89;197;172m \x1b[38;2;90;197;173m \x1b[38;2;92;197;174m \x1b[38;2;93;197;175m \x1b[38;2;94;197;176m \x1b[38;2;95;197;177m \x1b[38;2;96;197;178m \x1b[38;2;97;197;179m \x1b[38;2;98;197;180m \x1b[38;2;100;197;181m \x1b[38;2;101;197;182m \x1b[38;2;102;197;183m \x1b[38;2;103;197;184m \x1b[38;2;104;197;185m \x1b[38;2;105;197;186m \x1b[38;2;107;197;187m \x1b[0m\n\x1b[38;2;37;197;127m█\x1b[38;2;38;197;127m█\x1b[38;2;39;197;128m╔\x1b[38;2;40;197;129m═\x1b[38;2;41;197;130m═\x1b[38;2;42;197;131m═\x1b[38;2;43;197;132m═\x1b[38;2;45;197;133m╝\x1b[38;2;46;197;134m \x1b[38;2;47;197;135m█\x1b[38;2;48;197;136m█\x1b[38;2;49;197;137m║\x1b[38;2;50;197;138m \x1b[38;2;51;197;139m \x1b[38;2;53;197;140m \x1b[38;2;54;197;141m \x1b[38;2;55;197;142m \x1b[38;2;56;197;143m \x1b[38;2;57;197;144m \x1b[38;2;58;197;145m \x1b[38;2;59;197;146m \x1b[38;2;61;197;147m \x1b[38;2;62;197;148m \x1b[38;2;63;197;149m \x1b[38;2;64;197;150m \x1b[38;2;65;197;151m \x1b[38;2;66;197;152m \x1b[38;2;67;197;153m \x1b[38;2;69;197;154m \x1b[38;2;70;197;155m \x1b[38;2;71;197;156m \x1b[38;2;72;197;157m \x1b[38;2;73;197;158m \x1b[38;2;74;197;159m \x1b[38;2;76;197;160m \x1b[38;2;77;197;161m \x1b[38;2;78;197;162m \x1b[38;2;79;197;163m \x1b[38;2;80;197;164m█\x1b[38;2;81;197;165m█\x1b[38;2;82;197;166m╔\x1b[38;2;84;197;167m═\x1b[38;2;85;197;168m═\x1b[38;2;86;197;169m█\x1b[38;2;87;197;170m█\x1b[38;2;88;197;171m╗\x1b[38;2;89;197;172m \x1b[38;2;90;197;173m \x1b[38;2;92;197;174m \x1b[38;2;93;197;175m \x1b[38;2;94;197;176m \x1b[38;2;95;197;177m \x1b[38;2;96;197;178m \x1b[38;2;97;197;179m \x1b[38;2;98;197;180m \x1b[38;2;100;197;181m \x1b[38;2;101;197;182m \x1b[38;2;102;197;183m█\x1b[38;2;103;197;184m█\x1b[38;2;104;197;185m╗\x1b[38;2;105;197;186m \x1b[38;2;107;197;187m \x1b[0m\n\x1b[38;2;37;197;127m█\x1b[38;2;38;197;127m█\x1b[38;2;39;197;128m█\x1b[38;2;40;197;129m█\x1b[38;2;41;197;130m█\x1b[38;2;42;197;131m╗\x1b[38;2;43;197;132m \x1b[38;2;45;197;133m \x1b[38;2;46;197;134m \x1b[38;2;47;197;135m█\x1b[38;2;48;197;136m█\x1b[38;2;49;197;137m║\x1b[38;2;50;197;138m \x1b[38;2;51;197;139m \x1b[38;2;53;197;140m \x1b[38;2;54;197;141m█\x1b[38;2;55;197;142m█\x1b[38;2;56;197;143m█\x1b[38;2;57;197;144m█\x1b[38;2;58;197;145m╗\x1b[38;2;59;197;146m \x1b[38;2;61;197;147m \x1b[38;2;62;197;148m \x1b[38;2;63;197;149m█\x1b[38;2;64;197;150m█\x1b[38;2;65;197;151m█\x1b[38;2;66;197;152m█\x1b[38;2;67;197;153m╗\x1b[38;2;69;197;154m \x1b[38;2;70;197;155m \x1b[38;2;71;197;156m█\x1b[38;2;72;197;157m█\x1b[38;2;73;197;158m█\x1b[38;2;74;197;159m█\x1b[38;2;76;197;160m╗\x1b[38;2;77;197;161m \x1b[38;2;78;197;162m \x1b[38;2;79;197;163m \x1b[38;2;80;197;164m█\x1b[38;2;81;197;165m█\x1b[38;2;82;197;166m█\x1b[38;2;84;197;167m█\x1b[38;2;85;197;168m█\x1b[38;2;86;197;169m█\x1b[38;2;87;197;170m╔\x1b[38;2;88;197;171m╝\x1b[38;2;89;197;172m \x1b[38;2;90;197;173m \x1b[38;2;92;197;174m█\x1b[38;2;93;197;175m█\x1b[38;2;94;197;176m█\x1b[38;2;95;197;177m█\x1b[38;2;96;197;178m╗\x1b[38;2;97;197;179m \x1b[38;2;98;197;180m \x1b[38;2;100;197;181m█\x1b[38;2;101;197;182m█\x1b[38;2;102;197;183m█\x1b[38;2;103;197;184m█\x1b[38;2;104;197;185m█\x1b[38;2;105;197;186m█\x1b[38;2;107;197;187m╗\x1b[0m\n\x1b[38;2;37;197;127m█\x1b[38;2;38;197;127m█\x1b[38;2;39;197;128m╔\x1b[38;2;40;197;129m═\x1b[38;2;41;197;130m═\x1b[38;2;42;197;131m╝\x1b[38;2;43;197;132m \x1b[38;2;45;197;133m \x1b[38;2;46;197;134m \x1b[38;2;47;197;135m█\x1b[38;2;48;197;136m█\x1b[38;2;49;197;137m║\x1b[38;2;50;197;138m \x1b[38;2;51;197;139m \x1b[38;2;53;197;140m█\x1b[38;2;54;197;141m█\x1b[38;2;55;197;142m╔\x1b[38;2;56;197;143m═\x1b[38;2;57;197;144m█\x1b[38;2;58;197;145m█\x1b[38;2;59;197;146m╗\x1b[38;2;61;197;147m \x1b[38;2;62;197;148m█\x1b[38;2;63;197;149m█\x1b[38;2;64;197;150m╔\x1b[38;2;65;197;151m═\x1b[38;2;66;197;152m═\x1b[38;2;67;197;153m╝\x1b[38;2;69;197;154m \x1b[38;2;70;197;155m█\x1b[38;2;71;197;156m█\x1b[38;2;72;197;157m╔\x1b[38;2;73;197;158m═\x1b[38;2;74;197;159m█\x1b[38;2;76;197;160m█\x1b[38;2;77;197;161m╗\x1b[38;2;78;197;162m \x1b[38;2;79;197;163m \x1b[38;2;80;197;164m█\x1b[38;2;81;197;165m█\x1b[38;2;82;197;166m╔\x1b[38;2;84;197;167m═\x1b[38;2;85;197;168m═\x1b[38;2;86;197;169m█\x1b[38;2;87;197;170m█\x1b[38;2;88;197;171m╗\x1b[38;2;89;197;172m \x1b[38;2;90;197;173m█\x1b[38;2;92;197;174m█\x1b[38;2;93;197;175m╔\x1b[38;2;94;197;176m═\x1b[38;2;95;197;177m█\x1b[38;2;96;197;178m█\x1b[38;2;97;197;179m╗\x1b[38;2;98;197;180m \x1b[38;2;100;197;181m \x1b[38;2;101;197;182m╚\x1b[38;2;102;197;183m█\x1b[38;2;103;197;184m█\x1b[38;2;104;197;185m╔\x1b[38;2;105;197;186m═\x1b[38;2;107;197;187m╝\x1b[0m\n\x1b[38;2;37;197;127m█\x1b[38;2;38;197;127m█\x1b[38;2;39;197;128m║\x1b[38;2;40;197;129m \x1b[38;2;41;197;130m \x1b[38;2;42;197;131m \x1b[38;2;43;197;132m \x1b[38;2;45;197;133m \x1b[38;2;46;197;134m \x1b[38;2;47;197;135m█\x1b[38;2;48;197;136m█\x1b[38;2;49;197;137m█\x1b[38;2;50;197;138m╗\x1b[38;2;51;197;139m \x1b[38;2;53;197;140m╚\x1b[38;2;54;197;141m█\x1b[38;2;55;197;142m█\x1b[38;2;56;197;143m█\x1b[38;2;57;197;144m█\x1b[38;2;58;197;145m╔\x1b[38;2;59;197;146m╝\x1b[38;2;61;197;147m \x1b[38;2;62;197;148m█\x1b[38;2;63;197;149m█\x1b[38;2;64;197;150m║\x1b[38;2;65;197;151m \x1b[38;2;66;197;152m \x1b[38;2;67;197;153m \x1b[38;2;69;197;154m \x1b[38;2;70;197;155m╚\x1b[38;2;71;197;156m█\x1b[38;2;72;197;157m█\x1b[38;2;73;197;158m█\x1b[38;2;74;197;159m█\x1b[38;2;76;197;160m█\x1b[38;2;77;197;161m█\x1b[38;2;78;197;162m╗\x1b[38;2;79;197;163m \x1b[38;2;80;197;164m█\x1b[38;2;81;197;165m█\x1b[38;2;82;197;166m█\x1b[38;2;84;197;167m█\x1b[38;2;85;197;168m█\x1b[38;2;86;197;169m█\x1b[38;2;87;197;170m╔\x1b[38;2;88;197;171m╝\x1b[38;2;89;197;172m \x1b[38;2;90;197;173m╚\x1b[38;2;92;197;174m█\x1b[38;2;93;197;175m█\x1b[38;2;94;197;176m█\x1b[38;2;95;197;177m█\x1b[38;2;96;197;178m╔\x1b[38;2;97;197;179m╝\x1b[38;2;98;197;180m \x1b[38;2;100;197;181m \x1b[38;2;101;197;182m \x1b[38;2;102;197;183m█\x1b[38;2;103;197;184m█\x1b[38;2;104;197;185m█\x1b[38;2;105;197;186m╗\x1b[38;2;107;197;187m \x1b[0m\n\x1b[38;2;37;197;127m╚\x1b[38;2;38;197;127m═\x1b[38;2;39;197;128m╝\x1b[38;2;40;197;129m \x1b[38;2;41;197;130m \x1b[38;2;42;197;131m \x1b[38;2;43;197;132m \x1b[38;2;45;197;133m \x1b[38;2;46;197;134m \x1b[38;2;47;197;135m╚\x1b[38;2;48;197;136m═\x1b[38;2;49;197;137m═\x1b[38;2;50;197;138m╝\x1b[38;2;51;197;139m \x1b[38;2;53;197;140m \x1b[38;2;54;197;141m╚\x1b[38;2;55;197;142m═\x1b[38;2;56;197;143m═\x1b[38;2;57;197;144m═\x1b[38;2;58;197;145m╝\x1b[38;2;59;197;146m \x1b[38;2;61;197;147m \x1b[38;2;62;197;148m╚\x1b[38;2;63;197;149m═\x1b[38;2;64;197;150m╝\x1b[38;2;65;197;151m \x1b[38;2;66;197;152m \x1b[38;2;67;197;153m \x1b[38;2;69;197;154m \x1b[38;2;70;197;155m \x1b[38;2;71;197;156m╚\x1b[38;2;72;197;157m═\x1b[38;2;73;197;158m═\x1b[38;2;74;197;159m═\x1b[38;2;76;197;160m═\x1b[38;2;77;197;161m═\x1b[38;2;78;197;162m╝\x1b[38;2;79;197;163m \x1b[38;2;80;197;164m╚\x1b[38;2;81;197;165m═\x1b[38;2;82;197;166m═\x1b[38;2;84;197;167m═\x1b[38;2;85;197;168m═\x1b[38;2;86;197;169m═\x1b[38;2;87;197;170m╝\x1b[38;2;88;197;171m \x1b[38;2;89;197;172m \x1b[38;2;90;197;173m \x1b[38;2;92;197;174m╚\x1b[38;2;93;197;175m═\x1b[38;2;94;197;176m═\x1b[38;2;95;197;177m═\x1b[38;2;96;197;178m╝\x1b[38;2;97;197;179m \x1b[38;2;98;197;180m \x1b[38;2;100;197;181m \x1b[38;2;101;197;182m \x1b[38;2;102;197;183m╚\x1b[38;2;103;197;184m═\x1b[38;2;104;197;185m═\x1b[38;2;105;197;186m╝\x1b[38;2;107;197;187m \x1b[0m\n" 31 | use_ansi_color = supports_color() 32 | if not use_ansi_color: 33 | flora_logo = re.compile(r"\x1B[@-_][0-?]*[ -/]*[@-~]").sub("", flora_logo) 34 | ansi_color = False 35 | print("当前终端不支持使用 ANSI 转义符字体变色\n") 36 | 37 | flora_server = Flask("FloraBot", template_folder="FloraBot", static_folder="FloraBot") 38 | connection_type = "HTTP" 39 | flora_host = "127.0.0.1" 40 | flora_port = 3003 41 | framework_address = "127.0.0.1:3000" 42 | bot_id = 0 43 | administrator = [] 44 | auto_install = False 45 | 46 | flora_version = "V1.12" 47 | big_update = False 48 | update_content = """修复 BUG: 49 | 修复了 HTTP 协议下无法正常接收消息的问题 50 | 内置功能: 51 | 1. /帮助 - 若不知道 FloraBot 有哪些功能, 请试试使用该指令 52 | 2. /帮助 + [空格] + [插件名] - 若不知道一个插件有哪些功能, 请试试使用该指令 53 | 3. /检查更新 - 验性功能, 检查 FloraBot 是否有新的版本可更新, 并且引导你进行下一步更新 54 | 4. /插件列表 - 将该指令移出仅 Bot 管理员可用指令""" 55 | 56 | plugins_dict = {} # 插件对象字典 57 | plugins_info_dict = {} # 插件信息字典 58 | flora_help_dict = { 59 | "Flora": { 60 | "Help": [ 61 | { 62 | "Class": "基础功能", 63 | "Commands": [ 64 | { 65 | "Command": "/帮助", 66 | "Content": "若不知道 FloraBot 有哪些功能, 请试试使用该指令" 67 | }, 68 | { 69 | "Command": "/帮助 + [空格] + [插件名]", 70 | "Content": "若不知道一个插件有哪些功能, 请试试使用该指令" 71 | } 72 | ] 73 | }, 74 | { 75 | "Class": "插件相关", 76 | "Commands": [ 77 | { 78 | "Command": "/插件列表", 79 | "Content": "查看已添加的插件以及插件的禁启用状态" 80 | } 81 | ] 82 | }, 83 | { 84 | "Class": "插件管理(仅 Bot 管理员可用)", 85 | "AdminUse": True, 86 | "Commands": [ 87 | { 88 | "Command": "/重载插件", 89 | "Content": "若未找到插件, 但插件文件已添加, 请试试使用该指令" 90 | }, 91 | { 92 | "Command": "/启用插件 + [空格] + [插件名]", 93 | "Content": "启用一个处于禁用状态的插件" 94 | }, 95 | { 96 | "Command": "/禁用插件 + [空格] + [插件名]", 97 | "Content": "禁用一个处于启用状态的插件" 98 | } 99 | ] 100 | }, 101 | { 102 | "Class": "调试与测试(仅 Bot 管理员可用)", 103 | "AdminUse": True, 104 | "Commands": [ 105 | { 106 | "Command": "/echo + [空格] + [内容]", 107 | "Content": "测试 Bot 是否正常工作, 让 Bot 账号复述一遍内容" 108 | }, 109 | { 110 | "Command": "/echo1 + [空格] + [内容]", 111 | "Content": "与 /echo 功能一致, 复述不为回复" 112 | }, 113 | { 114 | "Command": "/API测试 + [空格] + [API(终结点, 不要带上\"/\")] + [空格] + 参数 + [空格] + [参数(Json格式)]", 115 | "Content": "测试调用框架的 API, 后面的\"参数\"部分可选(根据 API 而定, 有的 API 无需参数)" 116 | } 117 | ] 118 | }, 119 | { 120 | "Class": "更新 FloraBot (仅 Bot 管理员可用)", 121 | "AdminUse": True, 122 | "Commands": [ 123 | { 124 | "Command": "/检查更新", 125 | "Content": "实验性功能, 检查 FloraBot 是否有新的版本可更新, 并且引导你进行下一步更新" 126 | } 127 | ] 128 | } 129 | ] 130 | } 131 | } 132 | help_info_dict = {} 133 | 134 | 135 | def load_config(): # 加载FloraBot配置文件函数 136 | global auto_install, connection_type, flora_host, flora_port, framework_address, bot_id, administrator 137 | if not os.path.isdir("./FloraBot"): 138 | os.makedirs("./FloraBot") 139 | if not os.path.isdir("./FloraBot/Plugins"): 140 | os.makedirs("./FloraBot/Plugins") 141 | if os.path.isdir("./FloraBot/UpdateCache"): 142 | shutil.rmtree("./FloraBot/UpdateCache") 143 | if os.path.isfile("./Config.json"): # 若文件存在 144 | with open("./Config.json", "r", encoding="UTF-8") as read_flora_config: 145 | flora_config = json.loads(read_flora_config.read()) 146 | auto_install = flora_config.get("AutoInstallLibraries") 147 | connection_type = flora_config.get("ConnectionType") 148 | flora_api.update({"ConnectionType": connection_type}) 149 | flora_host = flora_config.get("FloraHost") 150 | flora_api.update({"FloraHost": flora_host}) 151 | flora_port = flora_config.get("FloraPort") 152 | flora_api.update({"FloraPort": flora_port}) 153 | framework_address = flora_config.get("FrameworkAddress") 154 | flora_api.update({"FrameworkAddress": framework_address}) 155 | bot_id = flora_config.get("BotID") 156 | flora_api.update({"BotID": bot_id}) 157 | administrator = flora_config.get("Administrator") 158 | flora_api.update({"Administrator": administrator}) 159 | else: # 若文件不存在 160 | print("FloraBot 启动失败, 未找到配置文件 Config.json") 161 | with open("./Config.json", "w", encoding="UTF-8") as write_flora_config: 162 | write_flora_config.write(json.dumps({"AutoInstallLibraries": True, "ConnectionType": "HTTP", "FloraHost": "127.0.0.1", "FloraPort": 3003, "FrameworkAddress": "127.0.0.1:3000", "BotID": 0, "Administrator": [0]}, ensure_ascii=False, indent=4)) 163 | print("已生成一个新的配置文件 Config.json , 请修改后再次启动 FloraBot") 164 | exit() 165 | 166 | 167 | def send_msg(send_type: str, msg: str, uid: str | int, gid: str | int | None, mid: str | int | None = None, ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): 168 | # 发送消息函数,send_type: 发送类型,决定是用HTTP还是WebSocket发送消息 169 | # msg: 正文,uid: ID,gid: 群号,mid: 消息编号 170 | # ws_client: WebSocket连接实例,ws_server: WebSocket服务端实例(若发送类型为WebSocket这两个参数必填) 171 | # send_host: HTTP协议发送地址,send_port: HTTP协议发送端口(若填这两个参数则使用自定义地址发送) 172 | if send_type == "HTTP": 173 | send_address = framework_address 174 | if send_host != "" and send_port != "": 175 | send_address = f"{send_host}:{send_port}" 176 | url = f"http://{send_address}" 177 | data = {} 178 | if mid is not None: # 当消息编号不为None时,则发送的消息为回复 179 | data.update({"message": f"[CQ:reply,id={mid}]{msg}"}) 180 | else: # 反之为普通消息 181 | data.update({"message": msg}) 182 | if gid is not None: # 当群号不为None时,则发送给群聊 183 | url += f"/send_group_msg" 184 | data.update({"group_id": gid}) 185 | else: # 反之为私聊 186 | url += f"/send_private_msg" 187 | data.update({"user_id": uid}) 188 | try: 189 | return requests.post(url, json=data).json() # 提交发送消息 190 | except requests.exceptions.RequestException: 191 | pass 192 | elif send_type == "WebSocket": 193 | data = {} 194 | send_params = {} 195 | if mid is not None: # 当消息编号不为None时,则发送的消息为回复 196 | send_params.update({"message": f"[CQ:reply,id={mid}]{msg}"}) 197 | else: # 反之为普通消息 198 | send_params.update({"message": msg}) 199 | if gid is not None: # 当群号不为None时,则发送给群聊 200 | data.update({"action": "send_group_msg"}) 201 | send_params.update({"group_id": gid}) 202 | else: # 反之为私聊 203 | data.update({"action": "send_private_msg"}) 204 | send_params.update({"user_id": uid}) 205 | data.update({"params": send_params}) 206 | try: 207 | if ws_server is not None and ws_client is not None: 208 | ws_server.send_message(ws_client, json.dumps(data, ensure_ascii=False)) 209 | random_num = random.random() 210 | call_api_return.append(random_num) 211 | while random_num not in call_api_returned: 212 | if exit_flag: 213 | break 214 | time.sleep(0.1) 215 | if exit_flag: 216 | return None 217 | else: 218 | return call_api_returned.pop(random_num) 219 | except ConnectionError: 220 | return None 221 | except TypeError: 222 | return None 223 | 224 | 225 | def call_api(send_type: str, api: str, params: dict, ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): 226 | # 发送消息函数,send_type: 发送类型,决定是用HTTP还是WebSocket发送消息 227 | # api: 接口(str类型), data: 数据(dict类型) 228 | # ws_client: WebSocket连接实例,ws_server: WebSocket服务端实例(若发送类型为WebSocket这两个参数必填) 229 | # send_host: HTTP协议发送地址,send_port: HTTP协议发送端口(若填这两个参数则使用自定义地址发送) 230 | if send_type == "HTTP": 231 | send_address = framework_address 232 | if send_host != "" and send_port != "": 233 | send_address = f"{send_host}:{send_port}" 234 | try: 235 | return requests.post(f"http://{send_address}/{api}", json=params).json() # 提交发送消息 236 | except requests.exceptions.RequestException: 237 | return None 238 | elif send_type == "WebSocket": 239 | try: 240 | if ws_server is not None and ws_client is not None: 241 | ws_server.send_message(ws_client, json.dumps({"action": api, "params": params}, ensure_ascii=False)) 242 | random_num = random.random() 243 | call_api_return.append(random_num) 244 | while random_num not in call_api_returned: 245 | if exit_flag: 246 | break 247 | time.sleep(0.1) 248 | if exit_flag: 249 | return None 250 | else: 251 | return call_api_returned.pop(random_num) 252 | except ConnectionError: 253 | return None 254 | except TypeError: 255 | return None 256 | 257 | 258 | def install_libraries(libraries_name: str): 259 | if importlib.util.find_spec(libraries_name) is None: 260 | print(f"正在安装 {libraries_name} 库...") 261 | try: 262 | subprocess.check_call([sys.executable, "-m", "pip", "install", libraries_name]) 263 | print(f"{libraries_name} 库安装完成") 264 | except subprocess.CalledProcessError: 265 | print(f"{libraries_name} 库安装失败") 266 | 267 | 268 | def load_plugins(): # 加载插件函数 269 | print("正在加载插件, 请稍后...") 270 | help_info_dict.clear() 271 | help_info_dict.update(flora_help_dict) 272 | plugins_help_info_dict = {} 273 | plugins_info_dict.clear() 274 | plugins_dict.clear() 275 | for plugin in os.listdir("./FloraBot/Plugins"): # 遍历所有插件 276 | plugin_path = f"FloraBot/Plugins/{plugin}" 277 | if os.path.isfile(f"./{plugin_path}/Plugin.json"): 278 | with open(f"./{plugin_path}/Plugin.json", "r", encoding="UTF-8") as read_plugin_config: 279 | plugin_config = json.loads(read_plugin_config.read()) 280 | if os.path.isfile(f"./{plugin_path}/{plugin_config.get('MainPyName')}.py") and plugin_config.get("EnablePlugin"): # 如果配置正确则导入插件 281 | plugin_config = plugin_config.copy() 282 | print(f"正在加载插件 {plugin_config.get('PluginName')} ...") 283 | plugin_config.update({"ThePluginPath": plugin_path}) 284 | plugins_info_dict.update({plugin_config.get("PluginName"): plugin_config}) # 添加插件信息 285 | if plugin_config.get("Help") is not None: 286 | plugins_help_info_dict.update({plugin_config.get("PluginName"): {"Help": plugin_config.get("Help")}}) 287 | if auto_install and plugin_config.get("DependentLibraries") is not None: 288 | print("已开启自动安装依赖库, 正在安装插件所依赖的库...") 289 | for libraries_name in plugin_config.get("DependentLibraries"): 290 | install_libraries(libraries_name) 291 | spec = importlib.util.spec_from_file_location(plugin_config.get("MainPyName"), f"./{plugin_path}/{plugin_config.get('MainPyName')}.py") 292 | module = importlib.util.module_from_spec(spec) 293 | try: 294 | spec.loader.exec_module(module) 295 | except ModuleNotFoundError as error_info: 296 | if auto_install: 297 | install_libraries(str(error_info).split("'")[1]) 298 | spec.loader.exec_module(module) 299 | else: 300 | raise ModuleNotFoundError(error_info) 301 | try: 302 | module.flora_api = flora_api.copy() # 传入API参数 303 | except AttributeError: 304 | pass 305 | module.flora_api.update({"ThePluginPath": plugin_path}) 306 | try: 307 | threading.Thread(target=module.init).start() # 开线程初始化插件 308 | except AttributeError: 309 | pass 310 | plugins_dict.update({plugin_config.get("PluginName"): module}) # 添加插件对象 311 | help_info_dict.update({"Plugins": plugins_help_info_dict}) 312 | update_flora_api() 313 | 314 | 315 | def broadcast_event(data: dict, send_type: str, ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): 316 | # 广播消息函数,data: 基于OneBot协议的数据 317 | # send_type: 发送类型,告诉插件是用HTTP还是WebSocket发送消息 318 | # ws_client: WebSocket连接实例,ws_server: WebSocket服务端实例(若发送类型为WebSocket这两个参数必填) 319 | # send_host: HTTP协议发送地址,send_port: HTTP协议发送端口(若填这两个参数则使用自定义地址发送) 320 | threading.Thread(target=builtin_function, args=(data, send_type, ws_client, ws_server, send_host, send_port)).start() 321 | send_address = {} 322 | if send_type == "HTTP": 323 | send_address.update({"WebSocketClient": None, "WebSocketServer": None, "SendHost": send_host, "SendPort": send_port}) 324 | elif send_type == "WebSocket": 325 | send_address.update({"WebSocketClient": ws_client, "WebSocketServer": ws_server, "SendHost": "", "SendPort": ""}) 326 | data.update({"SendType": send_type, "SendAddress": send_address}) 327 | for plugin in plugins_dict.values(): # 遍历开线程调用所有的插件事件函数 328 | try: 329 | threading.Thread(target=plugin.event, kwargs={"data": data}).start() 330 | except AttributeError: 331 | pass 332 | 333 | 334 | def update_flora_api(): # 更新API内容函数 335 | # noinspection PyTypeChecker 336 | flora_api.update({"PluginsDict": plugins_dict.copy(), "PluginsInfoDict": plugins_info_dict.copy(), "HelpInfoDict": help_info_dict.copy()}) 337 | for plugin in plugins_dict.values(): 338 | try: 339 | plugin.flora_api.update(flora_api.copy()) 340 | except AttributeError: 341 | pass 342 | for plugin in plugins_dict.values(): # 遍历开线程调用所有的API更新事件函数 343 | try: 344 | threading.Thread(target=plugin.api_update_event).start() 345 | except AttributeError: 346 | pass 347 | 348 | 349 | def builtin_function(data: dict, send_type: str = "HTTP", ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): # 一些内置的功能 350 | uid = data.get("user_id") 351 | gid = data.get("group_id") 352 | mid = data.get("message_id") 353 | msg = data.get("raw_message") 354 | if msg is not None: 355 | msg = msg.replace("[", "[").replace("]", "]").replace("&", "&").replace(",", ",") # 消息需要将URL编码替换到正确内容 356 | if msg == "/帮助": 357 | send_text = f"FloraBot {flora_version}\n\n帮助菜单:\nFloraBot 内置功能:" 358 | for help_class in help_info_dict.get("Flora").get("Help"): 359 | if help_class.get("AdminUse") is not None and help_class.get("AdminUse") and uid not in administrator: 360 | continue 361 | send_text += f"\n {help_class.get('Class')}:" 362 | for help_command in help_class.get("Commands"): 363 | send_text += f"\n •{help_command.get('Command')} - {help_command.get('Content')}" 364 | send_text += "\n\n若要查看插件的帮助菜单, 请使用指令\n\"/帮助 + [空格] + [插件名]\"" 365 | send_msg(send_type, send_text, uid, gid, mid, ws_client, ws_server, send_host, send_port) 366 | elif msg.startswith("/帮助 "): 367 | msg = msg.replace("/帮助 ", "", 1) 368 | if help_info_dict.get("Plugins") is not None and msg in help_info_dict.get("Plugins"): 369 | send_text = f"FloraBot {flora_version}\n\n帮助菜单:\n{msg}:" 370 | for help_class in help_info_dict.get("Plugins").get(msg).get("Help"): 371 | if help_class.get("AdminUse") is not None and help_class.get("AdminUse") and uid not in administrator: 372 | continue 373 | send_text += f"\n {help_class.get('Class')}:" 374 | for help_command in help_class.get("Commands"): 375 | send_text += f"\n •{help_command.get('Command')} - {help_command.get('Content')}" 376 | send_text += "\n\n若要查看其他插件的帮助菜单, 请使用指令\n\"/帮助 + [空格] + [插件名]\"" 377 | send_msg(send_type, send_text, uid, gid, mid, ws_client, ws_server, send_host, send_port) 378 | else: 379 | send_msg(send_type, f"未找到插件 {msg} 的帮助", uid, gid, mid, ws_client, ws_server, send_host, send_port) 380 | elif msg == "/插件列表": 381 | plugins = f"FloraBot {flora_version}\n\n插件列表:\n" 382 | for plugin_info in plugins_info_dict.values(): 383 | plugin_status = "启用" 384 | if not plugin_info.get("EnablePlugin"): 385 | plugin_status = "禁用" 386 | plugins += f"•{plugin_info.get('PluginName')} [状态: {plugin_status}]\n" 387 | plugins += f"\n共有 {len(plugins_info_dict)} 个插件, 已启用 {len(plugins_dict)} 个插件" 388 | if uid in administrator: 389 | plugins += "\n可使用 \"/启用/禁用插件 + [空格] + [插件名]\" 来启用或者禁用插件\n若未找到插件, 但插件文件已添加, 请试试使用 \"/重载插件\"" 390 | send_msg(send_type, plugins, uid, gid, mid, ws_client, ws_server, send_host, send_port) 391 | elif uid in administrator: 392 | admin_function(msg, uid, gid, mid, send_type, ws_client, ws_server, send_host, send_port) 393 | 394 | 395 | update_request_id = [] 396 | update_flora = False 397 | flora_updater_py = """import shutil 398 | import os 399 | import subprocess 400 | import sys 401 | 402 | source_dir = "./FloraBot-main" 403 | target_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 404 | 405 | for item in os.listdir(source_dir): 406 | source_path = os.path.join(source_dir, item) 407 | target_path = os.path.join(target_dir, item) 408 | if os.path.exists(target_path): 409 | if os.path.isfile(target_path) or os.path.islink(target_path): 410 | os.remove(target_path) 411 | elif os.path.isdir(target_path): 412 | shutil.rmtree(target_path) 413 | shutil.move(source_path, target_path) 414 | shutil.rmtree(source_dir) 415 | 416 | subprocess.Popen([sys.executable, os.path.join(target_dir, "FloraBot.py")]) 417 | """ 418 | 419 | 420 | def admin_function(msg: str, uid: str | int, gid: str | int | None, mid: str | int | None = None, send_type: str = "HTTP", ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): # 一些内置的功能 421 | global update_flora 422 | if msg == "/重载插件": 423 | send_msg(send_type, "正在重载插件, 请稍后...", uid, gid, mid, ws_client, ws_server) 424 | load_plugins() 425 | send_msg(send_type, f"FloraBot {flora_version}\n\n插件重载完成, 共有 {len(plugins_info_dict)} 个插件, 已启用 {len(plugins_dict)} 个插件", uid, gid, mid, ws_client, ws_server, send_host, send_port) 426 | elif msg.startswith("/启用插件 "): 427 | msg = msg.replace("/启用插件 ", "", 1) 428 | if plugins_info_dict.get(msg) is not None and not plugins_info_dict.get(msg).get("EnablePlugin"): 429 | plugin_info = plugins_info_dict.get(msg) 430 | plugin_info.update({"EnablePlugin": True}) 431 | plugins_info_dict.update({msg: plugin_info}) 432 | spec = importlib.util.spec_from_file_location(plugin_info.get("MainPyName"), f"./{plugin_info.get('ThePluginPath')}/{plugin_info.get('MainPyName')}.py") 433 | module = importlib.util.module_from_spec(spec) 434 | try: 435 | spec.loader.exec_module(module) 436 | except ModuleNotFoundError as error_info: 437 | if auto_install: 438 | install_libraries(str(error_info).split("'")[1]) 439 | spec.loader.exec_module(module) 440 | else: 441 | raise ModuleNotFoundError(error_info) 442 | try: 443 | module.flora_api = flora_api.copy() 444 | except AttributeError: 445 | pass 446 | module.flora_api.update({"ThePluginPath": plugin_info.get("ThePluginPath")}) 447 | try: 448 | threading.Thread(target=module.init).start() 449 | except AttributeError: 450 | pass 451 | plugins_dict.update({plugin_info.get("PluginName"): module}) 452 | update_flora_api() 453 | with open(f"./{plugin_info.get('ThePluginPath')}/Plugin.json", "w", encoding="UTF-8") as write_plugin_config: 454 | plugin_info_copy = plugin_info.copy() 455 | plugin_info_copy.pop("ThePluginPath") 456 | write_plugin_config.write(json.dumps(plugin_info_copy, ensure_ascii=False, indent=4)) 457 | send_msg(send_type, f"FloraBot {flora_version}\n\n插件 {msg} 已启用, 共有 {len(plugins_info_dict)} 个插件, 已启用 {len(plugins_dict)} 个插件", uid, gid, mid, ws_client, ws_server, send_host, send_port) 458 | else: 459 | send_msg(send_type, f"FloraBot {flora_version}\n\n未找到或已启用插件 {msg} , 若未找到插件, 但插件文件已添加, 请试试使用 \"/重载插件\"", uid, gid, mid, ws_client, ws_server, send_host, send_port) 460 | elif msg.startswith("/禁用插件 "): 461 | msg = msg.replace("/禁用插件 ", "", 1) 462 | if plugins_info_dict.get(msg) is not None and plugins_info_dict.get(msg).get("EnablePlugin"): 463 | plugin_info = plugins_info_dict.get(msg) 464 | plugin_info.update({"EnablePlugin": False}) 465 | plugins_info_dict.update({msg: plugin_info}) 466 | if plugins_dict.get(msg) is not None: 467 | plugins_dict.pop(msg) 468 | update_flora_api() 469 | with open(f"./{plugin_info.get('ThePluginPath')}/Plugin.json", "w", encoding="UTF-8") as write_plugin_config: 470 | plugin_info_copy = plugin_info.copy() 471 | plugin_info_copy.pop("ThePluginPath") 472 | write_plugin_config.write(json.dumps(plugin_info_copy, ensure_ascii=False, indent=4)) 473 | send_msg(send_type, f"FloraBot {flora_version}\n\n插件 {msg} 已禁用, 共有 {len(plugins_info_dict)} 个插件, 已启用 {len(plugins_dict)} 个插件", uid, gid, mid, ws_client, ws_server, send_host, send_port) 474 | else: 475 | send_msg(send_type, f"FloraBot {flora_version}\n\n未找到或已禁用插件 {msg} , 若未找到插件, 但插件文件已添加, 请试试使用 \"/重载插件\"", uid, gid, mid, ws_client, ws_server, send_host, send_port) 476 | elif msg.startswith("/echo "): 477 | send_msg(send_type, msg.replace("/echo ", "", 1), uid, gid, mid, ws_client, ws_server, send_host, send_port) 478 | elif msg.startswith("/echo1 "): 479 | send_msg(send_type, msg.replace("/echo1 ", "", 1), uid, gid, None, ws_client, ws_server, send_host, send_port) 480 | elif msg.startswith("/API测试 "): 481 | search_api = re.search(r"/API测试 (.*?)( 参数|$)", msg).group(1) 482 | if not search_api.isspace() and search_api != "": 483 | search_params = re.search(r"参数 (.*)", msg) 484 | call_params = {} 485 | if search_params is not None: 486 | search_params = search_params.group(1) 487 | if not search_params.isspace() and search_params != "": 488 | try: 489 | call_params = json.loads(search_params) 490 | except json.JSONDecodeError: 491 | send_msg(send_type, "参数错误, 参数不是标准的 Json 格式", uid, gid, mid, ws_client, ws_server, send_host, send_port) 492 | send_msg(send_type, json.dumps(call_api(send_type, search_api, call_params, ws_client, ws_server, send_host, send_port), ensure_ascii=False, indent=4), uid, gid, mid, ws_client, ws_server,send_host, send_port) 493 | elif msg == "/检查更新": 494 | if not update_flora: 495 | if len(update_request_id) != 0: 496 | if time.time() - update_request_id[1] > 60: 497 | update_request_id.clear() 498 | update_flora = False 499 | if len(update_request_id) == 0: 500 | update_request_id.extend([uid, time.time()]) 501 | send_msg(send_type, "请选择一个 GitHub 源:\n1. 官方源(github.com)\n2. gh.llkk.cc\n3. github.moeyy.xyz\n4. mirror.ghproxy.com\n5. ghproxy.net\n6. gh.ddlc.top\n\n下一步请在一分钟之内发送以下格式的指令:\n/GitHub源 + [空格] + [序号(1~6)]", uid, gid, mid, ws_client, ws_server, send_host, send_port) 502 | else: 503 | send_msg(send_type, "已经有 Bot 管理员发起了更新请求, 请稍等一会", uid, gid, mid, ws_client, ws_server, send_host, send_port) 504 | else: 505 | send_msg(send_type, "FloraBot 正在执行步骤", uid, gid, mid, ws_client, ws_server, send_host, send_port) 506 | elif msg.startswith("/GitHub源 ") and len(update_request_id) != 0 and not update_flora: 507 | if time.time() - update_request_id[1] > 60: 508 | update_request_id.clear() 509 | update_flora = False 510 | if len(update_request_id) != 0 and uid == update_request_id[0]: 511 | msg = msg.replace("/GitHub源 ", "", 1) 512 | try: 513 | update_flora = True 514 | file_source = "https://raw.githubusercontent.com/FloraBotTeam/FloraBot/main/FloraBot.py" 515 | download_source = "" 516 | msg = int(msg) 517 | if msg == 1: 518 | send_msg(send_type, "你选择了 1, 即将使用 官方 源", uid, gid, mid, ws_client, ws_server, send_host, send_port) 519 | elif msg == 2: 520 | download_source = "https://gh.llkk.cc/" 521 | send_msg(send_type, "你选择了 2, 即将使用 gh.llkk.cc 镜像源", uid, gid, mid, ws_client, ws_server, send_host, send_port) 522 | elif msg == 3: 523 | download_source = "https://github.moeyy.xyz/" 524 | send_msg(send_type, "你选择了 3, 即将使用 github.moeyy.xyz 镜像源", uid, gid, mid, ws_client, ws_server, send_host, send_port) 525 | elif msg == 4: 526 | download_source = "https://mirror.ghproxy.com/" 527 | send_msg(send_type, "你选择了 4, 即将使用 mirror.ghproxy.com 镜像源", uid, gid, mid, ws_client, ws_server, send_host, send_port) 528 | elif msg == 5: 529 | download_source = "https://ghproxy.net/" 530 | send_msg(send_type, "你选择了 5, 即将使用 ghproxy.net 镜像源", uid, gid, mid, ws_client, ws_server, send_host, send_port) 531 | elif msg == 6: 532 | download_source = "https://gh.ddlc.top/" 533 | send_msg(send_type, "你选择了 6, 即将使用 gh.ddlc.top 镜像源", uid, gid, mid, ws_client, ws_server, send_host, send_port) 534 | else: 535 | update_flora = False 536 | send_msg(send_type, "参数错误, 序号的范围应该为 1~6\n正确的指令格式为:\n/GitHub源 + [空格] + [序号(1~6)]", uid, gid, mid, ws_client, ws_server, send_host, send_port) 537 | if 1 <= msg <= 6: 538 | send_msg(send_type, "开始检查更新 FloraBot, 请稍后...", uid, gid, None, ws_client, ws_server, send_host, send_port) 539 | try: 540 | update_request_id.append(download_source) 541 | response = requests.get(download_source + file_source) 542 | response.raise_for_status() 543 | get_version = re.search(r'flora_version\s*=\s*"(.+?)"', response.text) 544 | if get_version is not None: 545 | get_version = get_version.group(1) 546 | if get_version != flora_version: 547 | get_update_content = re.search(r'update_content\s*=\s*"""(.+?)"""', response.text) 548 | if get_update_content is not None: 549 | get_update_content = get_update_content.group(1) 550 | else: 551 | get_update_content = "未获取到更新日志" 552 | send_msg(send_type, f"检查到 FloraBot 有新的版本, 当前版本为 {flora_version}, 最新版本为 {get_version}, 更新内容:\n{get_update_content}", uid, gid, None, ws_client, ws_server, send_host, send_port) 553 | is_big_update = re.search(r"\bbig_update\s*=\s*True\b", response.text) 554 | send_text = "" 555 | if is_big_update is not None: 556 | send_text = "更新到最新版本为大更新, 可能会出现插件功能失效等问题\n\n" 557 | send_text += "确认要更新到最新版吗?\n\n下一步请在一分钟之内发送以下指令:\n/确认更新" 558 | update_request_id[1] = time.time() 559 | send_msg(send_type, send_text, uid, gid, mid, ws_client, ws_server, send_host, send_port) 560 | else: 561 | update_flora = False 562 | send_msg(send_type, f"FloraBot 当前已是最新版本", uid, gid, None, ws_client, ws_server, send_host, send_port) 563 | else: 564 | update_flora = False 565 | send_msg(send_type, f"FloraBot 检查更新失败, 获取版本号失败", uid, gid, None, ws_client, ws_server, send_host, send_port) 566 | except requests.RequestException as error_info: 567 | update_flora = False 568 | send_msg(send_type, f"FloraBot 检查更新失败, 网络请求出现异常, 详细信息: {error_info}", uid, gid, None, ws_client, ws_server, send_host, send_port) 569 | except ValueError: 570 | update_flora = False 571 | send_msg(send_type, "参数错误, 这好像不是序号吧\n正确的指令格式为:\n/GitHub源 + [空格] + [序号(1~6)]", uid, gid, mid, ws_client, ws_server, send_host, send_port) 572 | elif msg == "/确认更新" and len(update_request_id) != 0 and update_flora: 573 | if time.time() - update_request_id[1] > 60: 574 | update_request_id.clear() 575 | update_flora = False 576 | if len(update_request_id) != 0 and uid == update_request_id[0]: 577 | send_msg(send_type, "开始更新 FloraBot, 请稍后...", uid, gid, mid, ws_client, ws_server, send_host, send_port) 578 | try: 579 | send_msg(send_type, "开始下载 FloraBot ...", uid, gid, None, ws_client, ws_server, send_host, send_port) 580 | response = requests.get(update_request_id[2] + "https://github.com/FloraBotTeam/FloraBot/archive/main.zip") 581 | response.raise_for_status() 582 | if not os.path.isdir("./FloraBot/UpdateCache"): 583 | os.makedirs("./FloraBot/UpdateCache") 584 | with open("./FloraBot/UpdateCache/FloraBot.zip", "wb") as flora_file: 585 | flora_file.write(response.content) 586 | send_msg(send_type, "FloraBot 下载完成, 开始解压 FloraBot ...", uid, gid, None, ws_client, ws_server, send_host, send_port) 587 | zip_object = zipfile.ZipFile("./FloraBot/UpdateCache/FloraBot.zip") 588 | for file in zip_object.namelist(): 589 | zip_object.extract(file, "./FloraBot/UpdateCache") 590 | zip_object.close() 591 | os.remove("./FloraBot/UpdateCache/FloraBot.zip") 592 | shutil.rmtree(f"./FloraBot/UpdateCache/FloraBot-main/PluginTemplate") 593 | send_msg(send_type, "FloraBot 解压完成, 开始更新依赖的库...", uid, gid, None, ws_client, ws_server, send_host, send_port) 594 | try: 595 | subprocess.check_call([sys.executable, "-m", "pip", "install", "-r", "./FloraBot/UpdateCache/FloraBot-main/requirements.txt"]) 596 | os.remove("./FloraBot/UpdateCache/FloraBot-main/requirements.txt") 597 | send_msg(send_type, "依赖的库更新完成, 开始替换更新 FloraBot, 这将会重启 FloraBot, FloraBot 重启后即为更新完成", uid, gid, None, ws_client, ws_server, send_host, send_port) 598 | except subprocess.CalledProcessError: 599 | send_msg(send_type, "依赖的库更新失败, 跳过更新依赖的库, 开始替换更新 FloraBot, 这将会重启 FloraBot, FloraBot 重启后即为更新完成", uid, gid, None, ws_client, ws_server, send_host, send_port) 600 | with open("./FloraBot/UpdateCache/FloraUpdater.py", "w") as flora_updater: 601 | flora_updater.write(flora_updater_py) 602 | subprocess.Popen([sys.executable, "./FloraBot/UpdateCache/FloraUpdater.py"]) 603 | # noinspection PyUnresolvedReferences 604 | # noinspection PyProtectedMember 605 | os._exit(0) 606 | except requests.RequestException as error_info: 607 | update_flora = False 608 | send_msg(send_type, f"FloraBot 更新失败, 网络请求出现异常, 详细信息: {error_info}", uid, gid, None, ws_client, ws_server, send_host, send_port) 609 | 610 | 611 | @flora_server.post("/") 612 | def http_message_received(): # HTTP协议消息接收函数,不要主动调用这个函数 613 | data = request.get_json() # 获取提交数据 614 | threading.Thread(target=broadcast_event, args=(data, "HTTP")).start() # 遍历开线程调用所有的插件事件函数 615 | return "OK" 616 | 617 | 618 | call_api_return = [] # WebSocket协议等待回调编号注册列表(按先后顺序) 619 | call_api_returned = {} # WebSocket协议等待回调结果字典, 键值为: {编号: 数据} 620 | 621 | 622 | def ws_message_received(client, server, message): # WebSocket消息接收函数,不要主动调用这个函数 623 | global call_api_return, call_api_returned 624 | try: 625 | data = json.loads(message) 626 | if "status" in data and len(call_api_return) != 0: 627 | call_api_returned.update({call_api_return.pop(0): data}) 628 | elif data.get("meta_event_type") != "heartbeat": 629 | threading.Thread(target=broadcast_event, args=(data, "WebSocket", client, server)).start() # 遍历开线程调用所有的插件事件函数 630 | except json.JSONDecodeError: 631 | pass 632 | 633 | 634 | def check_privileges(): 635 | system = platform.system() 636 | print(f"当前系统为 {system}") 637 | if system == "Windows": 638 | try: 639 | from ctypes import windll 640 | is_admin = windll.shell32.IsUserAnAdmin() 641 | except: 642 | is_admin = False 643 | if is_admin: 644 | print("\033[91m警告: 当前用户具有管理员权限, 若是添加了恶意插件, 后果不堪设想!!!\033[0m") 645 | elif system in ["Linux", "Darwin"]: 646 | if os.geteuid() == 0: 647 | print("\033[91m警告: 当前用户具有 root 权限, 若是添加了恶意插件, 后果不堪设想!!!\033[0m") 648 | 649 | 650 | logger = logging.getLogger(__name__) 651 | logging.basicConfig() 652 | 653 | 654 | class FloraWebSocketHandler(WebSocketHandler): # 重写WebSocketHandler类 655 | def read_http_headers(self): 656 | headers = {} 657 | http_get = self.rfile.readline().decode().strip() 658 | if not http_get.upper().startswith("GET"): 659 | logger.warning("框架使用了不正确的 HTTP 方法进行请求 WebSocket 通信") 660 | response = "HTTP/1.1 400 Bad Request\r\n\r\n" 661 | with self._send_lock: 662 | self.request.sendall(response.encode()) 663 | self.keep_alive = False 664 | return headers 665 | while True: 666 | header = self.rfile.readline().decode().strip() 667 | if not header: 668 | break 669 | head, value = header.split(":", 1) 670 | headers[head.lower().strip()] = value.strip() 671 | return headers 672 | 673 | def handshake(self): 674 | headers = self.read_http_headers() 675 | if "upgrade" in headers: 676 | try: 677 | assert headers["upgrade"].lower() == "websocket" 678 | except AssertionError: 679 | self.keep_alive = False 680 | return 681 | try: 682 | key = headers["sec-websocket-key"] 683 | except KeyError: 684 | logger.warning("框架尝试进行连接, 但缺少密钥") 685 | self.keep_alive = False 686 | return 687 | response = self.make_handshake_response(key) 688 | with self._send_lock: 689 | self.handshake_done = self.request.send(response.encode()) 690 | self.valid_client = True 691 | self.server._new_client_(self) 692 | else: 693 | logger.warning("框架使用了不正确的 WebSocket 协议进行通信") 694 | response = "HTTP/1.1 400 Bad Request\r\n\r\n" 695 | with self._send_lock: 696 | self.request.sendall(response.encode()) 697 | self.keep_alive = False 698 | 699 | 700 | class FloraWebsocketServer(WebsocketServer): # 重写WebsocketServer类 701 | def __init__(self, host="127.0.0.1", port=0, loglevel=logging.WARNING, key=None, cert=None): 702 | logger.setLevel(loglevel) 703 | # noinspection PyTypeChecker 704 | TCPServer.__init__(self, (host, port), FloraWebSocketHandler) 705 | self.host = host 706 | self.port = self.socket.getsockname()[1] 707 | self.key = key 708 | self.cert = cert 709 | self.clients = [] 710 | self.id_counter = 0 711 | self.thread = None 712 | self._deny_clients = False 713 | 714 | 715 | def client_connect(client, server): 716 | global exit_flag 717 | exit_flag = False 718 | print(f"框架已通过 WebSocket 连接, 连接ID: {client.get('id')}") 719 | 720 | 721 | exit_flag = False 722 | 723 | 724 | def client_left(client, server): 725 | global call_api_return, call_api_returned, exit_flag 726 | if client is not None: 727 | exit_flag = True 728 | call_api_return.clear() 729 | call_api_returned.clear() 730 | print(f"框架已从 WebSocket 连接断开, 连接ID: {client.get('id')}") 731 | 732 | 733 | def reboot(): 734 | pass 735 | 736 | 737 | flora_api = {"FloraPath": os.path.dirname(os.path.abspath(__file__)), "ConnectionType": connection_type, "FloraHost": flora_host, "FloraPort": flora_port, "FrameworkAddress": framework_address, "BotID": bot_id, "Administrator": administrator, "FloraVersion": flora_version, "FloraServer": flora_server, "CallApiReturned": call_api_returned, "UpdateFloraApi": update_flora_api, "LoadPlugins": load_plugins, "BroadcastEvent": broadcast_event, "SendMsg": send_msg, "CallApi": call_api} 738 | 739 | 740 | if __name__ == "__main__": 741 | print(flora_logo) 742 | check_privileges() 743 | print("正在初始化 FloraBot , 请稍后...") 744 | load_config() 745 | print(f"欢迎使用 FloraBot {flora_version}") 746 | if use_ansi_color: 747 | print("\033[93m声明: 插件为第三方内容, 请您自行分辨是否为恶意插件, 若被恶意插件入侵/破坏了您的设备或恶意盗取了您的信息, 造成的损失请自负, FloraBotTeam 概不负责也无义务负责!!!\033[0m") 748 | else: 749 | print("声明: 插件为第三方内容, 请您自行分辨是否为恶意插件, 若被恶意插件入侵/破坏了您的设备或恶意盗取了您的信息, 造成的损失请自负, FloraBotTeam 概不负责也无义务负责!!!") 750 | load_plugins() 751 | print(f"框架连接方式为: {connection_type}") 752 | if connection_type == "HTTP": 753 | flora_server.run(host=flora_host, port=flora_port) 754 | elif connection_type == "WebSocket": 755 | flora_server_ws = FloraWebsocketServer(host=flora_host, port=flora_port) 756 | flora_server_ws.set_fn_new_client(client_connect) 757 | flora_server_ws.set_fn_client_left(client_left) 758 | flora_server_ws.set_fn_message_received(ws_message_received) 759 | flora_server_ws.run_forever() 760 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /PluginTemplate/Plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "PluginName": "插件名", 3 | "PluginVersion": "插件版本", 4 | "DependentLibraries": null, 5 | "IsLibraries": false, 6 | "PluginIcon": null, 7 | "PluginAuthor": "插件作者名", 8 | "MainPyName": "插件主 .py 文件名, 不要带上 .py 后缀名", 9 | "PluginDescription": "插件描述", 10 | "EnablePlugin": true, 11 | 12 | "Help": [ 13 | { 14 | "Class": "分类", 15 | "Commands": [ 16 | { 17 | "Command": "指令", 18 | "Content": "指令介绍" 19 | } 20 | ] 21 | } 22 | ] 23 | } -------------------------------------------------------------------------------- /PluginTemplate/PluginTemplate.py: -------------------------------------------------------------------------------- 1 | # 前言,这里用不到的函数可以不定义,可以直接删去,包括API也可以删去不定义,不会报错的 2 | 3 | flora_api = {} # 顾名思义,FloraBot的API,载入(若插件已设为禁用则不载入)后会赋值上 4 | 5 | 6 | def occupying_function(*values): # 该函数仅用于占位,并没有任何意义 7 | pass 8 | 9 | 10 | send_msg = occupying_function 11 | 12 | 13 | def init(): # 插件初始化函数,在载入(若插件已设为禁用则不载入)或启用插件时会调用一次,API可能没有那么快更新,可等待,无传入参数 14 | global send_msg 15 | print(flora_api) 16 | send_msg = flora_api.get("SendMsg") 17 | print("FloraBot插件模板 加载成功") 18 | 19 | 20 | def api_update_event(): # 在API更新时会调用一次(若插件已设为禁用则不调用),可及时获得最新的API内容,无传入参数 21 | print(flora_api) 22 | 23 | 24 | def event(data: dict): # 事件函数,FloraBot每收到一个事件都会调用这个函数(若插件已设为禁用则不调用),传入原消息JSON参数 25 | print(data) 26 | send_type = data.get("SendType") 27 | send_address = data.get("SendAddress") 28 | ws_client = send_address.get("WebSocketClient") 29 | ws_server = send_address.get("WebSocketServer") 30 | send_host = send_address.get("SendHost") 31 | send_port = send_address.get("SendPort") 32 | uid = data.get("user_id") # 事件对象QQ号 33 | gid = data.get("group_id") # 事件对象群号 34 | mid = data.get("message_id") # 消息ID 35 | msg = data.get("raw_message") # 消息内容 36 | if msg is not None: 37 | msg = msg.replace("[", "[").replace("]", "]").replace("&", "&").replace(",", ",") # 消息需要将URL编码替换到正确内容 38 | print(send_type, uid, gid, mid, msg, ws_client, ws_server, send_host, send_port) 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | FloraBot Logo 3 |

4 | 5 |
6 | 7 | # FloraBot 8 | 9 | ✨ 一个新的, 使用 Python 编写的支持插件的 ChatBot ✨ 10 | 11 |
12 | 13 |

14 | 15 | license 16 | 17 | 18 | python 19 | 20 | 21 | release 22 | 23 |

24 | 25 |

26 | 📖Docs 27 |

28 | 29 | ## 介绍 30 | 31 | FloraBot是一个功能强大的聊天机器人,旨在为用户提供智能、便捷的聊天服务。以下是FloraBot的主要特点: 32 | 33 | - **方便快捷**:FloraBot支持多个平台,旨在提供更加简便快捷的服务 34 | - **拓展方便**: Florabot允许用户自行开发和安装插件来拓展功能,并且开发难度低 35 | 36 | ## 快速开始 37 | 38 | 1. **安装Python** 39 | - Windows用户前往[Python官网](https://www.python.org/downloads)下载不低于`Python3.11`的版本进行安装。 40 | - Linux用户运行命令`apt install python3`,可将`python3`改为指定版本,如`python3.11`。 41 | 42 | 2. **下载FloraBot** 43 | - 点击GitHub仓库右上角的绿色`Code`按钮,选择`Download ZIP`下载,或通过链接下载[FloraBot源码](https://github.com/FloraBotTeam/FloraBot/archive/main.zip)。 44 | 45 | 3. **解压文件** 46 | - 解压下载的文件后,可删除`PluginTemplate`文件夹(开发者可保留)。 47 | 48 | 4. **创建启动脚本** 49 | - **Windows用户**:在`FloraBot.py`同级目录创建文本文档,内容为`python FloraBot.py`,保存后将后缀名改为`.bat`,双击运行。若无法运行Python,需将脚本内容改为Python.exe的绝对路径。 50 | - **Linux用户**:在`FloraBot.py`同级目录创建`.sh`文件,内容为`python3 FloraBot.py`,可将`python3`改为指定版本,如`python3.11`。 51 | 52 | 5. **安装必要库** 53 | - 打开终端(Windows使用CMD或PowerShell),运行以下命令安装所有必要库: 54 | ```Shell 55 | pip install -r requirements.txt文件所在路径 56 | ``` 57 | - 若手动安装,依次运行以下命令: 58 | ```Shell 59 | pip install flask 60 | pip install requests 61 | pip install websocket-server 62 | pip install colorama 63 | ``` 64 | - 若`pip`命令无法运行,可尝试将`pip`替换为`python3 -m pip`。 65 | 66 | 6. **首次启动与配置** 67 | - 首次启动会失败,但会在`FloraBot.py`同级目录生成`Config.json`文件,编辑该文件进行配置。 68 | 69 | ## 配置说明 70 | 71 | `Config.json`文件的键值对照表如下: 72 | 73 | ```Json 74 | { 75 | "AutoInstallLibraries": true, 76 | "ConnectionType": "HTTP", 77 | "FloraHost": "127.0.0.1", 78 | "FloraPort": 3003, 79 | "FrameworkAddress": "127.0.0.1:3000", 80 | "BotID": 0, 81 | "Administrator": [0] 82 | } 83 | ``` 84 | 85 | - **`AutoInstallLibraries`**:是否自动安装pip安装所需的第三方库,默认为`true`。 86 | - **`ConnectionType`**:Bot与框架的连接方式,可选`HTTP`和`WebSocket`,默认为`HTTP`。 87 | - **`FloraHost`**:Bot监听的IP地址,默认为`127.0.0.1`。 88 | - **`FloraPort`**:Bot监听的端口号,默认为`3003`。 89 | - **`FrameworkAddress`**:框架的Http协议监听地址,格式为`IP地址:端口号`,默认为`127.0.0.1:3000`。 90 | - **`BotID`**:登录的Bot账号的ID,默认为`0`。 91 | - **`Administrator`**:管理员/所有者/主人的ID列表,格式为`[ID, ID, ...]`,默认为`[0]`。 92 | 93 | ## 再次启动 94 | 95 | 完成配置后,再次启动FloraBot,若无意外,已可正常使用。 96 | 97 | ## 注意事项 98 | 99 | 部分框架使用WebSocket协议进行连接,Bot可能会警告WebSocket相关的问题。如果能够正常收发消息,请忽略警告;如果不能,则可能是框架的问题。 100 | 101 | ## 框架配置 102 | 103 | - **Http协议配置**:框架需要开启Http服务,启用Http事件上报,关闭Http心跳(避免出现Bug),并在事件上报地址中添加Bot的监听地址。若框架支持消息上报格式,设置为`CQ码`即可。 104 | - **WebSocket协议配置**:Bot仅支持反向WebSocket,因此框架应配置反向WebSocket,其他部分与HTTP协议配置相同。 105 | 106 | ## 添加插件 107 | 108 | **重要声明**:插件为第三方内容,请自行分辨是否为恶意插件。若因恶意插件导致设备受损或信息泄露,FloraBotTeam概不负责。 109 | 110 | 首次启动会创建一个名为`FloraBot`的文件夹,进入该文件夹,再进入`Plugins`文件夹,将插件文件夹放入其中。若插件为压缩包,需解压后再放入。 111 | 112 | 文件结构示例: 113 | ```File 114 | FloraBot 115 | \-Plugins 116 | |-插件文件夹 117 | | |-Plugin.json 118 | | \-插件.py 119 | \-插件文件夹 120 | |-Plugin.json 121 | \-插件.py 122 | Config.json 123 | FloraBot.py 124 | 启动脚本 125 | ``` 126 | 127 | ## 内置功能 128 | 129 | 与Bot账号私聊或在Bot加入的群聊中发送指令触发: 130 | 131 | - **`/重载插件`**:重新加载插件,若在FloraBot运行中添加/删除/修改了插件文件,请发送该指令重新加载。 132 | - **`/插件列表`**:发送该指令后,Bot会自动发送当前已添加的所有插件的列表,包括插件的状态等。 133 | - **`/启用插件 + [空格] + [插件名]`**:若插件被禁用,可使用该指令启用插件,插件名可通过`/插件列表`指令查询。 134 | - **`/禁用插件 + [空格] + [插件名]`**:若不想要该插件的功能,可使用该指令禁用插件,插件名可通过`/插件列表`指令查询。 135 | - **`/echo + [空格] + [内容]`**:让Bot复读一遍内容,用于调试(Debug),此指令复读方式为回复。 136 | - **`/echo1 + [空格] + [内容]`**:与`/echo`功能相同,只是复读方式不为回复。 137 | - **`/帮助`**:查看FloraBot可使用的指令。 138 | - **`/帮助 + [空格] + [插件名]`**:可查看对应插件的帮助。 139 | 140 | ## 插件开发 141 | 142 | **要求**: 143 | - 会Python的基础知识。 144 | - 会使用Python解析Json。 145 | - 如果会CQ码会更好。 146 | 147 | **示例模板**:可在仓库中的`PluginTemplate`文件夹内找到模板。 148 | 149 | **`Plugin.json`(必要)**: 150 | ```Json 151 | { 152 | "PluginName": "插件名", 153 | "PluginVersion": "插件版本", 154 | "DependentLibraries": null, 155 | "IsLibraries": false, 156 | "PluginIcon": null, 157 | "PluginAuthor": "插件作者名", 158 | "MainPyName": "插件主.py文件名,不要带上.py后缀名", 159 | "PluginDescription": "插件描述", 160 | "EnablePlugin": true, 161 | 162 | "Help": [ 163 | { 164 | "Class": "分类", 165 | "Commands": [ 166 | { 167 | "Command": "指令", 168 | "Content": "指令介绍" 169 | } 170 | ] 171 | } 172 | ] 173 | } 174 | ``` 175 | 176 | **`Plugin.json`文件键值对照表**: 177 | - **`PluginName`**:插件名。 178 | - **`PluginVersion`**:插件版本。 179 | - **`DependentLibraries`**:依赖的第三方库的名称,若`AutoInstallLibraries`值为`true`,则会尝试自动安装这些库,格式为`["库名", "库名", ...]`。 180 | - **`IsLibraries`**:是否为依赖库插件。 181 | - **`PluginIcon`**:插件的图标,格式为`xxx.png`(要带上后缀,主流文件格式即可,可以在文件夹下,但相对路径是从`Plugin.json`文件所在的目录开始的)。 182 | - **`PluginAuthor`**:插件作者名。 183 | - **`MainPyName`**:插件主.py文件名,不要带上.py后缀名,另外py文件里不要赋值`__name__`变量! 184 | - **`PluginDescription`**:插件描述。 185 | - **`EnablePlugin`**:是否启用插件,这是一个标志,用于启用和禁用插件,默认值为`true`即可。 186 | - **`Help`**:帮助菜单内容列表。 187 | - **`Class`**:指令分类。 188 | - **`Commands`**:分类的指令列表。 189 | - **`Command`**:指令。 190 | - **`Content`**:指令介绍。 191 | 192 | **Python文件(必要)示例**: 193 | 194 | ```Python 195 | # 前言,这里用不到的函数可以不定义,可以直接删去,包括API也可以删去不定义,不会报错的 196 | 197 | flora_api = {} # 顾名思义,FloraBot的API,载入(若插件已设为禁用则不载入)后会赋值上 198 | 199 | 200 | def occupying_function(*values): # 该函数仅用于占位,并没有任何意义 201 | pass 202 | 203 | 204 | send_msg = occupying_function 205 | 206 | 207 | def init(): # 插件初始化函数,在载入(若插件已设为禁用则不载入)或启用插件时会调用一次,API可能没有那么快更新,可等待,无传入参数 208 | global send_msg 209 | print(flora_api) 210 | send_msg = flora_api.get("SendMsg") 211 | print("FloraBot插件模板 加载成功") 212 | 213 | 214 | def api_update_event(): # 在API更新时会调用一次(若插件已设为禁用则不调用),可及时获得最新的API内容,无传入参数 215 | print(flora_api) 216 | 217 | 218 | def event(data: dict): # 事件函数,FloraBot每收到一个事件都会调用这个函数(若插件已设为禁用则不调用),传入原消息JSON参数 219 | print(data) 220 | send_type = data.get("SendType") 221 | send_address = data.get("SendAddress") 222 | ws_client = send_address.get("WebSocketClient") 223 | ws_server = send_address.get("WebSocketServer") 224 | send_host = send_address.get("SendHost") 225 | send_port = send_address.get("SendPort") 226 | uid = data.get("user_id") # 事件对象QQ号 227 | gid = data.get("group_id") # 事件对象群号 228 | mid = data.get("message_id") # 消息ID 229 | msg = data.get("raw_message") # 消息内容 230 | if msg is not None: 231 | msg = msg.replace("[", "[").replace("]", "]").replace("&", "&").replace(",", ",") # 消息需要将URL编码替换到正确内容 232 | print(send_type, uid, gid, mid, msg, ws_client, ws_server, send_host, send_port) 233 | ``` 234 | 235 | **注意事项**: 236 | - 这些函数以及`flora_api`变量都不是必要的,少了也不会报错。 237 | - `init`函数里获取API内容的话,`PluginsDict`和`PluginsInfoDict`还不是正确的,推荐放到`api_update_event`函数中处理。 238 | 239 | ## 插件API 240 | 241 | 这些在插件中都可以使用`flora_api.get()`获取到: 242 | 243 | - **`FloraPath`**:FloraBot.py文件所在的绝对路径,不是这个文件的路径,而是所在目录。 244 | - **`FloraHost`**:Bot监听的IP地址。 245 | - **`FloraPort`**:Bot监听的端口号。 246 | - **`FrameworkAddress`**:QQ框架的Http协议监听地址。 247 | - **`BotID`**:登录的Bot账号的ID。 248 | - **`Administrator`**:管理员/所有者/主人的ID列表。 249 | - **`FloraVersion`**:Bot的版本号。 250 | - **`FloraServer`**:Bot的Flask实例。 251 | - **`UpdateFloraApi`**:更新`flora_api`的函数,调用了会同时调用插件中的`api_update_event`函数,无参数。 252 | - **`LoadPlugins`**:加载/重载插件函数,会调用`UpdateFloraApi`函数,无参数。 253 | - **`BroadcastEvent`**:广播消息函数,向所有插件包括内置功能广播基于OneBot协议的数据,参数如下方注释解释: 254 | ```Python 255 | def broadcast_event(data: dict, send_type: str, ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): 256 | # 广播消息函数,data: 基于OneBot协议的数据 257 | # send_type: 发送类型,告诉插件是用HTTP还是WebSocket发送消息 258 | # ws_client: WebSocket连接实例,ws_server: WebSocket服务端实例(若发送类型为WebSocket这两个参数必填) 259 | # send_host: HTTP协议发送地址,send_port: HTTP协议发送端口(若填这两个参数则使用自定义地址发送) 260 | ``` 261 | **调用示例**: 262 | - HTTP: 263 | ```Python 264 | broadcast_event(data, "HTTP") 265 | ``` 266 | - WebSocket: 267 | ```Python 268 | broadcast_event(data, "WebSocket", ws_client, ws_server) 269 | ``` 270 | - **`SendMsg`**:发送信息函数,也可以发送事件,只要你会CQ码,参数如下方注释解释: 271 | ```Python 272 | def send_msg(msg: str, uid: str | int, gid: str | int | None, mid: str | int | None = None, ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): 273 | # 发送消息函数,send_type: 发送类型,决定是用HTTP还是WebSocket发送消息 274 | # msg: 正文,uid: ID,gid: 群号,mid: 消息编号 275 | # ws_client: WebSocket连接实例,ws_server: WebSocket服务端实例(若发送类型为WebSocket这两个参数必填) 276 | # send_host: HTTP协议发送地址,send_port: HTTP协议发送端口(若填这两个参数则使用自定义地址发送) 277 | ``` 278 | **关于`SendMsg`的补充**:默认传入参数如下即可: 279 | ```Python 280 | send_msg(send_type, "正文", uid, gid, mid, ws_client, ws_server, send_host, send_port) 281 | ``` 282 | 默认为回复信息,如果不需要回复将参数`mid`改成`None`即可。调用函数后会返回相应的信息(dict类型),有需求可获取其中的数据(如获取该消息的mid,可用于撤回该消息)。 283 | - **`CallApi`**:向框架调用API,会返回dict类型的数据,参数与`SendMsg`差不多,函数如下: 284 | ```Python 285 | def call_api(send_type: str, api: str, params: dict, ws_client=None, ws_server=None, send_host: str = "", send_port: int | str = ""): 286 | # 发送消息函数,send_type: 发送类型,决定是用HTTP还是WebSocket发送消息 287 | # api: 接口/终结点去掉"/"(str类型),data: 数据(dict类型) 288 | # ws_client: WebSocket连接实例,ws_server: WebSocket服务端实例(若发送类型为WebSocket这两个参数必填) 289 | # send_host: HTTP协议发送地址,send_port: HTTP协议发送端口(若填这两个参数则使用自定义地址发送) 290 | ``` 291 | **API(终结点)以及该API的参数查阅OneBot文档调用**,调用函数后会返回相应的信息(dict类型),有需求可获取其中的数据。以下是调用示例: 292 | ```Python 293 | print(call_api(send_type, "API", {}, ws_client, ws_server, send_host, send_port)) 294 | ``` 295 | - **`HelpInfoDict`**:所有插件包括FloraBot的帮助字典。 296 | - **`CallApiReturned`**:调用框架API时,如果为WebSocket协议,这里将会记录返回的数据。这个逻辑也相对地引出了新的Bug,当插件以WebSocket协议广播消息时,若其他插件会发送消息或调用API时会等待WebSocket返回调用参数返回的数据,因为是插件广播的,所以会一直永远的等待下去。若插件有这个需求,还请查看源代码进行开发。作者已经尽力了(一般绝对用不上,要用请参考源代码)。 297 | - **`PluginsDict`**:插件对象字典,使用对应的插件名获取,并赋值给变量(或不赋值直接调用),即可将对应的插件当作库来调用。 298 | - **`PluginsInfoDict`**:插件信息字典,使用对应的插件名获取,可获取到对应插件的`Plugin.json`已转换为Python对象的内容。 299 | - **`ThePluginPath`**:插件对于`FloraBot.py`文件所在的目录的相对路径。由于是将插件导入再调用的,所以任何相对路径都是从`FloraBot.py`文件所在的目录开始的。这非常重要,不推荐使用自己手动定义到插件资源的相对路径,而是推荐使用`ThePluginPath` + 插件相对于资源的相对路径(因为可能会出现种种原因导致你手动定义到插件资源的相对路径不能正确使用插件文件夹中的文件)。示例:我有一个叫做Test.json的文件,在插件目录中的文件夹Test中(即Test/Test.json),那么获取`ThePluginPath`的值拼接到路径"/Test/Test.json"的前面即可获得`FloraBot.py`与该文件的相对路径,现在就可以在插件中正确地使用这个文件了(希望不会那么拗口:))。 300 | **如果还是不能理解`ThePluginPath`的话,直接上代码**: 301 | ```Python 302 | import json 303 | 304 | group_white_list = [] 305 | 306 | 307 | def init(): 308 | global group_white_list 309 | with open(f"./{flora_api.get('ThePluginPath')}/Plugin.json", "r", encoding="UTF-8") as plugin_config: 310 | group_white_list = json.loads(plugin_config.read()).get("GroupWhiteList") 311 | ``` 312 | 上述代码使用`ThePluginPath`拼接了当前插件配置文件的路径,并且读取并获取了当中的`GroupWhiteList`键的值。 313 | 另外,如果要发送图片等本地文件需要绝对路径可以这么写: 314 | ```Python 315 | flora_api = {} 316 | 317 | 318 | def occupying_function(*values): # 该函数仅用于占位,并没有任何意义 319 | pass 320 | 321 | 322 | send_msg = occupying_function 323 | 324 | 325 | def init(): 326 | global send_msg 327 | send_msg = flora_api.get("SendMsg") 328 | 329 | 330 | def event(data: dict): # 事件函数,FloraBot每收到一个事件都会调用这个函数(若插件已设为禁用则不调用),传入原消息JSON参数 331 | print(data) 332 | send_type = data.get("SendType") 333 | send_address = data.get("SendAddress") 334 | ws_client = send_address.get("WebSocketClient") 335 | ws_server = send_address.get("WebSocketServer") 336 | send_host = send_address.get("SendHost") 337 | send_port = data.get("SendPort") 338 | uid = data.get("user_id") # 事件对象QQ号 339 | gid = data.get("group_id") # 事件对象群号 340 | mid = data.get("message_id") # 消息ID 341 | msg = data.get("raw_message") # 消息内容 342 | if msg is not None: 343 | msg = msg.replace("[", "[").replace("]", "]").replace("&", "&").replace(",", ",") # 消息需要将URL编码替换到正确内容 344 | if msg == "TestSendImage": 345 | send_msg(send_type, f"[CQ:image,file=file:///{flora_api.get('FloraPath')}/{flora_api.get('ThePluginPath')}/Test.png]", uid, gid, mid, ws_client, ws_server, send_host, send_port) 346 | ``` 347 | 上述代码发送图片时使用了`FloraPath`和`ThePluginPath`拼接了当前插件文件夹下的Test.png的绝对路径,或获取相对路径的文件的绝对路径。 348 | **注意!!!**:拼接路径请使用`/`而不是`\`,因为如果路径中出现了`\`则只能在Windows中使用,而`/`则是全平台,Windows支持使用`/`拼接路径。 349 | 350 | ## 推荐QQ框架 351 | 352 | - **[NapNeko/NapCatQQ](https://github.com/NapNeko/NapCatQQ)** 353 | 354 | ## 关于 355 | 356 | **QQ群(仅供开发者加入):[994825372](http://qm.qq.com/cgi-bin/qm/qr?group_code=994825372)** 357 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple 2 | requests 3 | flask 4 | websocket-server 5 | colorama --------------------------------------------------------------------------------