diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 7dbf230..8f20470 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -34,4 +34,5 @@ api_exe(VoiceMessage) api_exe(NudgeEvent) api_exe(FileMessage) api_exe(GetProfile) -api_exe(GroupFile) \ No newline at end of file +api_exe(GroupFile) +api_exe(GroupAnnouncement) \ No newline at end of file diff --git a/examples/GroupAnnouncement.cpp b/examples/GroupAnnouncement.cpp new file mode 100644 index 0000000..b8a85f4 --- /dev/null +++ b/examples/GroupAnnouncement.cpp @@ -0,0 +1,121 @@ +#include +// 使用静态库必须要在引入 mirai.h 前定义这个宏 +#define MIRAICPP_STATICLIB +// 按需引用头文件 +// 你也可以使用 #include 引用所有头文件(可能导致编译缓慢) +#include +#include +#include +using namespace std; +using namespace Cyan; + +int main(int argc, char* argv[]) +{ + // 源文件使用 UTF-8 编码保存,在 Windows 上需要切换代码页才不会显示乱码 +#if defined(WIN32) || defined(_WIN32) + system("chcp 65001"); +#endif + + MiraiBot bot; + SessionOptions opts = SessionOptions::FromCommandLine(argc, argv); + + // 自动重试地与 mirai-api-http 建立连接 + while (true) + { + try + { + bot.Connect(opts); + break; + } + catch (const std::exception& ex) + { + cout << ex.what() << endl; + } + MiraiBot::SleepSeconds(1); + } + + // 检查一下版本 + try + { + string mah_version = bot.GetMiraiApiHttpVersion(); + string mc_version = bot.GetMiraiCppVersion(); + cout << "mirai-api-http 的版本: " << mah_version + << "; mirai-cpp 的版本: " << mc_version << "; " << endl; + if (mah_version != mc_version) + { + cout << "Warning: 你的 mirai-api-http 插件的版本与 mirai-cpp 的版本不同,可能存在兼容性问题。" << endl; + } + } + catch (const std::exception& ex) + { + cout << ex.what() << endl; + } + + cout << "Bot Working..." << endl; + + // 修改 groupId 以测试发送群公告! + auto groupId = 12345678_gid; + if (groupId != 12345678_gid) + { + try + { + MiraiImage img; + // 设置 img 的 Path、Base64 或 Url 以设置群公告图片(设置 Id 无效) + // img.Base64 = "..."; + auto options = GroupAnnouncement::PublishFlags::Pinned | GroupAnnouncement::PublishFlags::RequireConfirmation; + auto result = bot.PublishGroupAnnouncement(groupId, "测试测试222", options, img); + cout << "已经发送群公告:" << result.Content << endl; + } + catch (const std::exception& ex) + { + cout << ex.what() << endl; + } + } + else + { + cout << "修改 groupId 以测试发送群公告!" << endl; + } + + bot.On([&](LostConnection e) + { + cout << e.ErrorMessage << " (" << e.Code << ")" << endl; + while (true) + { + try + { + cout << "尝试与 mirai-api-http 重新建立连接..." << endl; + bot.Reconnect(); + break; + } + catch (const std::exception& ex) + { + cout << ex.what() << endl; + } + MiraiBot::SleepSeconds(1); + } + cout << "成功与 mirai-api-http 重新建立连接!" << endl; + }); + + bot.On([&](EventParsingError e) + { + try + { + e.Rethrow(); + } + catch (const std::exception& ex) + { + cout << "解析事件时出现错误: " << ex.what() << endl; + } + }); + + string command; + while (cin >> command) + { + if (command == "exit") break; + } + + // 程序结束前必须释放 Session, 否则会导致 mirai-api-http 内存泄漏 + bot.Disconnect(); + + return 0; +} \ No newline at end of file diff --git a/include/mirai/MiraiBot.hpp b/include/mirai/MiraiBot.hpp index e474539..66a2358 100644 --- a/include/mirai/MiraiBot.hpp +++ b/include/mirai/MiraiBot.hpp @@ -8,6 +8,7 @@ #include #include #include +#include // third-party #include "mirai/third-party/nlohmann/json.hpp" // mirai header files @@ -29,517 +30,553 @@ using nlohmann::json; // 前置声明 namespace httplib { - class Client; + class Client; } namespace Cyan { - class EXPORTED MiraiBot - { - public: - MiraiBot(); - ~MiraiBot(); - MiraiBot(const MiraiBot&) = delete; - MiraiBot& operator=(const MiraiBot&) = delete; - - /** - * \brief 获得 mirai-cpp 的版本号 - * \return 用字符串表示的版本号,如:"1.6.5" - */ - inline string GetMiraiCppVersion() const - { - return "2.4.0"; - } - - std::shared_ptr GetHttpClient(); - - /** - * \brief 获得 mirai-api-http 插件的版本 - * \return 用字符串表示的版本号,如:"1.6.5" - */ - string GetMiraiApiHttpVersion(); - - /** - * \brief 获得验证后的 SessionKey - * \return SessionKey - */ - string GetSessionKey() const; - - /** - * @brief 获取 SessionOptions - * @return SessionOptions - */ - const SessionOptions& GetSessionOptions() const; - - /** - * @brief 与 mirai-api-http 建立连接 - * @param opts - */ - void Connect(const SessionOptions& opts); - - /** - * @brief 与 mirai-api-http 重新建立连接 - */ - void Reconnect(); - - /** - * @brief 释放 Session,程序结束前请必须执行一次此函数,否则可能导致 mirai-api-http 内存泄漏. - */ - void Disconnect(); - - /** - * \brief 获得验证后的 QQ 号码 - * \return QQ_t - */ - QQ_t GetBotQQ() const; - - /** - * \brief 发送私聊消息 - * \param target 发送对象(QQ_t) - * \param messageChain 消息链 - * \param msgId 可选, 如果不为空则发送引用消息 - * \return 用于引用或撤回的消息 ID (MessageId) - */ - MessageId_t SendMessage(const QQ_t& target, const MessageChain& messageChain, MessageId_t msgId = 0); - - /** - * \brief 发送群聊消息 - * \param target 发送对象(GID_t) - * \param messageChain 消息链 - * \param msgId 可选, 如果不为空则发送引用消息 - * \return 用于引用或撤回的消息 ID (MessageId) - */ - MessageId_t SendMessage(const GID_t& target, const MessageChain& messageChain, MessageId_t msgId = 0); - - /** - * \brief 发送临时消息 - * \param gid 群组(GID) - * \param qq 群成员(QQ_t) - * \param messageChain 消息链 - * \param msgId 可选, 如果不为空则发送引用消息 - * \return 用于引用或撤回的消息 ID (MessageId) - */ - MessageId_t SendMessage(const GID_t& gid, const QQ_t& qq, const MessageChain& messageChain, MessageId_t msgId = 0); - - /** - * @brief 发送戳一戳 - * @param target 目标QQ,可以是好友或者Bot的QQ - * @param subject_id 戳一戳接收主体,好友QQ - */ - void SendNudge(const QQ_t& target, const QQ_t& subject_id); - - /** - * @brief 发送戳一戳 - * @param target 目标QQ,可以是好友或者Bot的QQ - * @param subject_id 戳一戳接收主体,群号码 - */ - void SendNudge(const QQ_t& target, const GID_t& subject_id); - - /** - * @brief 发送戳一戳 - * @param target 目标QQ,可以是好友或者Bot的QQ - * @param subject_id 戳一戳接收主体 - */ - void SendNudge(const QQ_t& target, const UID_t& subject_id); - - /** - * @brief 设置精华消息 - * @param target 群消息的 MessageId - */ - void SetEssence(MessageId_t target); - - /** - * \brief 上传可以发送给好友的图片 - * \param fileName 文件名 - * \return 好友图片 - */ - FriendImage UploadFriendImage(const string& fileName); - - /** - * \brief 上传可以发送给群组的图片 - * \param fileName 文件名 - * \return 群组图片 - */ - GroupImage UploadGroupImage(const string& fileName); - - /** - * \brief 上传可以发送给临时消息的图片 - * \param fileName 文件名 - * \return 临时消息图片 - */ - TempImage UploadTempImage(const string& fileName); - - /** - * @brief 上传可以发给群组的语音 - * @param filename 文件名(amr文件) - * @return MiraiVoice - */ - MiraiVoice UploadGroupVoice(const string& filename); - - /** - * @brief 上传并发送群文件 - * @param gid 目标群 - * @param filename 文件名 - * @return MiraiFile - */ - MiraiFile UploadFileAndSend(const GID_t& gid, const string& filename); - - /** - * \brief 获得好友列表 - * \return vector - */ - vector GetFriendList(); - - /** - * \brief 获得群组列表 - * \return vector - */ - vector GetGroupList(); - - /** - * \brief 获得群组的群成员列表 - * \param target 群组(GID_t) - * \return vector - */ - vector GetGroupMembers(const GID_t& target); - - /** - * \brief 获得群成员的信息 - * \param gid 群组(GID_t) - * \param memberId 群成员(QQ_t) - * \return GroupMember - */ - GroupMember GetGroupMemberInfo(const GID_t& gid, const QQ_t& memberId); - - /** - * @brief 获取群文件列表 - * @param gid 群组(GID_t) - * @param offset 分页偏移 - * @param size 分页大小 - * @param parentId 父目录ID - * @param withDownloadInfo 获取下载信息(需要较长时间) - * @return 群文件列表 - */ - vector GetGroupFiles(const GID_t& gid, bool withDownloadInfo = false, int offset = 0, int size = 20, const string& parentId = ""); - - /** - * @brief 通过群文件Id获取GroupFile对象 - * @param gid 群组(GID_t) - * @param groupFileId 群文件Id - * @param withDownloadInfo 获取下载信息(需要较长时间) - * @return GroupFile - */ - GroupFile GetGroupFileById(const GID_t& gid, const string& groupFileId, bool withDownloadInfo = false); - - /** - * @brief 创建群文件夹 - * @param target 目标群(GID_t) - * @param dictionaryName 文件夹名称 - * @return 群文件夹(GroupFile) - */ - GroupFile GroupFileMakeDirectory(const GID_t& target, const string& dictionaryName); - - /** - * @brief 重命名群文件 - * @param groupFile 群文件(GroupFile) - * @param newName 新群文件名 - */ - void GroupFileRename(const GroupFile& groupFile, const string& newName); - - /** - * @brief 移动群文件 - * @param groupFile 群文件(GroupFile) - * @param targetId 目标文件夹的ID - */ - void GroupFileMove(const GroupFile& groupFile, const string& targetId); - - /** - * @brief 删除群文件 - * @param groupFile 群文件(GroupFile) - */ - void GroupFileDelete(const GroupFile& groupFile); - - /** - * @brief 获取 Bot 账号的个人简介 - * @return Profile - */ - Profile GetBotProfile(); - - /** - * @brief 获取好友的个人简介 - * @param qq 好友 QQ - * @return Profile - */ - Profile GetFriendProfile(const QQ_t& qq); - - /** - * @brief 获取群成员的个人简介 - * @param gid 群号码 - * @param memberQQ 群成员QQ - * @return Profile - */ - Profile GetGroupMemberProfile(const GID_t& gid, const QQ_t& memberQQ); - - /** - * @brief 获取QQ用户的个人简介 - * @param qq 用户QQ - * @return Profile - */ - Profile GetUserProfile(const QQ_t& qq); - - /** - * \brief 设置群成员的群名片 - * \param gid 群组(GID_t) - * \param memberId 群成员(QQ_t) - * \param name 新的群名片 - * \return 始终为 true 出错会抛出异常 - */ - void SetGroupMemberName(const GID_t& gid, const QQ_t& memberId, const string& name); - - /** - * \brief 设置群成员的群头衔 - * \param gid 群组(GID_t) - * \param memberId 群成员(QQ_t) - * \param title 新的群头衔 - * \return 始终为 true 出错会抛出异常 - */ - void SetGroupMemberSpecialTitle(const GID_t& gid, const QQ_t& memberId, const string& title); - - /** - * \brief 全体禁言 - * \param target 群组(GID_t) - * \return 始终为 true 出错会抛出异常 - */ - void MuteAll(const GID_t& target); - - /** - * \brief 取消全体禁言 - * \param target 群组(GID_t) - * \return 始终为 true 出错会抛出异常 - */ - void UnMuteAll(const GID_t& target); - - /** - * \brief 禁言群成员 - * \param gid 群组(GID_t) - * \param memberId 群成员(QQ_t) - * \param time_seconds 时长(秒) - * \return 始终为 true 出错会抛出异常 - */ - void Mute(const GID_t& gid, const QQ_t& memberId, unsigned int time_seconds); - - /** - * \brief 取消禁言群成员 - * \param gid 群组(GID_t) - * \param memberId 群成员(QQ_t) - * \return 始终为 true 出错会抛出异常 - */ - void UnMute(const GID_t& gid, const QQ_t& memberId); - - /** - * \brief 将群成员踢出群组 - * \param gid 群组(GID_t) - * \param memberId 群成员(QQ_t) - * \param reason_msg 可选, 填写踢人理由, 默认为空 - * \return 始终为 true 出错会抛出异常 - */ - void Kick(const GID_t& gid, const QQ_t& memberId, const string& reason_msg = ""); - - /** - * \brief 撤回一条消息 - * \param mid 消息ID(MessageId) - * \return 始终为 true 出错会抛出异常 - */ - void Recall(MessageId_t mid); - - /** - * \brief 让 Bot 退出一个群 - * \param group 要退出的群(GID_t) - * \return 始终为 true 出错会抛出异常 - */ - void QuitGroup(const GID_t& group); - - /** - * @brief 删除好友 - * @param friendQQ 好友QQ - */ - void DeleteFriend(const QQ_t& friendQQ); - - /** - * \brief 获取群设置 - * \param group 群(GID_t) - * \return 群设置 - */ - GroupConfig GetGroupConfig(const GID_t& group); - - /** - * \brief 设置群设置 - * \param group 群(GID_t) - * \param groupConfig 群设置 - * \return 始终为 true 出错会抛出异常 - */ - void SetGroupConfig(const GID_t& group, const GroupConfig& groupConfig); - - /** - * \brief 根据消息ID(MessageId)获取对应的好友消息 - * \param mid 消息ID(MessageId) - * \return 始终为 true 出错会抛出异常 - */ - FriendMessage GetFriendMessageFromId(MessageId_t mid); - - /** - * \brief 根据消息ID(MessageId)获取对应的群组消息 - * \param mid 消息ID(MessageId) - * \return 始终为 true 出错会抛出异常 - */ - GroupMessage GetGroupMessageFromId(MessageId_t mid); - - /** - * @brief 注册指令 - * @param commandName 指令名称 - * @param alias 指令别名 - * @param description 指令描述 - * @param helpMessage 指令帮助,执行出错时显示 - */ - void RegisterCommand( - const string& commandName, - const vector& alias, - const string& description, - const string& helpMessage = ""); - - /** - * @brief 发送指令 - * @param command 指令,第一个元素为指令名称,后面的元素为指令参数 - */ - void SendCommand(const vector& command); - - /** - * @brief 获取 Session 信息 - * @return 关于 Bot 的信息 - */ - Friend_t GetSessionInfo(); - - /** - * @brief 设置群成员管理员 - * @param group 群号码 - * @param member 成员号码 - * @param assign 赋值(true: 设置为管理员; false: 取消管理员;) - */ - void SetGroupAdmin(const GID_t& group, const QQ_t& member, bool assign); - - /** - * \brief 监听事件 - * \tparam T 事件类型 - * \param ep 事件处理函数 - * \return MiraiBot 引用 - */ - template - MiraiBot& On(const EventCallback& ep) - { - return OnEventReceived(ep); - } - - /** - * \brief 监听事件 - * \tparam T 事件类型 - * \param ep 事件处理函数 - * \return MiraiBot 引用 - */ - template - MiraiBot& OnEventReceived(const EventCallback& ep); - - /** - * \brief 睡眠当前线程 - * \param sec 时长(秒) - */ - void static SleepSeconds(int sec) - { - std::this_thread::sleep_for(std::chrono::seconds(sec)); - } - - /** - * \brief 睡眠当前线程 - * \param ms 时长(毫秒) - */ - void static SleepMilliseconds(int ms) - { - std::this_thread::sleep_for(std::chrono::milliseconds(ms)); - } - - private: - struct pimpl; - pimpl* pmem = nullptr; - EventCallback lostConnectionCallback; - EventCallback eventParsingErrorCallback; - std::unordered_multimap processors; - - template - CallbackInvoker GetCallbackInvoker(const EventCallback& ep) - { - return [=](WeakEvent we) - { - // 这个lambda函数有两个作用 - // 1.创建类型为T的WeakEvent - // 2.将传入的WeakEvent转化为类型T - // 然后给 EventProcessor 使用 - if (we == nullptr) - { - std::shared_ptr e = std::make_shared(); - return std::dynamic_pointer_cast(e); - } - else - { - ep(*(std::dynamic_pointer_cast(we))); - return we; - } - }; - } - - template - void StoreCallbackInvoker(CallbackInvoker); - }; - - template - inline void MiraiBot::StoreCallbackInvoker(CallbackInvoker func) - { - processors.insert({ T::GetMiraiEvent(), func }); - } - - template<> - inline void MiraiBot::StoreCallbackInvoker(CallbackInvoker func) - { - processors.insert({ MiraiEvent::FriendMessage, func }); - processors.insert({ MiraiEvent::GroupMessage, func }); - processors.insert({ MiraiEvent::TempMessage, func }); - } - - template - inline MiraiBot& MiraiBot::OnEventReceived(const EventCallback& ep) - { - auto func = GetCallbackInvoker(ep); - StoreCallbackInvoker(func); - return *this; - } - - /** - * @brief 对 LostConnection 的偏特化 - */ - template<> - inline MiraiBot& MiraiBot::OnEventReceived(const EventCallback& cb) - { - lostConnectionCallback = cb; - return *this; - } - - /** - * @brief 对 EventParsingError 的偏特化 - */ - template<> - inline MiraiBot& MiraiBot::OnEventReceived(const EventCallback& cb) - { - eventParsingErrorCallback = cb; - return *this; - } + class EXPORTED MiraiBot + { + public: + MiraiBot(); + ~MiraiBot(); + MiraiBot(const MiraiBot&) = delete; + MiraiBot& operator=(const MiraiBot&) = delete; + + /** + * \brief 获得 mirai-cpp 的版本号 + * \return 用字符串表示的版本号,如:"1.6.5" + */ + inline string GetMiraiCppVersion() const + { + return "2.4.0"; + } + + std::shared_ptr GetHttpClient(); + + /** + * \brief 获得 mirai-api-http 插件的版本 + * \return 用字符串表示的版本号,如:"1.6.5" + */ + string GetMiraiApiHttpVersion(); + + /** + * \brief 获得验证后的 SessionKey + * \return SessionKey + */ + string GetSessionKey() const; + + /** + * @brief 获取 SessionOptions + * @return SessionOptions + */ + const SessionOptions& GetSessionOptions() const; + + /** + * @brief 与 mirai-api-http 建立连接 + * @param opts + */ + void Connect(const SessionOptions& opts); + + /** + * @brief 与 mirai-api-http 重新建立连接 + */ + void Reconnect(); + + /** + * @brief 释放 Session,程序结束前请必须执行一次此函数,否则可能导致 mirai-api-http 内存泄漏. + */ + void Disconnect(); + + /** + * \brief 获得验证后的 QQ 号码 + * \return QQ_t + */ + QQ_t GetBotQQ() const; + + /** + * \brief 发送私聊消息 + * \param target 发送对象(QQ_t) + * \param messageChain 消息链 + * \param msgId 可选, 如果不为空则发送引用消息 + * \return 用于引用或撤回的消息 ID (MessageId) + */ + MessageId_t SendMessage(const QQ_t& target, const MessageChain& messageChain, MessageId_t msgId = 0); + + /** + * \brief 发送群聊消息 + * \param target 发送对象(GID_t) + * \param messageChain 消息链 + * \param msgId 可选, 如果不为空则发送引用消息 + * \return 用于引用或撤回的消息 ID (MessageId) + */ + MessageId_t SendMessage(const GID_t& target, const MessageChain& messageChain, MessageId_t msgId = 0); + + /** + * \brief 发送临时消息 + * \param gid 群组(GID) + * \param qq 群成员(QQ_t) + * \param messageChain 消息链 + * \param msgId 可选, 如果不为空则发送引用消息 + * \return 用于引用或撤回的消息 ID (MessageId) + */ + MessageId_t SendMessage(const GID_t& gid, const QQ_t& qq, const MessageChain& messageChain, MessageId_t msgId = 0); + + /** + * @brief 发送戳一戳 + * @param target 目标QQ,可以是好友或者Bot的QQ + * @param subject_id 戳一戳接收主体,好友QQ + */ + void SendNudge(const QQ_t& target, const QQ_t& subject_id); + + /** + * @brief 发送戳一戳 + * @param target 目标QQ,可以是好友或者Bot的QQ + * @param subject_id 戳一戳接收主体,群号码 + */ + void SendNudge(const QQ_t& target, const GID_t& subject_id); + + /** + * @brief 发送戳一戳 + * @param target 目标QQ,可以是好友或者Bot的QQ + * @param subject_id 戳一戳接收主体 + */ + void SendNudge(const QQ_t& target, const UID_t& subject_id); + + /** + * @brief 设置精华消息 + * @param target 群消息的 MessageId + */ + void SetEssence(MessageId_t target); + + /** + * \brief 上传可以发送给好友的图片 + * \param fileName 文件名 + * \return 好友图片 + */ + FriendImage UploadFriendImage(const string& fileName); + + /** + * \brief 上传可以发送给群组的图片 + * \param fileName 文件名 + * \return 群组图片 + */ + GroupImage UploadGroupImage(const string& fileName); + + /** + * \brief 上传可以发送给临时消息的图片 + * \param fileName 文件名 + * \return 临时消息图片 + */ + TempImage UploadTempImage(const string& fileName); + + /** + * @brief 上传可以发给群组的语音 + * @param filename 文件名(amr文件) + * @return MiraiVoice + */ + MiraiVoice UploadGroupVoice(const string& filename); + + /** + * @brief 上传并发送群文件 + * @param gid 目标群 + * @param filename 文件名 + * @return MiraiFile + */ + MiraiFile UploadFileAndSend(const GID_t& gid, const string& filename); + + /** + * \brief 获得好友列表 + * \return vector + */ + vector GetFriendList(); + + /** + * \brief 获得群组列表 + * \return vector + */ + vector GetGroupList(); + + /** + * \brief 获得群组的群成员列表 + * \param target 群组(GID_t) + * \return vector + */ + vector GetGroupMembers(const GID_t& target); + + /** + * \brief 获得群成员的信息 + * \param gid 群组(GID_t) + * \param memberId 群成员(QQ_t) + * \return GroupMember + */ + GroupMember GetGroupMemberInfo(const GID_t& gid, const QQ_t& memberId); + + /** + * @brief 获取群文件列表 + * @param gid 群组(GID_t) + * @param offset 分页偏移 + * @param size 分页大小 + * @param parentId 父目录ID + * @param withDownloadInfo 获取下载信息(需要较长时间) + * @return 群文件列表 + */ + vector GetGroupFiles(const GID_t& gid, bool withDownloadInfo = false, int offset = 0, int size = 20, const string& parentId = ""); + + /** + * @brief 通过群文件Id获取GroupFile对象 + * @param gid 群组(GID_t) + * @param groupFileId 群文件Id + * @param withDownloadInfo 获取下载信息(需要较长时间) + * @return GroupFile + */ + GroupFile GetGroupFileById(const GID_t& gid, const string& groupFileId, bool withDownloadInfo = false); + + /** + * @brief 创建群文件夹 + * @param target 目标群(GID_t) + * @param dictionaryName 文件夹名称 + * @return 群文件夹(GroupFile) + */ + GroupFile GroupFileMakeDirectory(const GID_t& target, const string& dictionaryName); + + /** + * @brief 重命名群文件 + * @param groupFile 群文件(GroupFile) + * @param newName 新群文件名 + */ + void GroupFileRename(const GroupFile& groupFile, const string& newName); + + /** + * @brief 移动群文件 + * @param groupFile 群文件(GroupFile) + * @param targetId 目标文件夹的ID + */ + void GroupFileMove(const GroupFile& groupFile, const string& targetId); + + /** + * @brief 删除群文件 + * @param groupFile 群文件(GroupFile) + */ + void GroupFileDelete(const GroupFile& groupFile); + + /** + * @brief 获取 Bot 账号的个人简介 + * @return Profile + */ + Profile GetBotProfile(); + + /** + * @brief 获取好友的个人简介 + * @param qq 好友 QQ + * @return Profile + */ + Profile GetFriendProfile(const QQ_t& qq); + + /** + * @brief 获取群成员的个人简介 + * @param gid 群号码 + * @param memberQQ 群成员QQ + * @return Profile + */ + Profile GetGroupMemberProfile(const GID_t& gid, const QQ_t& memberQQ); + + /** + * @brief 获取QQ用户的个人简介 + * @param qq 用户QQ + * @return Profile + */ + Profile GetUserProfile(const QQ_t& qq); + + /** + * \brief 设置群成员的群名片 + * \param gid 群组(GID_t) + * \param memberId 群成员(QQ_t) + * \param name 新的群名片 + * \return 始终为 true 出错会抛出异常 + */ + void SetGroupMemberName(const GID_t& gid, const QQ_t& memberId, const string& name); + + /** + * \brief 设置群成员的群头衔 + * \param gid 群组(GID_t) + * \param memberId 群成员(QQ_t) + * \param title 新的群头衔 + * \return 始终为 true 出错会抛出异常 + */ + void SetGroupMemberSpecialTitle(const GID_t& gid, const QQ_t& memberId, const string& title); + + /** + * \brief 全体禁言 + * \param target 群组(GID_t) + * \return 始终为 true 出错会抛出异常 + */ + void MuteAll(const GID_t& target); + + /** + * \brief 取消全体禁言 + * \param target 群组(GID_t) + * \return 始终为 true 出错会抛出异常 + */ + void UnMuteAll(const GID_t& target); + + /** + * \brief 禁言群成员 + * \param gid 群组(GID_t) + * \param memberId 群成员(QQ_t) + * \param time_seconds 时长(秒) + * \return 始终为 true 出错会抛出异常 + */ + void Mute(const GID_t& gid, const QQ_t& memberId, unsigned int time_seconds); + + /** + * \brief 取消禁言群成员 + * \param gid 群组(GID_t) + * \param memberId 群成员(QQ_t) + * \return 始终为 true 出错会抛出异常 + */ + void UnMute(const GID_t& gid, const QQ_t& memberId); + + /** + * \brief 将群成员踢出群组 + * \param gid 群组(GID_t) + * \param memberId 群成员(QQ_t) + * \param reason_msg 可选, 填写踢人理由, 默认为空 + * \return 始终为 true 出错会抛出异常 + */ + void Kick(const GID_t& gid, const QQ_t& memberId, const string& reason_msg = ""); + + /** + * \brief 撤回一条消息 + * \param mid 消息ID(MessageId) + * \return 始终为 true 出错会抛出异常 + */ + void Recall(MessageId_t mid); + + /** + * \brief 让 Bot 退出一个群 + * \param group 要退出的群(GID_t) + * \return 始终为 true 出错会抛出异常 + */ + void QuitGroup(const GID_t& group); + + /** + * @brief 删除好友 + * @param friendQQ 好友QQ + */ + void DeleteFriend(const QQ_t& friendQQ); + + /** + * \brief 获取群设置 + * \param group 群(GID_t) + * \return 群设置 + */ + GroupConfig GetGroupConfig(const GID_t& group); + + /** + * \brief 设置群设置 + * \param group 群(GID_t) + * \param groupConfig 群设置 + * \return 始终为 true 出错会抛出异常 + */ + void SetGroupConfig(const GID_t& group, const GroupConfig& groupConfig); + + /** + * @brief 获取群公告 + * @param group 群(GID_t) + * @param offset 分页偏移 + * @param size 分页大小 + * @return std::vector + */ + std::vector GetGroupAnnouncement(const GID_t& group, int offset = 0, int size = 10); + + + /** + * @brief 发布群公告 + * @param group 群(GID_t) + * @param content 内容 + * @param flags 发布设置 + * @param image 群公告图片 + * @return 群公告对象 + */ + GroupAnnouncement PublishGroupAnnouncement(const GID_t& group, + const string& content, + int flags, + const std::optional& image = std::nullopt); + + /** + * @brief 删除群公告 + * @param group 群(GID_t) + * @param announcementId 群公告ID + */ + void DeleteGroupAnnoencement(const GID_t& group, const string& announcementId); + + /** + * @brief 删除群公告 + * @param announcement 群公告对象 + */ + void DeleteGroupAnnoencement(const GroupAnnouncement& announcement); + + /** + * \brief 根据消息ID(MessageId)获取对应的好友消息 + * \param mid 消息ID(MessageId) + * \return 始终为 true 出错会抛出异常 + */ + FriendMessage GetFriendMessageFromId(MessageId_t mid); + + /** + * \brief 根据消息ID(MessageId)获取对应的群组消息 + * \param mid 消息ID(MessageId) + * \return 始终为 true 出错会抛出异常 + */ + GroupMessage GetGroupMessageFromId(MessageId_t mid); + + /** + * @brief 注册指令 + * @param commandName 指令名称 + * @param alias 指令别名 + * @param description 指令描述 + * @param helpMessage 指令帮助,执行出错时显示 + */ + void RegisterCommand( + const string& commandName, + const vector& alias, + const string& description, + const string& helpMessage = ""); + + /** + * @brief 发送指令 + * @param command 指令,第一个元素为指令名称,后面的元素为指令参数 + */ + void SendCommand(const vector& command); + + /** + * @brief 获取 Session 信息 + * @return 关于 Bot 的信息 + */ + Friend_t GetSessionInfo(); + + /** + * @brief 设置群成员管理员 + * @param group 群号码 + * @param member 成员号码 + * @param assign 赋值(true: 设置为管理员; false: 取消管理员;) + */ + void SetGroupAdmin(const GID_t& group, const QQ_t& member, bool assign); + + /** + * \brief 监听事件 + * \tparam T 事件类型 + * \param ep 事件处理函数 + * \return MiraiBot 引用 + */ + template + MiraiBot& On(const EventCallback& ep) + { + return OnEventReceived(ep); + } + + /** + * \brief 监听事件 + * \tparam T 事件类型 + * \param ep 事件处理函数 + * \return MiraiBot 引用 + */ + template + MiraiBot& OnEventReceived(const EventCallback& ep); + + /** + * \brief 睡眠当前线程 + * \param sec 时长(秒) + */ + void static SleepSeconds(int sec) + { + std::this_thread::sleep_for(std::chrono::seconds(sec)); + } + + /** + * \brief 睡眠当前线程 + * \param ms 时长(毫秒) + */ + void static SleepMilliseconds(int ms) + { + std::this_thread::sleep_for(std::chrono::milliseconds(ms)); + } + + private: + struct pimpl; + pimpl* pmem = nullptr; + EventCallback lostConnectionCallback; + EventCallback eventParsingErrorCallback; + std::unordered_multimap processors; + + template + CallbackInvoker GetCallbackInvoker(const EventCallback& ep) + { + return [=](WeakEvent we) + { + // 这个lambda函数有两个作用 + // 1.创建类型为T的WeakEvent + // 2.将传入的WeakEvent转化为类型T + // 然后给 EventProcessor 使用 + if (we == nullptr) + { + std::shared_ptr e = std::make_shared(); + return std::dynamic_pointer_cast(e); + } + else + { + ep(*(std::dynamic_pointer_cast(we))); + return we; + } + }; + } + + template + void StoreCallbackInvoker(CallbackInvoker); + }; + + template + inline void MiraiBot::StoreCallbackInvoker(CallbackInvoker func) + { + processors.insert({ T::GetMiraiEvent(), func }); + } + + template<> + inline void MiraiBot::StoreCallbackInvoker(CallbackInvoker func) + { + processors.insert({ MiraiEvent::FriendMessage, func }); + processors.insert({ MiraiEvent::GroupMessage, func }); + processors.insert({ MiraiEvent::TempMessage, func }); + } + + template + inline MiraiBot& MiraiBot::OnEventReceived(const EventCallback& ep) + { + auto func = GetCallbackInvoker(ep); + StoreCallbackInvoker(func); + return *this; + } + + /** + * @brief 对 LostConnection 的偏特化 + */ + template<> + inline MiraiBot& MiraiBot::OnEventReceived(const EventCallback& cb) + { + lostConnectionCallback = cb; + return *this; + } + + /** + * @brief 对 EventParsingError 的偏特化 + */ + template<> + inline MiraiBot& MiraiBot::OnEventReceived(const EventCallback& cb) + { + eventParsingErrorCallback = cb; + return *this; + } } // namespace Cyan diff --git a/include/mirai/defs/GroupAnnouncement.hpp b/include/mirai/defs/GroupAnnouncement.hpp new file mode 100644 index 0000000..c12e7e1 --- /dev/null +++ b/include/mirai/defs/GroupAnnouncement.hpp @@ -0,0 +1,114 @@ +#pragma once +#ifndef mirai_cpp_defs_Announcement_hpp_H_ +#define mirai_cpp_defs_Announcement_hpp_H_ + +#include "mirai/third-party/nlohmann/json.hpp" +#include "mirai/defs/QQType.hpp" +#include "mirai/defs/Group.hpp" +#include "ISerializable.hpp" + +namespace Cyan +{ + /** + * @brief 群通告 + */ + class GroupAnnouncement : public ISerializable + { + public: + /** + * @brief 群组 + */ + Group_t Group; + + /** + * @brief 群公告内容 + */ + string Content; + + /** + * @brief 发送者 QQ + */ + QQ_t SenderQQ; + + /** + * @brief 群公告唯一Id + */ + string AnnouncementId; + + /** + * @brief 是否所有群成员已确认 + */ + bool AllConfirmed = false; + + /** + * @brief 已经确认的人数 + */ + int ConfirmedMembersCount = 0; + + /** + * @brief 发布时间 + */ + int64_t PublicationTime = 0; + + virtual bool Set(const json& j) override + { + Group.Set(j["group"]); + Content = j["content"].get(); + SenderQQ = (QQ_t)j["senderId"].get(); + AnnouncementId = j["fid"].get(); + AllConfirmed = j["allConfirmed"].get(); + ConfirmedMembersCount = j["confirmedMembersCount"].get(); + PublicationTime = j["publicationTime"].get(); + return true; + } + virtual json ToJson() const override + { + return + { + { "group", Group.ToJson() }, + { "content", Content }, + { "senderId", SenderQQ.ToInt64() }, + { "fid", AnnouncementId }, + { "allConfirmed", AllConfirmed }, + { "confirmedMembersCount", ConfirmedMembersCount }, + { "publicationTime", PublicationTime } + }; + } + + /** + * @brief 发布群公告的设置 + */ + enum PublishFlags + { + /** + * @brief 是否推送给新成员 + */ + SendToNewMember = 0x01, + + /** + * @brief 是否置顶 + */ + Pinned = 0x02, + + /** + * @brief 是否显示群成员修改群名片的引导 + */ + ShowEditCard = 0x04, + + /** + * @brief 是否自动弹出 + */ + ShowPopup = 0x08, + + /** + * @brief 是否需要群成员确认 + */ + RequireConfirmation = 0x10 + }; + + + }; + +} + +#endif \ No newline at end of file diff --git a/include/mirai/defs/GroupFile.hpp b/include/mirai/defs/GroupFile.hpp index ea56b66..1b435a2 100644 --- a/include/mirai/defs/GroupFile.hpp +++ b/include/mirai/defs/GroupFile.hpp @@ -57,7 +57,7 @@ namespace Cyan virtual bool Set(const json& j) override { - Size = j["size"].get(); + Size = j["size"].get(); Name = j["name"].get(); Id = j["id"].get(); Path = j["path"].get(); diff --git a/include/mirai/defs/QQType.hpp b/include/mirai/defs/QQType.hpp index 00e7b02..485d19a 100644 --- a/include/mirai/defs/QQType.hpp +++ b/include/mirai/defs/QQType.hpp @@ -124,6 +124,7 @@ namespace Cyan string Id; string Url; string Path; + string Base64; } FriendImage, GroupImage, TempImage; /** @@ -134,6 +135,7 @@ namespace Cyan string Id; string Url; string Path; + string Base64; /** * @brief 语音长度 */ diff --git a/include/mirai/defs/defs.hpp b/include/mirai/defs/defs.hpp index 20d6205..936d68c 100644 --- a/include/mirai/defs/defs.hpp +++ b/include/mirai/defs/defs.hpp @@ -12,5 +12,6 @@ #include "MessageChain.hpp" #include "Profile.hpp" #include "FileDownloadInfo.hpp" +#include "GroupAnnouncement.hpp" #endif // !mirai_cpp_defs_defs_hpp_H_ diff --git a/include/mirai/events/OtherClientMessage.hpp b/include/mirai/events/OtherClientMessage.hpp index 337a291..2ca3a12 100644 --- a/include/mirai/events/OtherClientMessage.hpp +++ b/include/mirai/events/OtherClientMessage.hpp @@ -19,7 +19,7 @@ namespace Cyan class Sender : public ISerializable { public: - int Id; + int Id = -1; string Platform; virtual bool Set(const json& j) override diff --git a/include/mirai/messages/ImageMessage.hpp b/include/mirai/messages/ImageMessage.hpp index f88b726..01b574b 100644 --- a/include/mirai/messages/ImageMessage.hpp +++ b/include/mirai/messages/ImageMessage.hpp @@ -10,7 +10,7 @@ namespace Cyan { public: ImageMessage() {} - ImageMessage(const MiraiImage& m) : imageId_(m.Id), url_(m.Url), path_(m.Path) {} + ImageMessage(const MiraiImage& m) : imageId_(m.Id), url_(m.Url), path_(m.Path), base64_(m.Base64) {} ImageMessage(const ImageMessage& m) : imageId_(m.imageId_), url_(m.url_), path_(m.path_), base64_(m.base64_) {} ImageMessage(ImageMessage&& m) noexcept { @@ -108,6 +108,7 @@ namespace Cyan tmp.Id = imageId_; tmp.Url = url_; tmp.Path = path_; + tmp.Base64 = base64_; return tmp; } diff --git a/include/mirai/messages/VoiceMessage.hpp b/include/mirai/messages/VoiceMessage.hpp index b50eaa4..f35f49b 100644 --- a/include/mirai/messages/VoiceMessage.hpp +++ b/include/mirai/messages/VoiceMessage.hpp @@ -11,8 +11,18 @@ namespace Cyan { public: VoiceMessage() {} - VoiceMessage(const MiraiVoice& m) : voiceId_(m.Id), url_(m.Url), path_(m.Path), length_(0) {} - VoiceMessage(const VoiceMessage& m) : voiceId_(m.voiceId_), url_(m.voiceId_), path_(m.path_), length_(m.length_) {} + VoiceMessage(const MiraiVoice& m) : + voiceId_(m.Id), + url_(m.Url), + path_(m.Path), + base64_(m.Base64), + length_(0) {} + VoiceMessage(const VoiceMessage& m) : + voiceId_(m.voiceId_), + url_(m.voiceId_), + path_(m.path_), + base64_(m.base64_), + length_(m.length_) {} VoiceMessage(VoiceMessage&& m) noexcept { std::swap(this->voiceId_, m.voiceId_); @@ -87,6 +97,10 @@ namespace Cyan json result = { { "type", type_ }, + { "voiceId", voiceId_.empty() ? nullptr : voiceId_ }, + { "url", url_.empty() ? nullptr : url_ }, + { "path", path_.empty() ? nullptr : path_ }, + { "base64", base64_.empty() ? nullptr : base64_ }, { "length", length_ } }; @@ -116,6 +130,7 @@ namespace Cyan tmp.Url = url_; tmp.Path = path_; tmp.Length = length_; + tmp.Base64 = base64_; return tmp; } diff --git a/src/MiraiBot.cpp b/src/MiraiBot.cpp index be0dd59..7760dab 100644 --- a/src/MiraiBot.cpp +++ b/src/MiraiBot.cpp @@ -31,979 +31,1056 @@ using namespace cyanray; namespace { - const string CONTENT_TYPE = "application/json;charset=UTF-8"; + const string CONTENT_TYPE = "application/json;charset=UTF-8"; - // 因为 httplib 使用 string 来保存文件内容,这里返回值也跟着适配 - string ReadFile(const string& filename) - { + // 因为 httplib 使用 string 来保存文件内容,这里返回值也跟着适配 + string ReadFile(const string& filename) + { #ifdef _MSC_VER - // 将 utf-8 转换为 utf-16 - // 这样才能打开 windows 上带有 unicode 字符的文件 - std::wstring_convert> converter; - std::ifstream ifs(converter.from_bytes(filename), std::ifstream::binary); + // 将 utf-8 转换为 utf-16 + // 这样才能打开 windows 上带有 unicode 字符的文件 + std::wstring_convert> converter; + std::ifstream ifs(converter.from_bytes(filename), std::ifstream::binary); #else - std::ifstream ifs(filename, std::ifstream::binary); + std::ifstream ifs(filename, std::ifstream::binary); #endif // _MSC_VER - if (!ifs.is_open()) throw std::runtime_error("打开文件失败,请确认路径是否正确并检查文件是否存在"); - std::filebuf* pbuf = ifs.rdbuf(); - std::size_t size = pbuf->pubseekoff(0, ifs.end, ifs.in); - pbuf->pubseekpos(0, ifs.in); - string result(size, '\0'); - pbuf->sgetn(&result[0], size); - ifs.close(); - return result; - } - - json ParseOrThrowException(const httplib::Result& result) - { - using namespace Cyan; - if (!result) throw NetworkException(); - json re_json = json::parse(result->body); - if (re_json.contains("code")) - { - int code = re_json["code"].get(); - if (code != 0) - { - string msg = re_json["msg"].get(); - throw MiraiApiHttpException(code, msg); - } - } - return re_json; - } + if (!ifs.is_open()) throw std::runtime_error("打开文件失败,请确认路径是否正确并检查文件是否存在"); + std::filebuf* pbuf = ifs.rdbuf(); + std::size_t size = pbuf->pubseekoff(0, ifs.end, ifs.in); + pbuf->pubseekpos(0, ifs.in); + string result(size, '\0'); + pbuf->sgetn(&result[0], size); + ifs.close(); + return result; + } + + json ParseOrThrowException(const httplib::Result& result) + { + using namespace Cyan; + if (!result) throw NetworkException(); + json re_json = json::parse(result->body); + if (re_json.contains("code")) + { + int code = re_json["code"].get(); + if (code != 0) + { + string msg = re_json["msg"].get(); + throw MiraiApiHttpException(code, msg); + } + } + return re_json; + } } namespace Cyan { - struct MiraiBot::pimpl - { - QQ_t botQQ; - string sessionKey; - std::shared_ptr sessionOptions; - std::shared_ptr httpClient; - std::unique_ptr threadPool; - WebSocketClient eventClient; - - /** - * \brief 验证 VerifyKey - * \param verifyKey VerifyKey - * \return 如果成功, 返回 SessionKey. - */ - string Verify(const string& verifyKey); - - /** - * @brief 绑定 Bot QQ 到 Session - * @param sessionKey 通过 Verify 得到的 SessionKey. - * @param qq Bot QQ - * @return true - */ - void SessionBind(const string& sessionKey, const QQ_t& qq); - - /** - * @brief 释放 Session. - * @param sessionKey 通过 Verify 得到的 SessionKey. - * @param qq Bot QQ - * @return true - */ - void SessionRelease(const string& sessionKey, const QQ_t& qq); - - /** - * @brief 发送“戳一戳” - * @param target 聊天目标 (私聊为聊天对象QQ号,群聊为群号码) - * @param subject_id 受体目标 (私聊为自己或对象的QQ号码,群聊为群成员QQ号码) - * @param kind 类型 (Friend | Group) - */ - void SendNudge(int64_t target, int64_t subject_id, const string& kind); - - /** - * @brief 上传图像 - * @param fileName 文件路径 - * @param type 类型 (Friend | Group | Temp) - * @return - */ - MiraiImage UploadImage(const string& fileName, const string& type); - - /** - * @brief 上传声音 - * @param filename 文件路径 - * @param type 类型 (Friend | Group) - * @return - */ - MiraiVoice UploadVoice(const string& filename, const string& type); - - /** - * @brief 上传文件并发送 - * @param target 发送目标(好友或群组) - * @param filename 文件路径 - * @param type 类型 (Friend | Group) - * @return - */ - MiraiFile UploadFileAndSend(int64_t target, const string& filename, const string& type); - - /** - * @brief 更新群成员信息(群名片和群头衔) - * @param gid 群号码 - * @param memberId 群成员QQ - * @param name 群名片 - * @param specialTitle 群头衔 - * @return - */ - void SetGroupMemberInfo(GID_t gid, QQ_t memberId, const string& name, const string& specialTitle) - { - json data = - { - { "sessionKey", sessionKey }, - { "target", int64_t(gid)}, - { "memberId", int64_t(memberId)}, - { "info", - { - { "name", name }, - { "specialTitle", specialTitle } - } - } - }; - - auto res = httpClient->Post("/memberInfo", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - }; - - MiraiBot::MiraiBot() : pmem(nullptr) - { - } - - MiraiBot::~MiraiBot() - { - delete pmem; - }; - - void MiraiBot::Connect(const SessionOptions& opts) - { - delete pmem; - pmem = new pimpl(); - pmem->sessionOptions = std::make_shared(opts); - pmem->threadPool = std::make_unique(opts.ThreadPoolSize.Get()); - pmem->httpClient = std::make_shared(opts.HttpHostname.Get(), opts.HttpPort.Get()); - - string& sessionKey = pmem->sessionKey; - if (opts.EnableVerify.Get()) - { - sessionKey = pmem->Verify(opts.VerifyKey.Get()); - } - if (!opts.SingleMode.Get()) - { - pmem->botQQ = opts.BotQQ.Get(); - pmem->SessionBind(sessionKey, opts.BotQQ.Get()); - } - else - { - auto info = GetSessionInfo(); - pmem->sessionOptions->BotQQ = info.QQ; - pmem->botQQ = info.QQ; - } - - pmem->eventClient.Connect( - opts.WebSocketHostname.Get(), - opts.WebSocketPort.Get(), - "/all?verifyKey="s.append(opts.VerifyKey.Get()).append("&sessionKey=").append(sessionKey)); - - pmem->eventClient.OnTextReceived([&](WebSocketClient& client, string text) - { - try - { - json event_json = json::parse(text)["data"]; - if (!event_json.contains("type")) return; - string event_name = event_json["type"].get(); - MiraiEvent mirai_event = MiraiEventStr(event_name); - auto range = processors.equal_range(mirai_event); - for (auto& it = range.first; it != range.second; ++it) - { - auto& executor = it->second; - // 给 executor 传入 nullptr 可以创建一个 WeakEvent - WeakEvent pevent = executor(nullptr); - pevent->SetMiraiBot(this); - pevent->Set(event_json); - - pmem->threadPool->enqueue([=]() - { - executor(pevent); - }); - } - - } - catch (...) - { - if (eventParsingErrorCallback) - { - eventParsingErrorCallback(EventParsingError(std::current_exception())); - } - } - }); - - pmem->eventClient.OnLostConnection([&](WebSocketClient& client, int code) - { - if (lostConnectionCallback) - { - LostConnection result; - result.Code = code; - result.ErrorMessage = "与 mirai-api-http 失去连接."; - lostConnectionCallback(result); - } - }); - } - - void MiraiBot::Reconnect() - { - pmem->eventClient.Shutdown(); - auto& opts = *pmem->sessionOptions; - auto& sessionKey = pmem->sessionKey; - if (opts.EnableVerify.Get()) - { - sessionKey = pmem->Verify(opts.VerifyKey.Get()); - } - if (!opts.SingleMode.Get()) - { - pmem->SessionBind(sessionKey, opts.BotQQ.Get()); - } - - pmem->eventClient.Connect( - opts.WebSocketHostname.Get(), - opts.WebSocketPort.Get(), - "/all?verifyKey="s.append(opts.VerifyKey.Get()).append("&sessionKey=").append(sessionKey)); - - } - - void MiraiBot::Disconnect() - { - pmem->eventClient.Shutdown(); - pmem->SessionRelease(pmem->sessionKey, pmem->botQQ); - } - - string MiraiBot::GetSessionKey() const - { - return pmem->sessionKey; - } - - const SessionOptions& MiraiBot::GetSessionOptions() const - { - return *pmem->sessionOptions; - } - - QQ_t MiraiBot::GetBotQQ() const - { - return pmem->botQQ; - } - - std::shared_ptr MiraiBot::GetHttpClient() - { - return pmem->httpClient; - } - - string MiraiBot::GetMiraiApiHttpVersion() - { - auto res = pmem->httpClient->Get("/about"); - if (!res) - throw NetworkException(); - if (res->status != 200) - throw MiraiApiHttpException(-1, res->body); - json re_json = json::parse(res->body); - int code = re_json["code"].get(); - if (code == 0) - { - string version = re_json["data"]["version"].get(); - return version; - } - string msg = re_json["msg"].get(); - throw runtime_error(msg); - } - - string MiraiBot::pimpl::Verify(const string& verifyKey) - { - json data = - { - { "verifyKey", verifyKey } - }; - auto res = httpClient->Post("/verify", data.dump(), CONTENT_TYPE.c_str()); - json re_json = ParseOrThrowException(res); - return re_json["session"].get(); - } - - void MiraiBot::pimpl::SessionBind(const string& sessionKey, const QQ_t& qq) - { - json data = - { - { "sessionKey", sessionKey }, - { "qq", int64_t(qq)} - }; - - auto res = httpClient->Post("/bind", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::pimpl::SessionRelease(const string& sessionKey, const QQ_t& qq) - { - json data = - { - { "sessionKey", sessionKey }, - { "qq", int64_t(qq)} - }; - - auto res = httpClient->Post("/release", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - MessageId_t MiraiBot::SendMessage(const QQ_t& target, const MessageChain& messageChain, MessageId_t msgId) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target",int64_t(target) } - }; - data["messageChain"] = messageChain.ToJson(); - if (msgId != 0) data["quote"] = msgId; - - auto res = pmem->httpClient->Post("/sendFriendMessage", data.dump(), CONTENT_TYPE.c_str()); - json re_json = ParseOrThrowException(res); - MessageId_t msg_id = re_json["messageId"].get(); - return msg_id; - } - - - MessageId_t MiraiBot::SendMessage(const GID_t& target, const MessageChain& messageChain, MessageId_t msgId) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target",int64_t(target) } - }; - data["messageChain"] = messageChain.ToJson(); - if (msgId != 0) data["quote"] = msgId; - - auto res = pmem->httpClient->Post("/sendGroupMessage", data.dump(), CONTENT_TYPE.c_str()); - json re_json = ParseOrThrowException(res); - MessageId_t msg_id = re_json["messageId"].get(); - return msg_id; - } - - MessageId_t MiraiBot::SendMessage(const GID_t& gid, const QQ_t& qq, const MessageChain& messageChain, MessageId_t msgId) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "group",int64_t(gid) }, - { "qq",int64_t(qq) } - }; - data["messageChain"] = messageChain.ToJson(); - if (msgId != 0) data["quote"] = msgId; - - auto res = pmem->httpClient->Post("/sendTempMessage", data.dump(), CONTENT_TYPE.c_str()); - json re_json = ParseOrThrowException(res); - MessageId_t msg_id = re_json["messageId"].get(); - return msg_id; - } - - void MiraiBot::pimpl::SendNudge(int64_t target, int64_t subject_id, const string& kind) - { - json data = - { - { "sessionKey", sessionKey }, - { "target", target }, - { "subject", subject_id }, - { "kind" , kind } - }; - - auto res = httpClient->Post("/sendNudge", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::SendNudge(const QQ_t& target, const QQ_t& subject_id) - { - pmem->SendNudge(target.ToInt64(), subject_id.ToInt64(), "Friend"); - } - - void MiraiBot::SendNudge(const QQ_t& target, const GID_t& subject_id) - { - pmem->SendNudge(target.ToInt64(), subject_id.ToInt64(), "Group"); - } - - void MiraiBot::SendNudge(const QQ_t& target, const UID_t& subject_id) - { - if (typeid(subject_id) == typeid(GID_t)) - { - SendNudge(target, (const GID_t&)(subject_id)); - } - else - { - SendNudge(target, (const QQ_t&)(subject_id)); - } - } - - void MiraiBot::SetEssence(MessageId_t target) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", target } - }; - - auto res = pmem->httpClient->Post("/setEssence", data.dump(), CONTENT_TYPE.c_str()); - json re_json = ParseOrThrowException(res); - } - - MiraiImage MiraiBot::pimpl::UploadImage(const string& filename, const string& type) - { - string base_filename = filename.substr(filename.find_last_of("/\\") + 1); - string img_data = ReadFile(filename); - httplib::MultipartFormDataItems items = - { - { "sessionKey", sessionKey, "", "" }, - { "type", type, "", "" }, - { "img", img_data, base_filename, "image/png" } - }; - - auto res = httpClient->Post("/uploadImage", items); - json re_json = ParseOrThrowException(res); - MiraiImage img; - img.Id = re_json["imageId"].get(); - img.Url = re_json.value("url", ""); - img.Path = re_json.value("path", ""); - return img; - } - - FriendImage MiraiBot::UploadFriendImage(const string& filename) - { - return pmem->UploadImage(filename, "friend"); - } - - GroupImage MiraiBot::UploadGroupImage(const string& filename) - { - return pmem->UploadImage(filename, "group"); - } - - TempImage MiraiBot::UploadTempImage(const string& filename) - { - return pmem->UploadImage(filename, "temp"); - } - - MiraiVoice MiraiBot::pimpl::UploadVoice(const string& filename, const string& type) - { - string base_filename = filename.substr(filename.find_last_of("/\\") + 1); - string voice_data = ReadFile(filename); - httplib::MultipartFormDataItems items = - { - { "sessionKey", sessionKey, "", "" }, - { "type", type, "", "" }, - { "voice", voice_data, base_filename, "application/octet-stream" } - }; - - auto res = httpClient->Post("/uploadVoice", items); - json re_json = ParseOrThrowException(res); - MiraiVoice result; - result.Id = re_json["voiceId"].get(); - if (!re_json["url"].is_null()) - result.Url = re_json["url"].get(); - result.Path = re_json["path"].get(); - return result; - } - - MiraiVoice MiraiBot::UploadGroupVoice(const string& filename) - { - return pmem->UploadVoice(filename, "group"); - } - - MiraiFile MiraiBot::pimpl::UploadFileAndSend(int64_t target, const string& filename, const string& type) - { - string base_filename = filename.substr(filename.find_last_of("/\\") + 1); - string file_data = ReadFile(filename); - httplib::MultipartFormDataItems items = - { - { "sessionKey", sessionKey, "", "" }, - { "type", type, "", "" }, - { "target", to_string(target), "", "" }, - { "path", "/" + base_filename, "", "" }, - { "file", file_data, base_filename, "application/octet-stream" } - }; - - auto res = httpClient->Post("/uploadFileAndSend", items); - json re_json = ParseOrThrowException(res); - MiraiFile result; - result.FileSize = file_data.size(); - result.FileName = base_filename; - result.Id = re_json["id"].get(); - return result; - } - - MiraiFile MiraiBot::UploadFileAndSend(const GID_t& gid, const string& filename) - { - return pmem->UploadFileAndSend(gid.ToInt64(), filename, "Group"); - } - - vector MiraiBot::GetFriendList() - { - auto res = pmem->httpClient->Get(("/friendList?sessionKey=" + pmem->sessionKey).data()); - json re_json = ParseOrThrowException(res); - vector result; - for (const auto& ele : re_json["data"]) - { - Friend_t f; - f.Set(ele); - result.emplace_back(f); - } - return result; - } - - - vector MiraiBot::GetGroupList() - { - auto res = pmem->httpClient->Get(("/groupList?sessionKey=" + pmem->sessionKey).data()); - json re_json = ParseOrThrowException(res); - vector result; - for (const auto& ele : re_json["data"]) - { - Group_t group; - group.Set(ele); - result.emplace_back(group); - } - return result; - } - - - vector MiraiBot::GetGroupMembers(const GID_t& target) - { - stringstream api_url; - api_url - << "/memberList?sessionKey=" - << pmem->sessionKey - << "&target=" - << target; - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - vector result; - for (const auto& ele : re_json["data"]) - { - GroupMember f; - f.Set(ele); - result.push_back(f); - } - return result; - } - - GroupMember MiraiBot::GetGroupMemberInfo(const GID_t& gid, const QQ_t& memberId) - { - stringstream api_url; - api_url - << "/memberInfo?sessionKey=" - << pmem->sessionKey - << "&target=" - << int64_t(gid) - << "&memberId=" - << int64_t(memberId); - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - GroupMember result; - result.Set(re_json); - return result; - } - - Profile MiraiBot::GetBotProfile() - { - stringstream api_url; - api_url - << "/botProfile?sessionKey=" - << pmem->sessionKey; - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - Profile result; - result.Set(re_json); - return result; - } - - Profile MiraiBot::GetFriendProfile(const QQ_t& qq) - { - stringstream api_url; - api_url - << "/friendProfile?sessionKey=" - << pmem->sessionKey - << "&target=" - << int64_t(qq); - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - Profile result; - result.Set(re_json); - return result; - } - - Profile MiraiBot::GetGroupMemberProfile(const GID_t& gid, const QQ_t& memberQQ) - { - stringstream api_url; - api_url - << "/memberProfile?sessionKey=" - << pmem->sessionKey - << "&target=" - << int64_t(gid) - << "&memberId=" - << int64_t(memberQQ); - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - Profile result; - result.Set(re_json); - return result; - } - - Profile MiraiBot::GetUserProfile(const QQ_t& qq) - { - stringstream api_url; - api_url - << "/userProfile?sessionKey=" - << pmem->sessionKey - << "&target=" - << int64_t(qq); - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - Profile result; - result.Set(re_json); - return result; - } - - void MiraiBot::SetGroupMemberName(const GID_t& gid, const QQ_t& memberId, const string& name) - { - auto member_info = this->GetGroupMemberInfo(gid, memberId); - pmem->SetGroupMemberInfo(gid, memberId, name, member_info.SpecialTitle); - } - - void MiraiBot::SetGroupMemberSpecialTitle(const GID_t& gid, const QQ_t& memberId, const string& title) - { - auto member_info = this->GetGroupMemberInfo(gid, memberId); - pmem->SetGroupMemberInfo(gid, memberId, member_info.MemberName, title); - } - - vector MiraiBot::GetGroupFiles(const GID_t& gid, bool withDownloadInfo, int offset, int size, const string& parentId) - { - stringstream api_url; - api_url - << "/file/list?sessionKey=" - << pmem->sessionKey - << "&target=" - << int64_t(gid) - << "&id=" - << parentId - << "&offset=" - << offset - << "&size=" - << size - << "&withDownloadInfo=" - << (withDownloadInfo ? "true" : "false"); - // 取文件列表响应比较慢 - pmem->httpClient->set_read_timeout(60, 0); - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - vector result; - for (const auto& item : re_json["data"]) - { - GroupFile f; - f.Set(item); - result.push_back(f); - } - return result; - } - - GroupFile MiraiBot::GetGroupFileById(const GID_t& gid, const string& groupFileId, bool withDownloadInfo) - { - stringstream api_url; - api_url - << "/file/info?sessionKey=" - << pmem->sessionKey - << "&target=" - << int64_t(gid) - << "&id=" - << groupFileId - << "&withDownloadInfo=" - << (withDownloadInfo ? "true" : "false"); - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - GroupFile result; - result.Set(re_json); - return result; - - } - - GroupFile MiraiBot::GroupFileMakeDirectory(const GID_t& target, const string& dictionaryName) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "group", int64_t(target) }, - { "dictionaryName", dictionaryName } - }; - - auto res = pmem->httpClient->Post("/file/mkdir", data.dump(), CONTENT_TYPE.c_str()); - auto re_json = ParseOrThrowException(res); - GroupFile result; - result.Set(re_json); - return result; - } - - void MiraiBot::GroupFileRename(const GroupFile& groupFile, const string& newName) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(groupFile.Group.GID) }, - { "id", groupFile.Id }, - { "renameTo", newName } - }; - - auto res = pmem->httpClient->Post("/file/rename", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::GroupFileMove(const GroupFile& groupFile, const string& targetId) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(groupFile.Group.GID) }, - { "id", groupFile.Id }, - { "moveTo", targetId } - }; - - auto res = pmem->httpClient->Post("/file/move", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::GroupFileDelete(const GroupFile& groupFile) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(groupFile.Group.GID) }, - { "id", groupFile.Id } - }; - - auto res = pmem->httpClient->Post("/file/delete", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::MuteAll(const GID_t& target) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(target)} - }; - - auto res = pmem->httpClient->Post("/muteAll", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - - void MiraiBot::UnMuteAll(const GID_t& target) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(target)} - }; - - auto res = pmem->httpClient->Post("/unmuteAll", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - - void MiraiBot::Mute(const GID_t& gid, const QQ_t& memberId, unsigned int time_seconds) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(gid)}, - { "memberId", int64_t(memberId)}, - { "time", time_seconds} - }; - - auto res = pmem->httpClient->Post("/mute", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - - void MiraiBot::UnMute(const GID_t& gid, const QQ_t& memberId) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(gid)}, - { "memberId", int64_t(memberId)} - }; - - auto res = pmem->httpClient->Post("/unmute", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - - void MiraiBot::Kick(const GID_t& gid, const QQ_t& memberId, const string& reason_msg) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(gid)}, - { "memberId", int64_t(memberId)}, - { "reason_msg" , reason_msg} - }; - - auto res = pmem->httpClient->Post("/kick", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - - void MiraiBot::Recall(MessageId_t mid) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(mid)} - }; - - auto res = pmem->httpClient->Post("/recall", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::QuitGroup(const GID_t& group) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(group)} - }; - - auto res = pmem->httpClient->Post("/quit", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::DeleteFriend(const QQ_t& friendQQ) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(friendQQ)} - }; - - auto res = pmem->httpClient->Post("/deleteFriend", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - GroupConfig MiraiBot::GetGroupConfig(const GID_t& group) - { - stringstream api_url; - api_url - << "/groupConfig?sessionKey=" - << pmem->sessionKey - << "&target=" - << int64_t(group); - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - GroupConfig group_config; - group_config.Set(re_json); - return group_config; - } - - void MiraiBot::SetGroupConfig(const GID_t& group, const GroupConfig& groupConfig) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(group) } - }; - data["config"] = groupConfig.ToJson(); - - auto res = pmem->httpClient->Post("/groupConfig", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - FriendMessage MiraiBot::GetFriendMessageFromId(MessageId_t mid) - { - stringstream api_url; - api_url - << "/messageFromId?sessionKey=" - << pmem->sessionKey - << "&id=" - << mid; - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - FriendMessage result; - result.Set(re_json["data"]); - return result; - } - - - GroupMessage MiraiBot::GetGroupMessageFromId(MessageId_t mid) - { - stringstream api_url; - api_url - << "/messageFromId?sessionKey=" - << pmem->sessionKey - << "&id=" - << mid; - - auto res = pmem->httpClient->Get(api_url.str().data()); - json re_json = ParseOrThrowException(res); - GroupMessage result; - result.Set(re_json["data"]); - return result; - } - - void MiraiBot::RegisterCommand( - const string& commandName, - const vector& alias, - const string& description, - const string& helpMessage) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "name", commandName }, - { "alias", json(alias) }, - { "description", description }, - { "usage", helpMessage } - }; - auto res = pmem->httpClient->Post("/cmd/register", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - void MiraiBot::SendCommand(const vector& command) - { - MessageChain mc; - for (const auto& val : command) - { - mc.Plain(val); - } - json data = - { - { "sessionKey", pmem->sessionKey }, - { "command", mc.ToJson() } - }; - auto res = pmem->httpClient->Post("/cmd/execute", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } - - Friend_t MiraiBot::GetSessionInfo() - { - auto res = pmem->httpClient->Get("/sessionInfo?sessionKey="s.append(pmem->sessionKey).data()); - json re_json = ParseOrThrowException(res); - Friend_t result; - result.Set(re_json["data"]["qq"]); - return result; - } - - void MiraiBot::SetGroupAdmin(const GID_t& group, const QQ_t& member, bool assign) - { - json data = - { - { "sessionKey", pmem->sessionKey }, - { "target", int64_t(group) }, - { "memberId", int64_t(member) }, - { "assign", assign } - }; - auto res = pmem->httpClient->Post("/memberAdmin", data.dump(), CONTENT_TYPE.c_str()); - ParseOrThrowException(res); - } + struct MiraiBot::pimpl + { + QQ_t botQQ; + string sessionKey; + std::shared_ptr sessionOptions; + std::shared_ptr httpClient; + std::unique_ptr threadPool; + WebSocketClient eventClient; + + /** + * \brief 验证 VerifyKey + * \param verifyKey VerifyKey + * \return 如果成功, 返回 SessionKey. + */ + string Verify(const string& verifyKey); + + /** + * @brief 绑定 Bot QQ 到 Session + * @param sessionKey 通过 Verify 得到的 SessionKey. + * @param qq Bot QQ + * @return true + */ + void SessionBind(const string& sessionKey, const QQ_t& qq); + + /** + * @brief 释放 Session. + * @param sessionKey 通过 Verify 得到的 SessionKey. + * @param qq Bot QQ + * @return true + */ + void SessionRelease(const string& sessionKey, const QQ_t& qq); + + /** + * @brief 发送“戳一戳” + * @param target 聊天目标 (私聊为聊天对象QQ号,群聊为群号码) + * @param subject_id 受体目标 (私聊为自己或对象的QQ号码,群聊为群成员QQ号码) + * @param kind 类型 (Friend | Group) + */ + void SendNudge(int64_t target, int64_t subject_id, const string& kind); + + /** + * @brief 上传图像 + * @param fileName 文件路径 + * @param type 类型 (Friend | Group | Temp) + * @return + */ + MiraiImage UploadImage(const string& fileName, const string& type); + + /** + * @brief 上传声音 + * @param filename 文件路径 + * @param type 类型 (Friend | Group) + * @return + */ + MiraiVoice UploadVoice(const string& filename, const string& type); + + /** + * @brief 上传文件并发送 + * @param target 发送目标(好友或群组) + * @param filename 文件路径 + * @param type 类型 (Friend | Group) + * @return + */ + MiraiFile UploadFileAndSend(int64_t target, const string& filename, const string& type); + + /** + * @brief 更新群成员信息(群名片和群头衔) + * @param gid 群号码 + * @param memberId 群成员QQ + * @param name 群名片 + * @param specialTitle 群头衔 + * @return + */ + void SetGroupMemberInfo(GID_t gid, QQ_t memberId, const string& name, const string& specialTitle) + { + json data = + { + { "sessionKey", sessionKey }, + { "target", int64_t(gid)}, + { "memberId", int64_t(memberId)}, + { "info", + { + { "name", name }, + { "specialTitle", specialTitle } + } + } + }; + + auto res = httpClient->Post("/memberInfo", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + }; + + MiraiBot::MiraiBot() : pmem(nullptr) + { + } + + MiraiBot::~MiraiBot() + { + delete pmem; + }; + + void MiraiBot::Connect(const SessionOptions& opts) + { + delete pmem; + pmem = new pimpl(); + pmem->sessionOptions = std::make_shared(opts); + pmem->threadPool = std::make_unique(opts.ThreadPoolSize.Get()); + pmem->httpClient = std::make_shared(opts.HttpHostname.Get(), opts.HttpPort.Get()); + + string& sessionKey = pmem->sessionKey; + if (opts.EnableVerify.Get()) + { + sessionKey = pmem->Verify(opts.VerifyKey.Get()); + } + if (!opts.SingleMode.Get()) + { + pmem->botQQ = opts.BotQQ.Get(); + pmem->SessionBind(sessionKey, opts.BotQQ.Get()); + } + else + { + auto info = GetSessionInfo(); + pmem->sessionOptions->BotQQ = info.QQ; + pmem->botQQ = info.QQ; + } + + pmem->eventClient.Connect( + opts.WebSocketHostname.Get(), + opts.WebSocketPort.Get(), + "/all?verifyKey="s.append(opts.VerifyKey.Get()).append("&sessionKey=").append(sessionKey)); + + pmem->eventClient.OnTextReceived([&](WebSocketClient& client, string text) + { + try + { + json event_json = json::parse(text)["data"]; + if (!event_json.contains("type")) return; + string event_name = event_json["type"].get(); + MiraiEvent mirai_event = MiraiEventStr(event_name); + auto range = processors.equal_range(mirai_event); + for (auto& it = range.first; it != range.second; ++it) + { + auto& executor = it->second; + // 给 executor 传入 nullptr 可以创建一个 WeakEvent + WeakEvent pevent = executor(nullptr); + pevent->SetMiraiBot(this); + pevent->Set(event_json); + + pmem->threadPool->enqueue([=]() + { + executor(pevent); + }); + } + + } + catch (...) + { + if (eventParsingErrorCallback) + { + eventParsingErrorCallback(EventParsingError(std::current_exception())); + } + } + }); + + pmem->eventClient.OnLostConnection([&](WebSocketClient& client, int code) + { + if (lostConnectionCallback) + { + LostConnection result; + result.Code = code; + result.ErrorMessage = "与 mirai-api-http 失去连接."; + lostConnectionCallback(result); + } + }); + } + + void MiraiBot::Reconnect() + { + pmem->eventClient.Shutdown(); + auto& opts = *pmem->sessionOptions; + auto& sessionKey = pmem->sessionKey; + if (opts.EnableVerify.Get()) + { + sessionKey = pmem->Verify(opts.VerifyKey.Get()); + } + if (!opts.SingleMode.Get()) + { + pmem->SessionBind(sessionKey, opts.BotQQ.Get()); + } + + pmem->eventClient.Connect( + opts.WebSocketHostname.Get(), + opts.WebSocketPort.Get(), + "/all?verifyKey="s.append(opts.VerifyKey.Get()).append("&sessionKey=").append(sessionKey)); + + } + + void MiraiBot::Disconnect() + { + pmem->eventClient.Shutdown(); + pmem->SessionRelease(pmem->sessionKey, pmem->botQQ); + } + + string MiraiBot::GetSessionKey() const + { + return pmem->sessionKey; + } + + const SessionOptions& MiraiBot::GetSessionOptions() const + { + return *pmem->sessionOptions; + } + + QQ_t MiraiBot::GetBotQQ() const + { + return pmem->botQQ; + } + + std::shared_ptr MiraiBot::GetHttpClient() + { + return pmem->httpClient; + } + + string MiraiBot::GetMiraiApiHttpVersion() + { + auto res = pmem->httpClient->Get("/about"); + if (!res) + throw NetworkException(); + if (res->status != 200) + throw MiraiApiHttpException(-1, res->body); + json re_json = json::parse(res->body); + int code = re_json["code"].get(); + if (code == 0) + { + string version = re_json["data"]["version"].get(); + return version; + } + string msg = re_json["msg"].get(); + throw runtime_error(msg); + } + + string MiraiBot::pimpl::Verify(const string& verifyKey) + { + json data = + { + { "verifyKey", verifyKey } + }; + auto res = httpClient->Post("/verify", data.dump(), CONTENT_TYPE.c_str()); + json re_json = ParseOrThrowException(res); + return re_json["session"].get(); + } + + void MiraiBot::pimpl::SessionBind(const string& sessionKey, const QQ_t& qq) + { + json data = + { + { "sessionKey", sessionKey }, + { "qq", int64_t(qq)} + }; + + auto res = httpClient->Post("/bind", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::pimpl::SessionRelease(const string& sessionKey, const QQ_t& qq) + { + json data = + { + { "sessionKey", sessionKey }, + { "qq", int64_t(qq)} + }; + + auto res = httpClient->Post("/release", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + MessageId_t MiraiBot::SendMessage(const QQ_t& target, const MessageChain& messageChain, MessageId_t msgId) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target",int64_t(target) } + }; + data["messageChain"] = messageChain.ToJson(); + if (msgId != 0) data["quote"] = msgId; + + auto res = pmem->httpClient->Post("/sendFriendMessage", data.dump(), CONTENT_TYPE.c_str()); + json re_json = ParseOrThrowException(res); + MessageId_t msg_id = re_json["messageId"].get(); + return msg_id; + } + + + MessageId_t MiraiBot::SendMessage(const GID_t& target, const MessageChain& messageChain, MessageId_t msgId) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target",int64_t(target) } + }; + data["messageChain"] = messageChain.ToJson(); + if (msgId != 0) data["quote"] = msgId; + + auto res = pmem->httpClient->Post("/sendGroupMessage", data.dump(), CONTENT_TYPE.c_str()); + json re_json = ParseOrThrowException(res); + MessageId_t msg_id = re_json["messageId"].get(); + return msg_id; + } + + MessageId_t MiraiBot::SendMessage(const GID_t& gid, const QQ_t& qq, const MessageChain& messageChain, MessageId_t msgId) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "group",int64_t(gid) }, + { "qq",int64_t(qq) } + }; + data["messageChain"] = messageChain.ToJson(); + if (msgId != 0) data["quote"] = msgId; + + auto res = pmem->httpClient->Post("/sendTempMessage", data.dump(), CONTENT_TYPE.c_str()); + json re_json = ParseOrThrowException(res); + MessageId_t msg_id = re_json["messageId"].get(); + return msg_id; + } + + void MiraiBot::pimpl::SendNudge(int64_t target, int64_t subject_id, const string& kind) + { + json data = + { + { "sessionKey", sessionKey }, + { "target", target }, + { "subject", subject_id }, + { "kind" , kind } + }; + + auto res = httpClient->Post("/sendNudge", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::SendNudge(const QQ_t& target, const QQ_t& subject_id) + { + pmem->SendNudge(target.ToInt64(), subject_id.ToInt64(), "Friend"); + } + + void MiraiBot::SendNudge(const QQ_t& target, const GID_t& subject_id) + { + pmem->SendNudge(target.ToInt64(), subject_id.ToInt64(), "Group"); + } + + void MiraiBot::SendNudge(const QQ_t& target, const UID_t& subject_id) + { + if (typeid(subject_id) == typeid(GID_t)) + { + SendNudge(target, (const GID_t&)(subject_id)); + } + else + { + SendNudge(target, (const QQ_t&)(subject_id)); + } + } + + void MiraiBot::SetEssence(MessageId_t target) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", target } + }; + + auto res = pmem->httpClient->Post("/setEssence", data.dump(), CONTENT_TYPE.c_str()); + json re_json = ParseOrThrowException(res); + } + + MiraiImage MiraiBot::pimpl::UploadImage(const string& filename, const string& type) + { + string base_filename = filename.substr(filename.find_last_of("/\\") + 1); + string img_data = ReadFile(filename); + httplib::MultipartFormDataItems items = + { + { "sessionKey", sessionKey, "", "" }, + { "type", type, "", "" }, + { "img", img_data, base_filename, "image/png" } + }; + + auto res = httpClient->Post("/uploadImage", items); + json re_json = ParseOrThrowException(res); + MiraiImage img; + img.Id = re_json["imageId"].get(); + img.Url = re_json.value("url", ""); + img.Path = re_json.value("path", ""); + return img; + } + + FriendImage MiraiBot::UploadFriendImage(const string& filename) + { + return pmem->UploadImage(filename, "friend"); + } + + GroupImage MiraiBot::UploadGroupImage(const string& filename) + { + return pmem->UploadImage(filename, "group"); + } + + TempImage MiraiBot::UploadTempImage(const string& filename) + { + return pmem->UploadImage(filename, "temp"); + } + + MiraiVoice MiraiBot::pimpl::UploadVoice(const string& filename, const string& type) + { + string base_filename = filename.substr(filename.find_last_of("/\\") + 1); + string voice_data = ReadFile(filename); + httplib::MultipartFormDataItems items = + { + { "sessionKey", sessionKey, "", "" }, + { "type", type, "", "" }, + { "voice", voice_data, base_filename, "application/octet-stream" } + }; + + auto res = httpClient->Post("/uploadVoice", items); + json re_json = ParseOrThrowException(res); + MiraiVoice result; + result.Id = re_json["voiceId"].get(); + if (!re_json["url"].is_null()) + result.Url = re_json["url"].get(); + result.Path = re_json["path"].get(); + return result; + } + + MiraiVoice MiraiBot::UploadGroupVoice(const string& filename) + { + return pmem->UploadVoice(filename, "group"); + } + + MiraiFile MiraiBot::pimpl::UploadFileAndSend(int64_t target, const string& filename, const string& type) + { + string base_filename = filename.substr(filename.find_last_of("/\\") + 1); + string file_data = ReadFile(filename); + httplib::MultipartFormDataItems items = + { + { "sessionKey", sessionKey, "", "" }, + { "type", type, "", "" }, + { "target", to_string(target), "", "" }, + { "path", "/" + base_filename, "", "" }, + { "file", file_data, base_filename, "application/octet-stream" } + }; + + auto res = httpClient->Post("/uploadFileAndSend", items); + json re_json = ParseOrThrowException(res); + MiraiFile result; + result.FileSize = file_data.size(); + result.FileName = base_filename; + result.Id = re_json["id"].get(); + return result; + } + + MiraiFile MiraiBot::UploadFileAndSend(const GID_t& gid, const string& filename) + { + return pmem->UploadFileAndSend(gid.ToInt64(), filename, "Group"); + } + + vector MiraiBot::GetFriendList() + { + auto res = pmem->httpClient->Get(("/friendList?sessionKey=" + pmem->sessionKey).data()); + json re_json = ParseOrThrowException(res); + vector result; + for (const auto& ele : re_json["data"]) + { + Friend_t f; + f.Set(ele); + result.emplace_back(f); + } + return result; + } + + + vector MiraiBot::GetGroupList() + { + auto res = pmem->httpClient->Get(("/groupList?sessionKey=" + pmem->sessionKey).data()); + json re_json = ParseOrThrowException(res); + vector result; + for (const auto& ele : re_json["data"]) + { + Group_t group; + group.Set(ele); + result.emplace_back(group); + } + return result; + } + + + vector MiraiBot::GetGroupMembers(const GID_t& target) + { + stringstream api_url; + api_url + << "/memberList?sessionKey=" + << pmem->sessionKey + << "&target=" + << target; + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + vector result; + for (const auto& ele : re_json["data"]) + { + GroupMember f; + f.Set(ele); + result.push_back(f); + } + return result; + } + + GroupMember MiraiBot::GetGroupMemberInfo(const GID_t& gid, const QQ_t& memberId) + { + stringstream api_url; + api_url + << "/memberInfo?sessionKey=" + << pmem->sessionKey + << "&target=" + << int64_t(gid) + << "&memberId=" + << int64_t(memberId); + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + GroupMember result; + result.Set(re_json); + return result; + } + + Profile MiraiBot::GetBotProfile() + { + stringstream api_url; + api_url + << "/botProfile?sessionKey=" + << pmem->sessionKey; + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + Profile result; + result.Set(re_json); + return result; + } + + Profile MiraiBot::GetFriendProfile(const QQ_t& qq) + { + stringstream api_url; + api_url + << "/friendProfile?sessionKey=" + << pmem->sessionKey + << "&target=" + << int64_t(qq); + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + Profile result; + result.Set(re_json); + return result; + } + + Profile MiraiBot::GetGroupMemberProfile(const GID_t& gid, const QQ_t& memberQQ) + { + stringstream api_url; + api_url + << "/memberProfile?sessionKey=" + << pmem->sessionKey + << "&target=" + << int64_t(gid) + << "&memberId=" + << int64_t(memberQQ); + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + Profile result; + result.Set(re_json); + return result; + } + + Profile MiraiBot::GetUserProfile(const QQ_t& qq) + { + stringstream api_url; + api_url + << "/userProfile?sessionKey=" + << pmem->sessionKey + << "&target=" + << int64_t(qq); + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + Profile result; + result.Set(re_json); + return result; + } + + void MiraiBot::SetGroupMemberName(const GID_t& gid, const QQ_t& memberId, const string& name) + { + auto member_info = this->GetGroupMemberInfo(gid, memberId); + pmem->SetGroupMemberInfo(gid, memberId, name, member_info.SpecialTitle); + } + + void MiraiBot::SetGroupMemberSpecialTitle(const GID_t& gid, const QQ_t& memberId, const string& title) + { + auto member_info = this->GetGroupMemberInfo(gid, memberId); + pmem->SetGroupMemberInfo(gid, memberId, member_info.MemberName, title); + } + + vector MiraiBot::GetGroupFiles(const GID_t& gid, bool withDownloadInfo, int offset, int size, const string& parentId) + { + stringstream api_url; + api_url + << "/file/list?sessionKey=" + << pmem->sessionKey + << "&target=" + << int64_t(gid) + << "&id=" + << parentId + << "&offset=" + << offset + << "&size=" + << size + << "&withDownloadInfo=" + << (withDownloadInfo ? "true" : "false"); + // 取文件列表响应比较慢 + pmem->httpClient->set_read_timeout(60, 0); + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + vector result; + for (const auto& item : re_json["data"]) + { + GroupFile f; + f.Set(item); + result.push_back(f); + } + return result; + } + + GroupFile MiraiBot::GetGroupFileById(const GID_t& gid, const string& groupFileId, bool withDownloadInfo) + { + stringstream api_url; + api_url + << "/file/info?sessionKey=" + << pmem->sessionKey + << "&target=" + << int64_t(gid) + << "&id=" + << groupFileId + << "&withDownloadInfo=" + << (withDownloadInfo ? "true" : "false"); + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + GroupFile result; + result.Set(re_json); + return result; + + } + + GroupFile MiraiBot::GroupFileMakeDirectory(const GID_t& target, const string& dictionaryName) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "group", int64_t(target) }, + { "dictionaryName", dictionaryName } + }; + + auto res = pmem->httpClient->Post("/file/mkdir", data.dump(), CONTENT_TYPE.c_str()); + auto re_json = ParseOrThrowException(res); + GroupFile result; + result.Set(re_json); + return result; + } + + void MiraiBot::GroupFileRename(const GroupFile& groupFile, const string& newName) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(groupFile.Group.GID) }, + { "id", groupFile.Id }, + { "renameTo", newName } + }; + + auto res = pmem->httpClient->Post("/file/rename", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::GroupFileMove(const GroupFile& groupFile, const string& targetId) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(groupFile.Group.GID) }, + { "id", groupFile.Id }, + { "moveTo", targetId } + }; + + auto res = pmem->httpClient->Post("/file/move", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::GroupFileDelete(const GroupFile& groupFile) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(groupFile.Group.GID) }, + { "id", groupFile.Id } + }; + + auto res = pmem->httpClient->Post("/file/delete", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::MuteAll(const GID_t& target) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(target)} + }; + + auto res = pmem->httpClient->Post("/muteAll", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + + void MiraiBot::UnMuteAll(const GID_t& target) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(target)} + }; + + auto res = pmem->httpClient->Post("/unmuteAll", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + + void MiraiBot::Mute(const GID_t& gid, const QQ_t& memberId, unsigned int time_seconds) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(gid)}, + { "memberId", int64_t(memberId)}, + { "time", time_seconds} + }; + + auto res = pmem->httpClient->Post("/mute", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + + void MiraiBot::UnMute(const GID_t& gid, const QQ_t& memberId) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(gid)}, + { "memberId", int64_t(memberId)} + }; + + auto res = pmem->httpClient->Post("/unmute", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + + void MiraiBot::Kick(const GID_t& gid, const QQ_t& memberId, const string& reason_msg) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(gid)}, + { "memberId", int64_t(memberId)}, + { "reason_msg" , reason_msg} + }; + + auto res = pmem->httpClient->Post("/kick", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + + void MiraiBot::Recall(MessageId_t mid) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(mid)} + }; + + auto res = pmem->httpClient->Post("/recall", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::QuitGroup(const GID_t& group) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(group)} + }; + + auto res = pmem->httpClient->Post("/quit", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::DeleteFriend(const QQ_t& friendQQ) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(friendQQ)} + }; + + auto res = pmem->httpClient->Post("/deleteFriend", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + std::vector MiraiBot::GetGroupAnnouncement(const GID_t& group, int offset, int size) + { + stringstream api_url; + api_url + << "/anno/list?sessionKey=" + << pmem->sessionKey + << "&id=" + << int64_t(group) + << "&offset=" + << offset + << "&size=" + << size; + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + vector result; + for (const auto& item : re_json["data"]) + { + GroupAnnouncement tmp; + tmp.Set(item); + result.emplace_back(std::move(tmp)); + } + return result; + } + + GroupAnnouncement MiraiBot::PublishGroupAnnouncement(const GID_t& group, + const string& content, + int flags, + const std::optional& image) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(group) }, + { "content", content }, + { "pinned", (bool)(flags & GroupAnnouncement::PublishFlags::Pinned) }, + { "showEditCard", (bool)(flags & GroupAnnouncement::PublishFlags::ShowEditCard) }, + { "showPopup", (bool)(flags & GroupAnnouncement::PublishFlags::ShowPopup) }, + { "requireConfirmation", (bool)(flags & GroupAnnouncement::PublishFlags::RequireConfirmation) }, + { "sendToNewMember", (bool)(flags & GroupAnnouncement::PublishFlags::SendToNewMember) } + }; + if (image.has_value()) + { + image->Url.empty() + ? data["imageUrl"] = json(nullptr) + : data["imageUrl"] = image->Url; + image->Path.empty() + ? data["imagePath"] = json(nullptr) + : data["imagePath"] = image->Path; + image->Base64.empty() + ? data["imageBase64"] = json(nullptr) + : data["imageBase64"] = image->Base64; + } + auto res = pmem->httpClient->Post("/anno/publish", data.dump(), CONTENT_TYPE.c_str()); + auto re_json = ParseOrThrowException(res); + GroupAnnouncement result; + result.Set(re_json["data"]); + return result; + } + + void MiraiBot::DeleteGroupAnnoencement(const GID_t& group, const string& announcementId) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "id", int64_t(group) }, + { "fid", announcementId } + }; + + auto res = pmem->httpClient->Post("/anno/delete", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::DeleteGroupAnnoencement(const GroupAnnouncement& announcement) + { + return DeleteGroupAnnoencement(announcement.Group.GID, announcement.AnnouncementId); + } + + GroupConfig MiraiBot::GetGroupConfig(const GID_t& group) + { + stringstream api_url; + api_url + << "/groupConfig?sessionKey=" + << pmem->sessionKey + << "&target=" + << int64_t(group); + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + GroupConfig group_config; + group_config.Set(re_json); + return group_config; + } + + void MiraiBot::SetGroupConfig(const GID_t& group, const GroupConfig& groupConfig) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(group) } + }; + data["config"] = groupConfig.ToJson(); + + auto res = pmem->httpClient->Post("/groupConfig", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + FriendMessage MiraiBot::GetFriendMessageFromId(MessageId_t mid) + { + stringstream api_url; + api_url + << "/messageFromId?sessionKey=" + << pmem->sessionKey + << "&id=" + << mid; + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + FriendMessage result; + result.Set(re_json["data"]); + return result; + } + + + GroupMessage MiraiBot::GetGroupMessageFromId(MessageId_t mid) + { + stringstream api_url; + api_url + << "/messageFromId?sessionKey=" + << pmem->sessionKey + << "&id=" + << mid; + + auto res = pmem->httpClient->Get(api_url.str().data()); + json re_json = ParseOrThrowException(res); + GroupMessage result; + result.Set(re_json["data"]); + return result; + } + + void MiraiBot::RegisterCommand( + const string& commandName, + const vector& alias, + const string& description, + const string& helpMessage) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "name", commandName }, + { "alias", json(alias) }, + { "description", description }, + { "usage", helpMessage } + }; + auto res = pmem->httpClient->Post("/cmd/register", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + void MiraiBot::SendCommand(const vector& command) + { + MessageChain mc; + for (const auto& val : command) + { + mc.Plain(val); + } + json data = + { + { "sessionKey", pmem->sessionKey }, + { "command", mc.ToJson() } + }; + auto res = pmem->httpClient->Post("/cmd/execute", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } + + Friend_t MiraiBot::GetSessionInfo() + { + auto res = pmem->httpClient->Get("/sessionInfo?sessionKey="s.append(pmem->sessionKey).data()); + json re_json = ParseOrThrowException(res); + Friend_t result; + result.Set(re_json["data"]["qq"]); + return result; + } + + void MiraiBot::SetGroupAdmin(const GID_t& group, const QQ_t& member, bool assign) + { + json data = + { + { "sessionKey", pmem->sessionKey }, + { "target", int64_t(group) }, + { "memberId", int64_t(member) }, + { "assign", assign } + }; + auto res = pmem->httpClient->Post("/memberAdmin", data.dump(), CONTENT_TYPE.c_str()); + ParseOrThrowException(res); + } } // namespace Cyan \ No newline at end of file