diff --git a/main.cpp b/main.cpp index cd363d3..ee35dc5 100644 --- a/main.cpp +++ b/main.cpp @@ -44,7 +44,7 @@ public: StoreResult store(Discord::Snowflake id, const Json::Value& object) { logger.log(Loglevel::verbose, "Stored "+id.str()+" in cache"); auto cache_hit = cache.find(id); - if (cache_hit != cache.end()) { + if (cache_hit != cache.end()) [[likely]] { bool changed = cache_hit->second != object; if (changed) { cache_hit->second = object; @@ -57,7 +57,7 @@ public: StoreResult store(Discord::Snowflake id, Json::Value&& object) { logger.log(Loglevel::verbose, "Moved "+id.str()+" into cache"); auto cache_hit = cache.find(id); - if (cache_hit != cache.end()) { + if (cache_hit != cache.end()) [[likely]] { bool changed = cache_hit->second != object; if (changed) { cache_hit->second = std::move(object); @@ -77,7 +77,7 @@ public: } const Json::Value& fetch(Discord::Snowflake id) const { const auto& entry = cache.find(id); - if (entry == cache.end()) { + if (entry == cache.end()) [[unlikely]] { throw Miss(); } return entry->second; @@ -97,6 +97,16 @@ static inline std::optional GetJSONAsOptionalString(const Json::Val static inline std::optional GetJSONAsOptionalInt(const Json::Value& data) { return data.isString()?data.asInt():std::optional(); } +static inline std::optional GetJSONAsOptionalBool(const Json::Value& data) { + return data.isBool()?data.asBool():std::optional(); +} +static inline std::string GetJSONAsJSONData(const Json::Value& data) { + return Json::writeString(Json::StreamWriterBuilder(), data); +} +static inline std::optional GetJSONAsOptionalJSONData(const Json::Value& data) { + return ((!data.empty()&&!data.isNull())?GetJSONAsJSONData(data):std::optional()); +} + static void reverseString(std::string& str) { int len = str.length(); for (int i = 0; i < len / 2; i++) { @@ -160,12 +170,12 @@ public: " has_nitro INTEGER NOT NULL," " is_bot INTEGER NOT NULL" ");"; - db << "CREATE TABLE IF NOT EXISTS user_statuses (" + db << "CREATE TABLE IF NOT EXISTS user_presences (" " user_id TEXT NOT NULL," " is_initial INTEGER DEFAULT 1 NOT NULL," " timestamp TEXT NOT NULL," - " status TEXT," // JSON array or null if not updated: [int:online/dnd/afk/offline enum, ?:emoji (null/empty if none), string:text (empty if none)] - " presence TEXT" // Discord JSON presence object + " status TEXT," // JSON array or null if not updated: [string:online/dnd/afk/offline, string:device] + " activities TEXT" // Discord JSON Activity object list ");"; db << "CREATE TABLE IF NOT EXISTS members (" " user_id TEXT NOT NULL," @@ -177,15 +187,15 @@ public: " in_guild INTEGER NOT NULL" ");"; db << "CREATE TABLE IF NOT EXISTS member_voice_connections (" - " channel_id TEXT NOT NULL," + " channel_id TEXT," " user_id TEXT NOT NULL," - " is_initial INTEGER DEFAULT 1 NOT NULL," " timestamp TEXT NOT NULL," - " voice_channel_id TEXT," - " self_muted INTEGER NOT NULL," - " self_deafened INTEGER NOT NULL," - " server_muted INTEGER NOT NULL," - " server_deafened INTEGER NOT NULL" + " self_mute INTEGER NOT NULL," + " self_deaf INTEGER NOT NULL," + " self_video INTEGER NOT NULL," + " self_stream INTEGER NOT NULL," + " server_mute INTEGER NOT NULL," + " server_deaf INTEGER NOT NULL" ");"; db << "CREATE TABLE IF NOT EXISTS channels (" " id TEXT NOT NULL," @@ -333,7 +343,7 @@ protected: // Insert member if (changed) { - insertMemberUpdate(data, user_id, guild_id, !cache.has(data["id"])); + insertMemberUpdate(data, user_id, guild_id, changed == Cache::StoreResult::created); } } Cache::StoreResult cacheStoreMember(const Json::Value& data, Discord::Snowflake user_id, Discord::Snowflake guild_id) { @@ -359,8 +369,8 @@ protected: " VALUES (?, ?, ?, ?, ?, ?, ?, ? );" << data["id"].asString() << is_initial << std::to_string(time(nullptr)) << data["username"].asString()+'#'+data["discriminator"].asString() << GetJSONAsOptionalString(data["avatar"]) - << GetJSONAsOptionalString(data["bio"]) << GetJSONAsOptionalInt(data["has_nitro"]).value_or(0) - << GetJSONAsOptionalInt(data["bot"]).value_or(false); + << GetJSONAsOptionalString(data["bio"]) << GetJSONAsOptionalInt(data["premium_type"]).value_or(0) + << GetJSONAsOptionalBool(data["bot"]).value_or(false); } /* @@ -390,7 +400,7 @@ protected: db << "INSERT INTO message_contents (message_id, is_initial, timestamp, content, embeds)" " VALUES (?, ?, ?, ?, ? );" << data["id"].asString() << is_initial << std::to_string(time(nullptr)) << data["content"].asString() - << ((embeds.isArray()&&!embeds.empty())?Json::writeString(Json::StreamWriterBuilder(), embeds):std::optional()); + << GetJSONAsOptionalJSONData(embeds); } void insertMessageUpdate(const Json::Value& data, bool is_initial) { bool has_content = !data["content"].asString().empty() || data["embeds"].isArray(); @@ -431,11 +441,50 @@ protected: insertChannelUpdate(cached_data, false, true); } + /* + * Member voice connections + */ + void updateMemberVoiceConnection(Json::Value&& data) { + // Insert voice connection update + insertMemberVoiceConnectionUpdate(data); + + // Update member + updateMember(std::move(data["member"]), data["guild_id"]); + } + void insertMemberVoiceConnectionUpdate(const Json::Value& data) { + db << "INSERT INTO member_voice_connections (channel_id, user_id, timestamp, self_mute, self_deaf, self_video, self_stream, server_mute, server_deaf)" + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ? );" + << data["channel_id"].asString() << data["user_id"].asString() << std::to_string(time(nullptr)) << data["self_mute"].asBool() + << data["self_deaf"].asBool() << data["self_video"].asBool() << GetJSONAsOptionalBool(data["self_stream"]).value_or(false) + << data["mute"].asBool() << data["deaf"].asBool(); + } + + /* + * User presences + */ + void insertUserPresenceUpdate(const Json::Value& data, bool is_initial) { + // Build status + std::string status_str; + { + const auto& client_status = data["client_status"]; + if (client_status.isArray() && !client_status.empty()) { + const auto device = client_status.getMemberNames()[0]; + status_str = "[\""+client_status[device].asString()+"\", \""+device+"\"]"; + } else { + status_str = R"(["offline", "none"])"; + } + } + + // Insert + db << "INSERT INTO user_presences (user_id, is_initial, timestamp, status, activities)" + " VALUES (?, ?, ?, ?, ? );" + << data["user"]["id"].asString() << is_initial << std::to_string(time(nullptr)) << status_str << GetJSONAsOptionalJSONData(data["activities"]); + } + /* * Intent handler */ virtual boost::asio::awaitable intentHandler(std::string intent, Json::Value data) override { - std::ofstream(intent+".json") << data; if (intent == "READY") [[unlikely]] { // Get own user const auto& user = cache.store(std::move(data["user"])).data; @@ -444,7 +493,7 @@ protected: co_await fetchAllGuilds(std::move(data["guilds"])); // Get users auto& users = data["users"]; - if (users.isArray()) { + if (users.isArray()) [[likely]] { for (const auto& user_data : users) { insertUserUpdate(user_data, true); cache.store(std::move(user_data)); @@ -487,6 +536,22 @@ protected: const auto& user_data = cache.store(data["user"]).data; insertMemberDelete(user_data["id"], data["guild_id"]); } + else if (intent == "VOICE_STATE_UPDATE") { + updateMemberVoiceConnection(std::move(data)); + } + else if (intent == "PRESENCE_UPDATE") { + auto changed = cache.store(Discord::Snowflake(data["user_id"]).uint()|0x04e2e7ce00000000, data).changed; + insertUserPresenceUpdate(data, changed==Cache::StoreResult::created); + } + else if (intent == "GUILD_BAN_ADD") [[unlikely]] { + // Should we log bans? Let's just use it as a source of user data + const auto& user_data = data["user"]; + auto changed = cache.store(user_data).changed; + if (changed) [[unlikely]] { + insertUserUpdate(user_data, (changed&Cache::StoreResult::created)?true:false); + } + //TODO: Should we do insertMemberDelete here? + } co_return; } };