First implementation for the conversation system (not 100% finished yet)
This commit is contained in:
		
							parent
							
								
									dd0eaf33e5
								
							
						
					
					
						commit
						e507d1f75d
					
				| @ -22,7 +22,7 @@ void AbstractMusicPlayer::registerEventHandler(const std::string& key, const std | |||||||
| void AbstractMusicPlayer::unregisterEventHandler(const std::string& string) { | void AbstractMusicPlayer::unregisterEventHandler(const std::string& string) { | ||||||
|     threads::MutexLock lock(this->eventLock); |     threads::MutexLock lock(this->eventLock); | ||||||
|     for(const auto& entry : this->eventHandlers){ |     for(const auto& entry : this->eventHandlers){ | ||||||
|         if(entry.first == string){ |         if(entry.first == string) { | ||||||
|             this->eventHandlers.erase(find_if(this->eventHandlers.begin(), this->eventHandlers.end(), [string](const std::pair<std::string, std::function<void(MusicEvent)>>& elm){ return elm.first == string; })); |             this->eventHandlers.erase(find_if(this->eventHandlers.begin(), this->eventHandlers.end(), [string](const std::pair<std::string, std::function<void(MusicEvent)>>& elm){ return elm.first == string; })); | ||||||
|             break; |             break; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -128,6 +128,8 @@ set(SERVER_SOURCE_FILES | |||||||
| 
 | 
 | ||||||
| 		src/weblist/WebListManager.cpp | 		src/weblist/WebListManager.cpp | ||||||
| 		src/weblist/TeamSpeakWebClient.cpp | 		src/weblist/TeamSpeakWebClient.cpp | ||||||
|  | 
 | ||||||
|  | 		src/manager/ConversationManager.cpp | ||||||
| ) | ) | ||||||
| if(COMPILE_WEB_CLIENT) | if(COMPILE_WEB_CLIENT) | ||||||
| 	add_definitions(-DCOMPILE_WEB_CLIENT) | 	add_definitions(-DCOMPILE_WEB_CLIENT) | ||||||
|  | |||||||
| @ -226,7 +226,8 @@ inline sockaddr_in* resolveAddress(const string& host, uint16_t port) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| bool InstanceHandler::startInstance() { | bool InstanceHandler::startInstance() { | ||||||
|     if (this->active) return false; |     if (this->active) | ||||||
|  |     	return false; | ||||||
|     active = true; |     active = true; | ||||||
|     this->web_list->enabled = ts::config::server::enable_teamspeak_weblist; |     this->web_list->enabled = ts::config::server::enable_teamspeak_weblist; | ||||||
| 
 | 
 | ||||||
| @ -236,6 +237,12 @@ bool InstanceHandler::startInstance() { | |||||||
| 		return false; | 		return false; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	this->conversation_io = make_shared<event::EventExecutor>("conv io #"); | ||||||
|  | 	if(!this->conversation_io->initialize(1)) { //TODO: Make the conversation IO loop thread size configurable
 | ||||||
|  | 		logCritical(LOG_GENERAL, "Failed to initialize conversation io write loop"); | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	//Startup file server
 | 	//Startup file server
 | ||||||
| 	sockaddr_in *fAddr = resolveAddress(this->properties()[property::SERVERINSTANCE_FILETRANSFER_HOST].as<string>(), this->properties()[property::SERVERINSTANCE_FILETRANSFER_PORT].as<uint16_t>()); | 	sockaddr_in *fAddr = resolveAddress(this->properties()[property::SERVERINSTANCE_FILETRANSFER_HOST].as<string>(), this->properties()[property::SERVERINSTANCE_FILETRANSFER_PORT].as<uint16_t>()); | ||||||
| 	if (!fAddr) { | 	if (!fAddr) { | ||||||
|  | |||||||
| @ -62,6 +62,7 @@ namespace ts { | |||||||
| 		        std::shared_ptr<ts::Properties> getDefaultServerProperties() { return this->default_server_properties; } | 		        std::shared_ptr<ts::Properties> getDefaultServerProperties() { return this->default_server_properties; } | ||||||
| 		        std::shared_ptr<webio::LoopManager> getWebIoLoop() { return this->web_event_loop; } | 		        std::shared_ptr<webio::LoopManager> getWebIoLoop() { return this->web_event_loop; } | ||||||
| 		        std::shared_ptr<weblist::WebListManager> getWebList() { return this->web_list; } | 		        std::shared_ptr<weblist::WebListManager> getWebList() { return this->web_list; } | ||||||
|  | 		        std::shared_ptr<ts::event::EventExecutor> getConversationIo() { return this->conversation_io; } | ||||||
|             private: |             private: | ||||||
|                 std::mutex activeLock; |                 std::mutex activeLock; | ||||||
|                 std::condition_variable activeCon; |                 std::condition_variable activeCon; | ||||||
| @ -86,6 +87,7 @@ namespace ts { | |||||||
| 
 | 
 | ||||||
|                 ts::Properties* _properties = nullptr; |                 ts::Properties* _properties = nullptr; | ||||||
| 
 | 
 | ||||||
|  |                 std::shared_ptr<ts::event::EventExecutor> conversation_io = nullptr; | ||||||
|                 std::shared_ptr<webio::LoopManager> web_event_loop = nullptr; |                 std::shared_ptr<webio::LoopManager> web_event_loop = nullptr; | ||||||
| 				std::shared_ptr<weblist::WebListManager> web_list = nullptr; | 				std::shared_ptr<weblist::WebListManager> web_list = nullptr; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ | |||||||
| #include <log/LogUtils.h> | #include <log/LogUtils.h> | ||||||
| #include "InstanceHandler.h" | #include "InstanceHandler.h" | ||||||
| #include "TSServer.h" | #include "TSServer.h" | ||||||
|  | #include "./manager/ConversationManager.h" | ||||||
| 
 | 
 | ||||||
| using namespace std; | using namespace std; | ||||||
| using namespace std::chrono; | using namespace std::chrono; | ||||||
| @ -52,7 +53,7 @@ void TSServer::executeServerTick() { | |||||||
| 		lastTick = system_clock::now(); | 		lastTick = system_clock::now(); | ||||||
| 
 | 
 | ||||||
| 		system_clock::time_point timing_begin, timing_end; | 		system_clock::time_point timing_begin, timing_end; | ||||||
| 		milliseconds timing_update_states, timing_client_tick, timing_channel, timing_statistic, timing_groups; | 		milliseconds timing_update_states, timing_client_tick, timing_channel, timing_statistic, timing_groups, timing_ccache; | ||||||
| 
 | 
 | ||||||
| 		auto client_list = this->getClients(); | 		auto client_list = this->getClients(); | ||||||
| 
 | 
 | ||||||
| @ -255,14 +256,26 @@ void TSServer::executeServerTick() { | |||||||
| 			END_TIMINGS(timing_groups); | 			END_TIMINGS(timing_groups); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | 		{ | ||||||
|  | 			BEGIN_TIMINGS(); | ||||||
|  | 			if(this->conversation_cache_cleanup_timestamp + minutes(15) < system_clock::now()) { | ||||||
|  | 				debugMessage(this->serverId, "Cleaning up conversation cache."); | ||||||
|  | 				this->_conversation_manager->cleanup_cache(); | ||||||
|  | 				conversation_cache_cleanup_timestamp = system_clock::now(); | ||||||
|  | 			} | ||||||
|  | 			END_TIMINGS(timing_ccache); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
| 		if(system_clock::now() - lastTick > milliseconds(100)) { | 		if(system_clock::now() - lastTick > milliseconds(100)) { | ||||||
| 			//milliseconds timing_update_states, timing_client_tick, timing_channel, timing_statistic;
 | 			//milliseconds timing_update_states, timing_client_tick, timing_channel, timing_statistic;
 | ||||||
| 			logError(this->serverId, "Server tick tooks to long ({}ms => Status updates: {}ms Client tick: {}ms, Channel tick: {}ms, Statistic tick: {}ms)", | 			logError(this->serverId, "Server tick tooks to long ({}ms => Status updates: {}ms Client tick: {}ms, Channel tick: {}ms, Statistic tick: {}ms, Groups: {}ms, Conversation cache: {}ms)", | ||||||
| 					duration_cast<milliseconds>(system_clock::now() - lastTick).count(), | 					duration_cast<milliseconds>(system_clock::now() - lastTick).count(), | ||||||
| 					 timing_update_states.count(), | 					 timing_update_states.count(), | ||||||
| 					 timing_client_tick.count(), | 					 timing_client_tick.count(), | ||||||
| 					 timing_channel.count(), | 					 timing_channel.count(), | ||||||
| 					 timing_statistic.count() | 					 timing_statistic.count(), | ||||||
|  | 					 timing_groups.count(), | ||||||
|  | 					 timing_ccache.count() | ||||||
| 			); | 			); | ||||||
| 		} | 		} | ||||||
| 	} catch (std::exception& ex) { | 	} catch (std::exception& ex) { | ||||||
|  | |||||||
| @ -21,6 +21,7 @@ | |||||||
| #include "InstanceHandler.h" | #include "InstanceHandler.h" | ||||||
| #include "Configuration.h" | #include "Configuration.h" | ||||||
| #include "TSServer.h" | #include "TSServer.h" | ||||||
|  | #include "src/manager/ConversationManager.h" | ||||||
| #include <misc/sassert.h> | #include <misc/sassert.h> | ||||||
| 
 | 
 | ||||||
| using namespace std; | using namespace std; | ||||||
| @ -113,6 +114,9 @@ bool TSServer::initialize(bool test_properties) { | |||||||
| 		properties()[property::VIRTUALSERVER_UNIQUE_IDENTIFIER] = base64::encode(digest::sha1(base64::encode(buffer, bufferLength))); | 		properties()[property::VIRTUALSERVER_UNIQUE_IDENTIFIER] = base64::encode(digest::sha1(base64::encode(buffer, bufferLength))); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	this->_conversation_manager = make_shared<conversation::ConversationManager>(this->ref()); | ||||||
|  | 	this->_conversation_manager->initialize(this->_conversation_manager); | ||||||
|  | 
 | ||||||
| 	channelTree = new ServerChannelTree(self.lock(), this->sql); | 	channelTree = new ServerChannelTree(self.lock(), this->sql); | ||||||
| 	channelTree->loadChannelsFromDatabase(); | 	channelTree->loadChannelsFromDatabase(); | ||||||
| 
 | 
 | ||||||
| @ -218,9 +222,11 @@ bool TSServer::initialize(bool test_properties) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if(this->properties()[property::VIRTUALSERVER_FILEBASE].value().empty()) { | 	if(this->properties()[property::VIRTUALSERVER_FILEBASE].value().empty()) | ||||||
| 		this->properties()[property::VIRTUALSERVER_FILEBASE] = serverInstance->getFileServer()->server_file_base(self.lock()); | 		this->properties()[property::VIRTUALSERVER_FILEBASE] = serverInstance->getFileServer()->server_file_base(self.lock()); | ||||||
| 	} | 
 | ||||||
|  | 	/* lets cleanup the conversations for not existent channels */ | ||||||
|  | 	this->_conversation_manager->cleanup_channels(); | ||||||
| 	return true; | 	return true; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -231,6 +237,7 @@ TSServer::~TSServer() { | |||||||
|     delete this->channelTree; |     delete this->channelTree; | ||||||
|     delete this->letters; |     delete this->letters; | ||||||
|     delete this->complains; |     delete this->complains; | ||||||
|  |     this->_conversation_manager.reset(); | ||||||
| 
 | 
 | ||||||
|     if(this->_serverKey) ecc_free(this->_serverKey); |     if(this->_serverKey) ecc_free(this->_serverKey); | ||||||
|     delete this->_serverKey; |     delete this->_serverKey; | ||||||
| @ -860,7 +867,7 @@ vector<pair<ts::permission::PermissionType, ts::permission::v2::PermissionFlagge | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if(found_negate) { | 		if(found_negate) { | ||||||
| 			server_group_data.erase(find_if(server_group_data.begin(), server_group_data.end(), [](auto data) { return !std::get<2>(data); }), server_group_data.end()); | 			server_group_data.erase(remove_if(server_group_data.begin(), server_group_data.end(), [](auto data) { return !std::get<2>(data); }), server_group_data.end()); | ||||||
| 			logTrace(this->serverId, "[Permission] Found negate flag within server groups. Groups left: {}", server_group_data.size()); | 			logTrace(this->serverId, "[Permission] Found negate flag within server groups. Groups left: {}", server_group_data.size()); | ||||||
| 			sassert(!server_group_data.empty()); /* this should never happen! */ | 			sassert(!server_group_data.empty()); /* this should never happen! */ | ||||||
| 			permission::PermissionValue current_lowest = 0; | 			permission::PermissionValue current_lowest = 0; | ||||||
|  | |||||||
| @ -64,6 +64,10 @@ namespace ts { | |||||||
|         class WebControlServer; |         class WebControlServer; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  |         namespace conversation { | ||||||
|  |         	class ConversationManager; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         struct ServerState { |         struct ServerState { | ||||||
| 	        enum value { | 	        enum value { | ||||||
| 		        OFFLINE, | 		        OFFLINE, | ||||||
| @ -268,6 +272,7 @@ namespace ts { | |||||||
|                 ); |                 ); | ||||||
| 
 | 
 | ||||||
| 			    inline int voice_encryption_mode() { return this->_voice_encryption_mode; } | 			    inline int voice_encryption_mode() { return this->_voice_encryption_mode; } | ||||||
|  | 			    inline std::shared_ptr<conversation::ConversationManager> conversation_manager() { return this->_conversation_manager; } | ||||||
|             protected: |             protected: | ||||||
|                 bool registerClient(std::shared_ptr<ConnectedClient>); |                 bool registerClient(std::shared_ptr<ConnectedClient>); | ||||||
|                 bool unregisterClient(std::shared_ptr<ConnectedClient>, std::string, std::unique_lock<std::shared_mutex>& channel_tree_lock); |                 bool unregisterClient(std::shared_ptr<ConnectedClient>, std::string, std::unique_lock<std::shared_mutex>& channel_tree_lock); | ||||||
| @ -289,6 +294,7 @@ namespace ts { | |||||||
|                 letter::LetterManager* letters = nullptr; |                 letter::LetterManager* letters = nullptr; | ||||||
|                 std::shared_ptr<music::MusicBotManager> musicManager; |                 std::shared_ptr<music::MusicBotManager> musicManager; | ||||||
|                 std::shared_ptr<stats::ConnectionStatistics> serverStatistics; |                 std::shared_ptr<stats::ConnectionStatistics> serverStatistics; | ||||||
|  |                 std::shared_ptr<conversation::ConversationManager> _conversation_manager; | ||||||
| 
 | 
 | ||||||
|                 sql::SqlManager* sql; |                 sql::SqlManager* sql; | ||||||
| 
 | 
 | ||||||
| @ -296,6 +302,7 @@ namespace ts { | |||||||
| 
 | 
 | ||||||
| 			    std::chrono::system_clock::time_point startTimestamp; | 			    std::chrono::system_clock::time_point startTimestamp; | ||||||
| 			    std::chrono::system_clock::time_point fileStatisticsTimestamp; | 			    std::chrono::system_clock::time_point fileStatisticsTimestamp; | ||||||
|  | 			    std::chrono::system_clock::time_point conversation_cache_cleanup_timestamp; | ||||||
| 
 | 
 | ||||||
|                 //The client list
 |                 //The client list
 | ||||||
|                 struct { |                 struct { | ||||||
|  | |||||||
| @ -32,7 +32,6 @@ namespace ts { | |||||||
| 		    ChannelId cached_parent_id = 0; | 		    ChannelId cached_parent_id = 0; | ||||||
|     }; |     }; | ||||||
| 
 | 
 | ||||||
|     typedef std::shared_lock<std::shared_recursive_mutex> TreeLock; |  | ||||||
|     class ClientChannelView : private TreeView { |     class ClientChannelView : private TreeView { | ||||||
|         public: |         public: | ||||||
|     		enum ChannelAction { |     		enum ChannelAction { | ||||||
|  | |||||||
| @ -7,6 +7,7 @@ | |||||||
| #include "src/client/ConnectedClient.h" | #include "src/client/ConnectedClient.h" | ||||||
| #include "src/server/file/FileServer.h" | #include "src/server/file/FileServer.h" | ||||||
| #include "src/InstanceHandler.h" | #include "src/InstanceHandler.h" | ||||||
|  | #include "../manager/ConversationManager.h" | ||||||
| 
 | 
 | ||||||
| using namespace std; | using namespace std; | ||||||
| using namespace ts; | using namespace ts; | ||||||
| @ -530,9 +531,10 @@ void ServerChannelTree::on_channel_entry_deleted(const shared_ptr<BasicChannel> | |||||||
| 	BasicChannelTree::on_channel_entry_deleted(channel); | 	BasicChannelTree::on_channel_entry_deleted(channel); | ||||||
| 
 | 
 | ||||||
| 	auto server = this->server.lock(); | 	auto server = this->server.lock(); | ||||||
| 	if(server) | 	if(server) { | ||||||
| 		server->getGroupManager()->handleChannelDeleted(channel); | 		server->getGroupManager()->handleChannelDeleted(channel); | ||||||
| 	else | 		server->conversation_manager()->delete_conversation(channel->channelId()); | ||||||
|  | 	} else | ||||||
| 		serverInstance->getGroupManager()->handleChannelDeleted(channel); | 		serverInstance->getGroupManager()->handleChannelDeleted(channel); | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -112,12 +112,12 @@ void ConnectedClient::updateChannelClientProperties(bool lock_channel_tree, bool | |||||||
| 
 | 
 | ||||||
|     deque<property::ClientProperties> notifyList; |     deque<property::ClientProperties> notifyList; | ||||||
| 	debugMessage(this->getServerId(), "{} Got a channel talk power of {} Talk power set is {}", CLIENT_STR_LOG_PREFIX, permission_talk_power, this->properties()[property::CLIENT_TALK_POWER].as<uint64_t>()); | 	debugMessage(this->getServerId(), "{} Got a channel talk power of {} Talk power set is {}", CLIENT_STR_LOG_PREFIX, permission_talk_power, this->properties()[property::CLIENT_TALK_POWER].as<uint64_t>()); | ||||||
|     if(permission_talk_power != this->properties()[property::CLIENT_TALK_POWER].as<uint64_t>() && this->currentChannel) { //We do not have to update tp if there's no channel
 |     if(permission_talk_power != this->properties()[property::CLIENT_TALK_POWER].as<uint64_t>()) { //We do not have to update tp if there's no channel
 | ||||||
|         this->properties()[property::CLIENT_TALK_POWER] = permission_talk_power; |         this->properties()[property::CLIENT_TALK_POWER] = permission_talk_power; | ||||||
|         notifyList.emplace_back(property::CLIENT_TALK_POWER); |         notifyList.emplace_back(property::CLIENT_TALK_POWER); | ||||||
| 
 | 
 | ||||||
| 	    auto update = this->properties()[property::CLIENT_IS_TALKER].as<bool>() || this->properties()[property::CLIENT_TALK_REQUEST].as<int64_t>() > 0; | 	    auto update = this->properties()[property::CLIENT_IS_TALKER].as<bool>() || this->properties()[property::CLIENT_TALK_REQUEST].as<int64_t>() > 0; | ||||||
| 	    if(update) { | 	    if(update && this->currentChannel) { | ||||||
| 	    	if(this->currentChannel->talk_power_granted({permission_talk_power, permission_talk_power != permNotGranted})) { | 	    	if(this->currentChannel->talk_power_granted({permission_talk_power, permission_talk_power != permNotGranted})) { | ||||||
| 			    this->properties()[property::CLIENT_IS_TALKER] = 0; | 			    this->properties()[property::CLIENT_IS_TALKER] = 0; | ||||||
| 			    this->properties()[property::CLIENT_TALK_REQUEST] = 0; | 			    this->properties()[property::CLIENT_TALK_REQUEST] = 0; | ||||||
|  | |||||||
| @ -583,6 +583,9 @@ namespace ts { | |||||||
| 		        CommandResult handleCommandQueryDelete(Command&); | 		        CommandResult handleCommandQueryDelete(Command&); | ||||||
| 		        CommandResult handleCommandQueryChangePassword(Command&); | 		        CommandResult handleCommandQueryChangePassword(Command&); | ||||||
| 
 | 
 | ||||||
|  | 		        CommandResult handleCommandConversationHistory(Command&); | ||||||
|  | 		        CommandResult handleCommandConversationFetch(Command&); | ||||||
|  | 
 | ||||||
|                 CommandResult handleCommandLogView(Command&); |                 CommandResult handleCommandLogView(Command&); | ||||||
|                 //CMD_TODO handleCommandLogAdd
 |                 //CMD_TODO handleCommandLogAdd
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ | |||||||
| #include "music/MusicClient.h" | #include "music/MusicClient.h" | ||||||
| #include "query/QueryClient.h" | #include "query/QueryClient.h" | ||||||
| #include "../weblist/WebListManager.h" | #include "../weblist/WebListManager.h" | ||||||
|  | #include "../manager/ConversationManager.h" | ||||||
| #include <experimental/filesystem> | #include <experimental/filesystem> | ||||||
| #include <cstdint> | #include <cstdint> | ||||||
| #include <StringVariable.h> | #include <StringVariable.h> | ||||||
| @ -345,6 +346,8 @@ CommandResult ConnectedClient::handleCommand(Command &cmd) { | |||||||
|     else if (command == "playlistsongremove") return this->handleCommandPlaylistSongRemove(cmd); |     else if (command == "playlistsongremove") return this->handleCommandPlaylistSongRemove(cmd); | ||||||
| 
 | 
 | ||||||
|     else if (command == "dummy_ipchange") return this->handleCommandDummy_IpChange(cmd); |     else if (command == "dummy_ipchange") return this->handleCommandDummy_IpChange(cmd); | ||||||
|  |     else if (command == "conversationhistory") return this->handleCommandConversationHistory(cmd); | ||||||
|  |     else if (command == "conversationfetch") return this->handleCommandConversationFetch(cmd); | ||||||
| 
 | 
 | ||||||
|     if (this->getType() == ClientType::CLIENT_QUERY) return CommandResult::NotImplemented; //Dont log query invalid commands
 |     if (this->getType() == ClientType::CLIENT_QUERY) return CommandResult::NotImplemented; //Dont log query invalid commands
 | ||||||
|     if (this->getType() == ClientType::CLIENT_TEAMSPEAK) |     if (this->getType() == ClientType::CLIENT_TEAMSPEAK) | ||||||
| @ -1162,8 +1165,8 @@ CommandResult ConnectedClient::handleCommandChannelGroupAddPerm(Command &cmd) { | |||||||
| 				    permission::v2::PermissionUpdateType::set_value, | 				    permission::v2::PermissionUpdateType::set_value, | ||||||
| 				    permission::v2::PermissionUpdateType::do_nothing, | 				    permission::v2::PermissionUpdateType::do_nothing, | ||||||
| 
 | 
 | ||||||
| 				    cmd[index]["permnegated"].as<bool>() ? 1 : 0, | 				    cmd[index]["permskip"].as<bool>() ? 1 : 0, | ||||||
| 				    cmd[index]["permskip"].as<bool>() ? 1 : 0 | 				    cmd[index]["permnegated"].as<bool>() ? 1 : 0 | ||||||
| 		    ); | 		    ); | ||||||
| 		    updateList |= permission_is_group_property(permType); | 		    updateList |= permission_is_group_property(permType); | ||||||
| 	    } | 	    } | ||||||
| @ -2328,8 +2331,8 @@ CommandResult ConnectedClient::handleCommandChannelAddPerm(Command &cmd) { | |||||||
| 	        		permission::v2::PermissionUpdateType::set_value, | 	        		permission::v2::PermissionUpdateType::set_value, | ||||||
| 	        		permission::v2::PermissionUpdateType::do_nothing, | 	        		permission::v2::PermissionUpdateType::do_nothing, | ||||||
| 
 | 
 | ||||||
| 			        cmd[index]["permnegated"].as<bool>() ? 1 : 0, | 			        cmd[index]["permskip"].as<bool>() ? 1 : 0, | ||||||
| 			        cmd[index]["permskip"].as<bool>() ? 1 : 0 | 			        cmd[index]["permnegated"].as<bool>() ? 1 : 0 | ||||||
|             ); |             ); | ||||||
| 	        updateClients |= permission_is_client_property(permType); | 	        updateClients |= permission_is_client_property(permType); | ||||||
|             update_view |= permType == permission::i_channel_needed_view_power; |             update_view |= permType == permission::i_channel_needed_view_power; | ||||||
| @ -2813,8 +2816,8 @@ CommandResult ConnectedClient::handleCommandServerGroupAddPerm(Command &cmd) { | |||||||
| 				    permission::v2::PermissionUpdateType::set_value, | 				    permission::v2::PermissionUpdateType::set_value, | ||||||
| 				    permission::v2::PermissionUpdateType::do_nothing, | 				    permission::v2::PermissionUpdateType::do_nothing, | ||||||
| 
 | 
 | ||||||
| 				    cmd[index]["permnegated"].as<bool>() ? 1 : 0, | 				    cmd[index]["permskip"].as<bool>() ? 1 : 0, | ||||||
| 				    cmd[index]["permskip"].as<bool>() ? 1 : 0 | 				    cmd[index]["permnegated"].as<bool>() ? 1 : 0 | ||||||
| 		    ); | 		    ); | ||||||
| 		    sgroupUpdate |= permission_is_group_property(permType); | 		    sgroupUpdate |= permission_is_group_property(permType); | ||||||
| 		    checkTp |= permission_is_client_property(permType); | 		    checkTp |= permission_is_client_property(permType); | ||||||
| @ -3210,6 +3213,10 @@ CommandResult ConnectedClient::handleCommandSendTextMessage(Command &cmd) { | |||||||
| 	    if(this->handleTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, cmd["msg"], nullptr)) return CommandResult::Success; | 	    if(this->handleTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, cmd["msg"], nullptr)) return CommandResult::Success; | ||||||
|         for (auto &cl : this->server->getClientsByChannel(this->currentChannel)) |         for (auto &cl : this->server->getClientsByChannel(this->currentChannel)) | ||||||
|             cl->notifyTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, _this.lock(), this->getClientId(), cmd["msg"].string()); |             cl->notifyTextMessage(ChatMessageMode::TEXTMODE_CHANNEL, _this.lock(), this->getClientId(), cmd["msg"].string()); | ||||||
|  | 
 | ||||||
|  |         auto conversations = this->server->conversation_manager(); | ||||||
|  |         auto conversation = conversations->get_or_create(this->currentChannel->channelId()); | ||||||
|  | 	    conversation->register_message(this->getClientDatabaseId(), this->getUid(), this->getDisplayName(), cmd["msg"].string()); | ||||||
|     } else if (cmd["targetmode"] == ChatMessageMode::TEXTMODE_SERVER) { |     } else if (cmd["targetmode"] == ChatMessageMode::TEXTMODE_SERVER) { | ||||||
| 	    CACHED_PERM_CHECK(permission::b_client_server_textmessage_send, 1); | 	    CACHED_PERM_CHECK(permission::b_client_server_textmessage_send, 1); | ||||||
| 
 | 
 | ||||||
| @ -7200,7 +7207,189 @@ CommandResult ConnectedClient::handleCommandDummy_IpChange(ts::Command &cmd) { | |||||||
| 	return CommandResult::Success; | 	return CommandResult::Success; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | //conversationhistory cid=1 [cpw=xxx] [timestamp_begin] [timestamp_end (0 := no end)] [message_count (default 25| max 100)] [-merge]
 | ||||||
|  | CommandResult ConnectedClient::handleCommandConversationHistory(ts::Command &command) { | ||||||
|  | 	CMD_REF_SERVER(ref_server); | ||||||
|  | 	CMD_CHK_AND_INC_FLOOD_POINTS(25); | ||||||
|  | 
 | ||||||
|  | 	if(!command[0].has("cid") || !command[0]["cid"].castable<ChannelId>()) | ||||||
|  | 		return {findError("conversation_invalid_id")}; | ||||||
|  | 
 | ||||||
|  | 	auto conversation_id = command[0]["cid"].as<ChannelId>(); | ||||||
|  | 	/* test if we have access to the conversation */ | ||||||
|  | 	{ | ||||||
|  | 		/* test if we're able to see the channel */ | ||||||
|  | 		{ | ||||||
|  | 			shared_lock channel_view_lock(this->channel_lock); | ||||||
|  | 			auto channel = this->channel_view()->find_channel(conversation_id); | ||||||
|  | 			if(!channel) | ||||||
|  | 				return {findError("conversation_invalid_id")}; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		/* test if there is a channel password or join power which denies that we see the conversation */ | ||||||
|  | 		{ | ||||||
|  | 			shared_lock channel_view_lock(ref_server->channel_tree_lock); | ||||||
|  | 			auto channel = ref_server->getChannelTree()->findChannel(conversation_id); | ||||||
|  | 			if(!channel) /* should never happen! */ | ||||||
|  | 				return {findError("conversation_invalid_id")}; | ||||||
|  | 
 | ||||||
|  | 			if(!command[0].has("cpw")) | ||||||
|  | 				command[0]["cpw"] = ""; | ||||||
|  | 
 | ||||||
|  | 			if (!channel->passwordMatch(command["cpw"], true)) | ||||||
|  | 				if (!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_join_ignore_password, 1, channel, true)) | ||||||
|  | 					return {findError("channel_invalid_password"), "invalid password"}; | ||||||
|  | 
 | ||||||
|  | 			if(!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_ignore_join_power, 1, channel, true)) { | ||||||
|  | 				CHANNEL_PERMISSION_TEST(permission::i_channel_join_power, permission::i_channel_needed_join_power, channel, false); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	auto conversation_manager = ref_server->conversation_manager(); | ||||||
|  | 	auto conversation = conversation_manager->get(conversation_id); | ||||||
|  | 	if(!conversation) | ||||||
|  | 		return {ErrorType::DBEmpty}; | ||||||
|  | 
 | ||||||
|  | 	system_clock::time_point timestamp_begin = system_clock::now(); | ||||||
|  | 	system_clock::time_point timestamp_end; | ||||||
|  | 	size_t message_count = 25; | ||||||
|  | 
 | ||||||
|  | 	if(command[0].has("timestamp_begin")) | ||||||
|  | 		timestamp_begin = system_clock::time_point{} + milliseconds(command[0]["timestamp_begin"].as<uint64_t>()); | ||||||
|  | 
 | ||||||
|  | 	if(command[0].has("timestamp_end")) | ||||||
|  | 		timestamp_end = system_clock::time_point{} + milliseconds(command[0]["timestamp_end"].as<uint64_t>()); | ||||||
|  | 
 | ||||||
|  | 	if(command[0].has("message_count")) | ||||||
|  | 		message_count = command[0]["message_count"].as<uint64_t>(); | ||||||
|  | 
 | ||||||
|  | 	if(timestamp_begin < timestamp_end) | ||||||
|  | 		return {findError("parameter_invalid")}; | ||||||
|  | 	if(message_count > 100) | ||||||
|  | 		message_count = 100; | ||||||
|  | 
 | ||||||
|  | 	auto messages = conversation->message_history(timestamp_begin, message_count + 1, timestamp_end); /* query one more to test for more data */ | ||||||
|  | 	if(messages.empty()) | ||||||
|  | 		return {ErrorType::DBEmpty}; | ||||||
|  | 	bool more_data = messages.size() > message_count; | ||||||
|  | 	if(more_data) | ||||||
|  | 		messages.pop_back(); | ||||||
|  | 
 | ||||||
|  | 	Command notify(this->notify_response_command("notifyconversationhistory")); | ||||||
|  | 	size_t index = 0; | ||||||
|  | 	size_t length = 0; | ||||||
|  | 	bool merge = command.hasParm("merge"); | ||||||
|  | 	for(auto& message : messages) { | ||||||
|  | 		if(index == 0) | ||||||
|  | 			notify[index]["cid"] = conversation_id; | ||||||
|  | 
 | ||||||
|  | 		notify[index]["timestamp"] = duration_cast<milliseconds>(message->message_timestamp.time_since_epoch()).count(); | ||||||
|  | 		notify[index]["sender_database_id"] = message->sender_database_id; | ||||||
|  | 		notify[index]["sender_unique_id"] = message->sender_unique_id; | ||||||
|  | 		notify[index]["sender_name"] = message->sender_name; | ||||||
|  | 
 | ||||||
|  | 		notify[index]["msg"] = message->message; | ||||||
|  | 		length += message->message.size(); | ||||||
|  | 		length += message->sender_name.size(); | ||||||
|  | 		length += message->sender_unique_id.size(); | ||||||
|  | 		if(length > 1024 * 8 || !merge) { | ||||||
|  | 			index = 0; | ||||||
|  | 			this->sendCommand(notify); | ||||||
|  | 			notify = Command{this->notify_response_command("notifyconversationhistory")}; | ||||||
|  | 		} else | ||||||
|  | 			index++; | ||||||
|  | 	} | ||||||
|  | 	if(index > 0) | ||||||
|  | 		this->sendCommand(notify); | ||||||
|  | 
 | ||||||
|  | 	if(more_data) | ||||||
|  | 		return {findError("conversation_more_data")}; | ||||||
|  | 
 | ||||||
|  | 	return CommandResult::Success; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | CommandResult ConnectedClient::handleCommandConversationFetch(ts::Command &cmd) { | ||||||
|  | 	CMD_REF_SERVER(ref_server); | ||||||
|  | 	CMD_CHK_AND_INC_FLOOD_POINTS(25); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	Command result(this->notify_response_command("notifyconversationindex")); | ||||||
|  | 	size_t result_index = 0; | ||||||
|  | 
 | ||||||
|  | 	auto conversation_manager = ref_server->conversation_manager(); | ||||||
|  | 	for(size_t index = 0; index < cmd.bulkCount(); index++) { | ||||||
|  | 		auto& bulk = cmd[index]; | ||||||
|  | 
 | ||||||
|  | 		if(!bulk.has("cid") || !bulk["cid"].castable<ChannelId>()) | ||||||
|  | 			continue; | ||||||
|  | 		auto conversation_id = bulk["cid"].as<ChannelId>(); | ||||||
|  | 
 | ||||||
|  | 		auto& result_bulk = result[result_index++]; | ||||||
|  | 		result_bulk["cid"] = conversation_id; | ||||||
|  | 
 | ||||||
|  | 		/* test if we have access to the conversation */ | ||||||
|  | 		{ | ||||||
|  | 			/* test if we're able to see the channel */ | ||||||
|  | 			{ | ||||||
|  | 				shared_lock channel_view_lock(this->channel_lock); | ||||||
|  | 				auto channel = this->channel_view()->find_channel(conversation_id); | ||||||
|  | 				if(!channel) { | ||||||
|  | 					auto error = findError("conversation_invalid_id"); | ||||||
|  | 					result_bulk["error_id"] = error.errorId; | ||||||
|  | 					result_bulk["error_msg"] = error.message; | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			/* test if there is a channel password or join power which denies that we see the conversation */ | ||||||
|  | 			{ | ||||||
|  | 				shared_lock channel_view_lock(ref_server->channel_tree_lock); | ||||||
|  | 				auto channel = ref_server->getChannelTree()->findChannel(conversation_id); | ||||||
|  | 				if(!channel) { /* should never happen! */ | ||||||
|  | 					auto error = findError("conversation_invalid_id"); | ||||||
|  | 					result_bulk["error_id"] = error.errorId; | ||||||
|  | 					result_bulk["error_msg"] = error.message; | ||||||
|  | 					continue; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if(!bulk.has("cpw")) | ||||||
|  | 					bulk["cpw"] = ""; | ||||||
|  | 
 | ||||||
|  | 				if (!channel->passwordMatch(bulk["cpw"], true)) | ||||||
|  | 					if (!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_join_ignore_password, 1, channel, true)) { | ||||||
|  | 						auto error = findError("channel_invalid_password"); | ||||||
|  | 						result_bulk["error_id"] = error.errorId; | ||||||
|  | 						result_bulk["error_msg"] = error.message; | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 				if(!this->permissionGranted(permission::PERMTEST_ORDERED, permission::b_channel_ignore_join_power, 1, channel, true)) { | ||||||
|  | 					auto permission_granted = this->calculate_permission_value(permission::i_channel_join_power, channel->channelId()); | ||||||
|  | 					if(!channel->permission_granted(permission::i_channel_needed_join_power, permission_granted, false)) { | ||||||
|  | 						auto error = findError("server_insufficeient_permissions"); | ||||||
|  | 						result_bulk["error_id"] = error.errorId; | ||||||
|  | 						result_bulk["error_msg"] = error.message; | ||||||
|  | 						result_bulk["failed_permid"] = permission::i_channel_join_power; | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		auto conversation = conversation_manager->get(conversation_id); | ||||||
|  | 		if(conversation) | ||||||
|  | 			result_bulk["timestamp"] = duration_cast<milliseconds>(conversation->last_message().time_since_epoch()).count(); | ||||||
|  | 		else | ||||||
|  | 			result_bulk["timestamp"] = 0; | ||||||
|  | 	} | ||||||
|  | 	if(result_index == 0) | ||||||
|  | 		return {ErrorType::DBEmpty}; | ||||||
|  | 	this->sendCommand(result); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 	return CommandResult::Success; | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,11 +1,14 @@ | |||||||
| #include <set> | #include <set> | ||||||
| #include <src/client/music/MusicClient.h> | #include <iomanip> | ||||||
| #include <src/InstanceHandler.h> |  | ||||||
| #include <src/music/MusicBotManager.h> |  | ||||||
| #include <log/LogUtils.h> |  | ||||||
| #include <netinet/in.h> | #include <netinet/in.h> | ||||||
| #include "ConnectedClient.h" | #include <log/LogUtils.h> | ||||||
| #include "src/client/voice/VoiceClient.h" | 
 | ||||||
|  | #include "../InstanceHandler.h" | ||||||
|  | #include "../manager/ConversationManager.h" | ||||||
|  | #include "../music/MusicBotManager.h" | ||||||
|  | #include "../client/music/MusicClient.h" | ||||||
|  | #include "../client/voice/VoiceClient.h" | ||||||
|  | #include "./ConnectedClient.h" | ||||||
| 
 | 
 | ||||||
| using namespace ts; | using namespace ts; | ||||||
| using namespace ts::server; | using namespace ts::server; | ||||||
| @ -82,6 +85,8 @@ inline std::string bot_volume(float vol) { | |||||||
| bool ConnectedClient::handleTextMessage(ChatMessageMode mode, std::string text, const std::shared_ptr<ConnectedClient>& target) { | bool ConnectedClient::handleTextMessage(ChatMessageMode mode, std::string text, const std::shared_ptr<ConnectedClient>& target) { | ||||||
|     if (text.length() < ts::config::music::command_prefix.length()) return false; |     if (text.length() < ts::config::music::command_prefix.length()) return false; | ||||||
|     if (text.find(ts::config::music::command_prefix) != 0) return false; |     if (text.find(ts::config::music::command_prefix) != 0) return false; | ||||||
|  |     if(!this->currentChannel) | ||||||
|  |         return false; | ||||||
| 
 | 
 | ||||||
|     std::string command = text.substr(ts::config::music::command_prefix.length()); |     std::string command = text.substr(ts::config::music::command_prefix.length()); | ||||||
|     auto arguments = command.find(' ') != -1 ? split(command.substr(command.find(' ') + 1), " ") : deque<string>{}; |     auto arguments = command.find(' ') != -1 ? split(command.substr(command.find(' ') + 1), " ") : deque<string>{}; | ||||||
| @ -669,6 +674,40 @@ bool ConnectedClient::handle_text_command( | |||||||
|         threads::self::sleep_until(end); |         threads::self::sleep_until(end); | ||||||
|         send_message(_this.lock(), "Done!"); |         send_message(_this.lock(), "Done!"); | ||||||
|         return true; |         return true; | ||||||
|  |     } else if(command == "conversation") { | ||||||
|  |         if(TARG(0, "history")) { | ||||||
|  |             system_clock::time_point timestamp_begin = system_clock::now(); | ||||||
|  |             system_clock::time_point timestamp_end; | ||||||
|  |             size_t message_count = 100; | ||||||
|  | 
 | ||||||
|  |             if(arguments.size() > 1) { | ||||||
|  |                 timestamp_begin -= seconds(stoll(arguments[1])); | ||||||
|  |             } | ||||||
|  |             if(arguments.size() > 2) { | ||||||
|  |                 timestamp_end = system_clock::now() - seconds(stoll(arguments[2])); | ||||||
|  |             } | ||||||
|  |             if(arguments.size() > 3) { | ||||||
|  |                 message_count = stoll(arguments[3]); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             auto time_str = [](const system_clock::time_point& tp) { | ||||||
|  |                 using system_clock_duration = std::chrono::system_clock::duration; | ||||||
|  |                 auto converted_timep = std::chrono::time_point_cast<system_clock_duration>(tp); | ||||||
|  |                 auto seconds_since_epoch = std::chrono::system_clock::to_time_t(tp); | ||||||
|  | 
 | ||||||
|  |                 ostringstream os; | ||||||
|  |                 os << std::put_time(std::localtime(&seconds_since_epoch), "%Y %b %d %H:%M:%S"); | ||||||
|  |                 return os.str(); | ||||||
|  |             }; | ||||||
|  |             send_message(_this.lock(), "Looking up history from " + time_str(timestamp_end) + " to " + time_str(timestamp_begin) + ". Max messages: " + to_string(message_count)); | ||||||
|  |             auto conversation = this->server->conversation_manager()->get_or_create(this->currentChannel->channelId()); | ||||||
|  |             auto data = conversation->message_history(timestamp_begin, message_count, timestamp_end); | ||||||
|  |             send_message(_this.lock(), "Entries: " + to_string(data.size())); | ||||||
|  |             for(auto& entry : data) { | ||||||
|  |                 send_message(_this.lock(), "<" + time_str(entry->message_timestamp) + ">" + entry->sender_name + ": " + entry->message); | ||||||
|  |             } | ||||||
|  |             return true; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     send_message(serverInstance->musicRoot(), "Invalid channel command."); |     send_message(serverInstance->musicRoot(), "Invalid channel command."); | ||||||
|  | |||||||
| @ -38,8 +38,6 @@ namespace ts { | |||||||
| 		        bool ignoresFlood() override; | 		        bool ignoresFlood() override; | ||||||
| 
 | 
 | ||||||
| 		        inline std::shared_ptr<QueryAccount> getQueryAccount() { return this->query_account; } | 		        inline std::shared_ptr<QueryAccount> getQueryAccount() { return this->query_account; } | ||||||
| 
 |  | ||||||
| 		        std::shared_recursive_mutex server_lock; |  | ||||||
| 	        protected: | 	        protected: | ||||||
|                 void preInitialize(); |                 void preInitialize(); | ||||||
|                 void postInitialize(); |                 void postInitialize(); | ||||||
|  | |||||||
| @ -318,7 +318,6 @@ CommandResult QueryClient::handleCommandServerSelect(Command &cmd) { | |||||||
| 
 | 
 | ||||||
|     //register at current server
 |     //register at current server
 | ||||||
| 	{ | 	{ | ||||||
| 		unique_lock server_lock(this->server_lock); |  | ||||||
| 		this->server = target; | 		this->server = target; | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										975
									
								
								server/src/manager/ConversationManager.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										975
									
								
								server/src/manager/ConversationManager.cpp
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,975 @@ | |||||||
|  | #include "./ConversationManager.h" | ||||||
|  | #include "../InstanceHandler.h" | ||||||
|  | #include "../TSServer.h" | ||||||
|  | 
 | ||||||
|  | #include <experimental/filesystem> | ||||||
|  | #include <log/LogUtils.h> | ||||||
|  | #include <misc/sassert.h> | ||||||
|  | 
 | ||||||
|  | /* for the file */ | ||||||
|  | #include <sys/types.h> | ||||||
|  | #include <unistd.h> | ||||||
|  | 
 | ||||||
|  | using namespace std; | ||||||
|  | using namespace std::chrono; | ||||||
|  | using namespace ts; | ||||||
|  | using namespace ts::server; | ||||||
|  | using namespace ts::server::conversation; | ||||||
|  | 
 | ||||||
|  | namespace fs = std::experimental::filesystem; | ||||||
|  | 
 | ||||||
|  | Conversation::Conversation(const std::shared_ptr<ts::server::conversation::ConversationManager> &handle, ts::ChannelId channel_id, const std::string& file) : _ref_handle(handle), _channel_id(channel_id), file_name(file) { } | ||||||
|  | 
 | ||||||
|  | Conversation::~Conversation() { | ||||||
|  | 	this->finalize(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool Conversation::initialize(std::string& error) { | ||||||
|  | 	auto ref_self = this->_ref_self.lock(); | ||||||
|  | 	assert(ref_self); | ||||||
|  | 
 | ||||||
|  | 	auto handle = this->_ref_handle.lock(); | ||||||
|  | 	assert(handle); | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = handle->ref_server(); | ||||||
|  | 	if(!ref_server) { | ||||||
|  | 		error = "invalid server ref"; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	auto file = fs::u8path(this->file_name); | ||||||
|  | 	if(!fs::is_directory(file.parent_path())) { | ||||||
|  | 		debugMessage(ref_server->getServerId(), "[Conversations] Creating conversation containing directory {}", file.parent_path().string()); | ||||||
|  | 		try { | ||||||
|  | 			fs::create_directories(file.parent_path()); | ||||||
|  | 		} catch(fs::filesystem_error& ex) { | ||||||
|  | 			error = "failed to create data directories (" + to_string(ex.code().value()) + "|" + string(ex.what()) + ")"; | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	this->file_handle = fopen(this->file_name.c_str(), fs::exists(file) ? "r+" : "w+"); | ||||||
|  | 	if(!this->file_handle) { | ||||||
|  | 		error = "failed to open file"; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 	setbuf(this->file_handle, nullptr); /* we're doing random access (a buffer is useless here) */ | ||||||
|  | 
 | ||||||
|  | 	auto sql = ref_server->getSql(); | ||||||
|  | 
 | ||||||
|  | 	auto result = sql::command(sql, "SELECT `begin_timestamp`, `end_timestamp`, `block_offset`, `flags` FROM `conversation_blocks` WHERE `server_id` = :sid AND `conversation_id` = :cid", | ||||||
|  | 	             variable{":sid", ref_server->getServerId()}, variable{":cid", this->_channel_id}).query([&](int count, std::string* values, std::string* names) { | ||||||
|  | 		std::chrono::system_clock::time_point begin_timestamp{}, end_timestamp{}; | ||||||
|  | 		uint64_t block_offset = 0; | ||||||
|  | 		uint16_t flags = 0; | ||||||
|  | 
 | ||||||
|  | 		try { | ||||||
|  | 			for(int index = 0; index < count; index++) { | ||||||
|  | 				if(names[index] == "begin_timestamp") | ||||||
|  | 					begin_timestamp += milliseconds(stoll(values[index])); | ||||||
|  | 				else if(names[index] == "end_timestamp") | ||||||
|  | 					end_timestamp += milliseconds(stoll(values[index])); | ||||||
|  | 				else if(names[index] == "block_offset") | ||||||
|  | 					block_offset = stoull(values[index]); | ||||||
|  | 				else if(names[index] == "flags") | ||||||
|  | 					flags = (uint16_t) stoll(values[index]); | ||||||
|  | 			} | ||||||
|  | 		} catch(std::exception& ex) { | ||||||
|  | 			logError(ref_server->getServerId(), "[Conversations] Failed to parse conversation block entry! Exception: {}", ex.what()); | ||||||
|  | 			return 0; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		auto block = make_shared<db::MessageBlock>(db::MessageBlock{ | ||||||
|  | 				begin_timestamp, | ||||||
|  | 				end_timestamp, | ||||||
|  | 
 | ||||||
|  | 				block_offset, | ||||||
|  | 				{flags}, | ||||||
|  | 
 | ||||||
|  | 				nullptr, | ||||||
|  | 				nullptr | ||||||
|  | 		}); | ||||||
|  | 
 | ||||||
|  | 		/* we dont load invalid blocks */ | ||||||
|  | 		if(block->flag_invalid) | ||||||
|  | 			return 0; | ||||||
|  | 
 | ||||||
|  | 		this->message_blocks.push_back(block); | ||||||
|  | 		return 0; | ||||||
|  | 	}); | ||||||
|  | 	LOG_SQL_CMD(result); | ||||||
|  | 
 | ||||||
|  | 	/* lets find the last block */ | ||||||
|  | 	if(!this->message_blocks.empty()) { | ||||||
|  | 		debugMessage(ref_server->getServerId(), "[Conversations][{}] Loaded {} blocks. Trying to find last block.", this->_channel_id, this->message_blocks.size()); | ||||||
|  | 		deque<shared_ptr<db::MessageBlock>> open_blocks; | ||||||
|  | 		for(auto& block : this->message_blocks) | ||||||
|  | 			if(!block->flag_finished) | ||||||
|  | 				open_blocks.push_back(block); | ||||||
|  | 
 | ||||||
|  | 		logTrace(ref_server->getServerId(), "[Conversations][{}] Found {} unfinished blocks. Searching for the \"latest\" and closing all previous blocks.", this->_channel_id, open_blocks.size()); | ||||||
|  | 		shared_ptr<db::MessageBlock> latest_block; | ||||||
|  | 		const auto calculate_latest_block = [&](bool open_only) { | ||||||
|  | 			latest_block = nullptr; | ||||||
|  | 			for(auto& block : open_blocks) { | ||||||
|  | 				if(block->flag_invalid) | ||||||
|  | 					continue; | ||||||
|  | 
 | ||||||
|  | 				if(!latest_block || latest_block->begin_timestamp < block->begin_timestamp) { | ||||||
|  | 					if(latest_block) { | ||||||
|  | 						latest_block->flag_finished_later = true; | ||||||
|  | 						latest_block->flag_finished = true; | ||||||
|  | 						this->db_save_block(latest_block); | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					latest_block = block; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		}; | ||||||
|  | 		calculate_latest_block(true); | ||||||
|  | 
 | ||||||
|  | 		if(latest_block) { | ||||||
|  | 			logTrace(ref_server->getServerId(), "[Conversations][{}] Found a latest block at index {}. Verify block with file.", this->_channel_id, latest_block->block_offset); | ||||||
|  | 
 | ||||||
|  | 			const auto verify_block = [&] { | ||||||
|  | 				auto result = fseek(this->file_handle, 0, SEEK_END); | ||||||
|  | 				if(result != 0) { | ||||||
|  | 					error = "failed to seek to the end (" + to_string(result) + " | " + to_string(errno) + ")"; | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				auto file_size = ftell(this->file_handle); | ||||||
|  | 				if(file_size < 0) { | ||||||
|  | 					error = "failed to tell the end position (" + to_string(file_size) + " | " + to_string(errno) + ")"; | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 				logTrace(ref_server->getServerId(), "[Conversations][{}] File total size {}, last block index {}", this->_channel_id, file_size, latest_block->block_offset); | ||||||
|  | 				if(file_size < (latest_block->block_offset + sizeof(fio::BlockHeader))) { | ||||||
|  | 					latest_block->flag_finished_later = true; | ||||||
|  | 					latest_block->flag_invalid = true; | ||||||
|  | 					this->finish_block(latest_block, false); | ||||||
|  | 
 | ||||||
|  | 					logTrace(ref_server->getServerId(), "[Conversations][{}] File total size is less than block requires. Appending a new block at the end of the file.", this->_channel_id, latest_block->block_offset); | ||||||
|  | 					latest_block = nullptr; | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				if(!this->load_message_block_header(latest_block, error)) { | ||||||
|  | 					latest_block->flag_finished_later = true; | ||||||
|  | 					latest_block->flag_invalid = true; | ||||||
|  | 					this->finish_block(latest_block, false); | ||||||
|  | 
 | ||||||
|  | 					logTrace(ref_server->getServerId(), "[Conversations][{}] Failed to load latest block at file index {}: {}. Appending an new one.", this->_channel_id, latest_block->block_offset, error); | ||||||
|  | 					error = ""; | ||||||
|  | 					latest_block = nullptr; | ||||||
|  | 					return; | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				/* We've a valid last block. Now the general write function could decide if we want a new block. */ | ||||||
|  | 				this->last_message_block = latest_block; | ||||||
|  | 				logTrace(ref_server->getServerId(), "[Conversations][{}] Last db saved block valid. Reusing it.", this->_channel_id, latest_block->block_offset, error); | ||||||
|  | 			}; | ||||||
|  | 			verify_block(); | ||||||
|  | 			if(!error.empty()) { | ||||||
|  | 				latest_block->flag_finished_later = true; | ||||||
|  | 				latest_block->flag_invalid = true; | ||||||
|  | 				this->finish_block(latest_block, false); | ||||||
|  | 
 | ||||||
|  | 				logError(ref_server->getServerId(), "[Conversations][{}] Could not verify last block. Appending a new one at the end of the file.", this->_channel_id); | ||||||
|  | 				latest_block = nullptr; | ||||||
|  | 			} | ||||||
|  | 			error = ""; | ||||||
|  | 		} else { | ||||||
|  | 			logTrace(ref_server->getServerId(), "[Conversations][{}] Found no open last block. Using a new one.", this->_channel_id); | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		debugMessage(ref_server->getServerId(), "[Conversations][{}] Found no blocks. Creating new at the end of the file.", this->_channel_id, this->message_blocks.size()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	std::stable_sort(this->message_blocks.begin(), this->message_blocks.end(), [](const shared_ptr<db::MessageBlock>& a, const shared_ptr<db::MessageBlock>& b) { return a->begin_timestamp < b->begin_timestamp; }); | ||||||
|  | 	this->_write_event = make_shared<event::ProxiedEventEntry<Conversation>>(ts::event::ProxiedEventEntry<Conversation>(ref_self, &Conversation::process_write_queue)); | ||||||
|  | 
 | ||||||
|  | 	/* set the last message timestamp property */ | ||||||
|  | 	{ | ||||||
|  | 		auto last_message = this->message_history(1); | ||||||
|  | 		if(!last_message.empty()) | ||||||
|  | 			this->_last_message_timestamp = last_message.back()->message_timestamp; | ||||||
|  | 		else | ||||||
|  | 			this->_last_message_timestamp = system_clock::time_point{}; | ||||||
|  | 	} | ||||||
|  | 	return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Conversation::finalize() { | ||||||
|  | 	this->_write_event = nullptr; /* we dont want to schedule/execute new events! */ | ||||||
|  | 	this->_write_loop_lock.lock(); /* wait until current write proceed */ | ||||||
|  | 	this->_write_loop_lock.unlock(); | ||||||
|  | 
 | ||||||
|  | 	//TODO: May flush?
 | ||||||
|  | 
 | ||||||
|  | 	{ | ||||||
|  | 		lock_guard lock(this->message_block_lock); | ||||||
|  | 		this->message_blocks.clear(); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Conversation::cleanup_cache() { | ||||||
|  | 	//FIXME: Implement this shit here!
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ssize_t Conversation::fread(void *target, size_t length, ssize_t index) { | ||||||
|  | 	if(length == 0) | ||||||
|  | 		return 0; | ||||||
|  | 
 | ||||||
|  | 	lock_guard file_lock(this->file_handle_lock); | ||||||
|  | 	if(index >= 0) { | ||||||
|  | 		auto result = fseek(this->file_handle, index, SEEK_SET); | ||||||
|  | 		if(result < 0) | ||||||
|  | 			return -2; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	size_t total_read = 0; | ||||||
|  | 	while(total_read < length) { | ||||||
|  | 		auto read = ::fread_unlocked((char*) target + total_read, 1, length - total_read, this->file_handle); | ||||||
|  | 		if(read <= 0) | ||||||
|  | 			return -1; | ||||||
|  | 		total_read += read; | ||||||
|  | 	} | ||||||
|  | 	return total_read; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ssize_t Conversation::fwrite(void *target, size_t length, ssize_t index, bool extend_file) { | ||||||
|  | 	if(length == 0) | ||||||
|  | 		return 0; | ||||||
|  | 
 | ||||||
|  | 	extend_file = false; /* fseek does the job good ad well */ | ||||||
|  | 	lock_guard file_lock(this->file_handle_lock); | ||||||
|  | 	if(index >= 0) { | ||||||
|  | 		auto result = extend_file ? lseek(fileno(this->file_handle), index, SEEK_SET) : fseek(this->file_handle, index, SEEK_SET); | ||||||
|  | 		if(result < 0) | ||||||
|  | 			return -2; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	size_t total_written = 0; | ||||||
|  | 	while(total_written < length) { | ||||||
|  | 		auto written = ::fwrite_unlocked((char*) target + total_written, 1, length - total_written, this->file_handle); | ||||||
|  | 		if(written <= 0) | ||||||
|  | 			return -1; | ||||||
|  | 		total_written += written; | ||||||
|  | 	} | ||||||
|  | 	return total_written; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool Conversation::load_message_block_header(const std::shared_ptr<ts::server::conversation::db::MessageBlock> &block, std::string &error) { | ||||||
|  | 	if(block->block_header) | ||||||
|  | 		return true; | ||||||
|  | 
 | ||||||
|  | 	auto block_header = make_unique<fio::BlockHeader>(); | ||||||
|  | 	if(this->fread(&*block_header, sizeof(*block_header), block->block_offset) != sizeof(*block_header)) { | ||||||
|  | 		error = "failed to read block header"; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 	if(block_header->version != 1) { | ||||||
|  | 		error = "block version missmatch (block version: " + to_string(block_header->version) + ", current version: 1)"; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if(block_header->cookie != fio::BlockHeader::HEADER_COOKIE) { | ||||||
|  | 		error = "block cookie missmatch"; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	block->block_header = move(block_header); | ||||||
|  | 	return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool Conversation::load_message_block_index(const std::shared_ptr<ts::server::conversation::db::MessageBlock> &block, std::string& error) { | ||||||
|  | 	if(block->indexed_block) | ||||||
|  | 		return true; | ||||||
|  | 
 | ||||||
|  | 	auto index = make_shared<fio::IndexedBlock>(); | ||||||
|  | 	index->successfully = false; | ||||||
|  | 	{ | ||||||
|  | 		if(!this->load_message_block_header(block, error)) { | ||||||
|  | 			error = "failed to load block header: " + error; | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 		auto block_header = block->block_header; | ||||||
|  | 		if(!block_header) { | ||||||
|  | 			error = "failed to reference block header "; | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if(block_header->block_size > fio::BlockHeader::MAX_BLOCK_SIZE) { | ||||||
|  | 			error = "block contains too many messages (" + to_string(block_header->block_size) + ")"; | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		size_t offset = block->block_offset; | ||||||
|  | 		offset += sizeof(fio::BlockHeader); | ||||||
|  | 		size_t max_offset = block->block_offset + block_header->block_size; /* block_size := Written size, must be smaller or equal to the max size, except max size is 0 */ | ||||||
|  | 
 | ||||||
|  | 		fio::MessageHeader header{}; | ||||||
|  | 		while(offset < max_offset) { | ||||||
|  | 			if(this->fread(&header, sizeof(header), offset) != sizeof(header)) { | ||||||
|  | 				error = "failed to read message header at index" + to_string(offset); | ||||||
|  | 				return false; | ||||||
|  | 			} | ||||||
|  | 			if(header.cookie != fio::MessageHeader::HEADER_COOKIE) { | ||||||
|  | 				error = "failed to verify message header cookie at index " + to_string(offset); | ||||||
|  | 				return false; | ||||||
|  | 			} | ||||||
|  | 			index->message_index.emplace_back(fio::IndexedMessage{(uint32_t) (offset - block->block_offset), system_clock::time_point{} + milliseconds{header.message_timestamp}, std::shared_ptr<fio::IndexedMessageData>{nullptr}}); | ||||||
|  | 			offset += header.total_length; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	block->indexed_block = index; | ||||||
|  | 	return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool Conversation::load_messages(const std::shared_ptr<db::MessageBlock> &block, size_t index, size_t end_index, std::string &error) { | ||||||
|  | 	if(!this->load_message_block_index(block, error)) { | ||||||
|  | 		error = "failed to index block: " + error; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	auto indexed_block = block->indexed_block; | ||||||
|  | 	auto header = block->block_header; | ||||||
|  | 	if(!indexed_block || !header) { | ||||||
|  | 		error = "failed to reference required data"; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	/* Note: We dont lock message_index_lock here because the write thread only increases the list and dont take stuff away where we could pointing at! */ | ||||||
|  | 	if(index >= indexed_block->message_index.size()) | ||||||
|  | 		return true; | ||||||
|  | 
 | ||||||
|  | 	auto result = fseek(this->file_handle, block->block_offset + indexed_block->message_index[index].offset, SEEK_SET); | ||||||
|  | 	if(result == EINVAL) { | ||||||
|  | 		error = "failed to seek to begin of an indexed block read"; | ||||||
|  | 		return false; | ||||||
|  | 	} | ||||||
|  | 	while(index < end_index && index < indexed_block->message_index.size()) { | ||||||
|  | 		auto& message_data = indexed_block->message_index[index]; | ||||||
|  | 		if(message_data.message_data) { | ||||||
|  | 			index++; | ||||||
|  | 			continue; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		auto data = make_shared<fio::IndexedMessageData>(); | ||||||
|  | 		if(this->fread(&data->header, sizeof(data->header), -1) != sizeof(data->header)) { | ||||||
|  | 			error = "failed to read message header at index " + to_string(index); | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if(data->header.cookie != fio::MessageHeader::HEADER_COOKIE) { | ||||||
|  | 			error = "failed to verify message header at " + to_string(index); | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		data->sender_unique_id.resize(data->header.sender_unique_id_length); | ||||||
|  | 		data->sender_name.resize(data->header.sender_name_length); | ||||||
|  | 		data->message.resize(data->header.message_length); | ||||||
|  | 
 | ||||||
|  | 		if(this->fread(data->sender_unique_id.data(), data->sender_unique_id.length(), -1) != data->sender_unique_id.length()) { | ||||||
|  | 			error = "failed to read message sender unique id at " + to_string(index); | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if(this->fread(data->sender_name.data(), data->sender_name.length(), -1) != data->sender_name.length()) { | ||||||
|  | 			error = "failed to read message sender name id at " + to_string(index); | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if(this->fread(data->message.data(), data->message.length(), -1) != data->message.length()) { | ||||||
|  | 			error = "failed to read message id at " + to_string(index); | ||||||
|  | 			return false; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if(header->message_encrypted) { | ||||||
|  | 			uint64_t crypt_key = block->block_offset ^ data->header.message_timestamp; | ||||||
|  | 			size_t length_left = data->message.size(); | ||||||
|  | 			auto ptr = (char*) data->message.data(); | ||||||
|  | 			while(length_left >= 8) { | ||||||
|  | 				crypt_key ^= (crypt_key << (length_left & 0x7)) ^ length_left; | ||||||
|  | 				*(uint64_t*) ptr ^= crypt_key; | ||||||
|  | 				ptr += 8; | ||||||
|  | 				length_left -= 8; | ||||||
|  | 			} | ||||||
|  | 			while(length_left > 0) { | ||||||
|  | 				crypt_key ^= (crypt_key << (length_left & 0x7)) ^ length_left; | ||||||
|  | 				*ptr ^= (uint8_t) crypt_key; | ||||||
|  | 				length_left--; | ||||||
|  | 				ptr++; | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		message_data.message_data = data; | ||||||
|  | 		index++; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return true; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Conversation::finish_block(const std::shared_ptr<ts::server::conversation::db::MessageBlock> &block, bool write_file) { | ||||||
|  | 	if(block->flag_finished) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	auto handle = this->_ref_handle.lock(); | ||||||
|  | 	sassert(handle); | ||||||
|  | 	if(!handle) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = handle->ref_server(); | ||||||
|  | 	if(!ref_server) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	block->flag_finished = true; | ||||||
|  | 
 | ||||||
|  | 	if(write_file) { | ||||||
|  | 		string error; | ||||||
|  | 		auto result = this->load_message_block_header(block, error); | ||||||
|  | 		auto header = block->block_header; | ||||||
|  | 		result &= !!header; /* only success if we really have a header */ | ||||||
|  | 		if(result) { | ||||||
|  | 			if(header->block_max_size == 0) { | ||||||
|  | 				header->block_max_size = header->block_size; | ||||||
|  | 				header->finished = true; | ||||||
|  | 				if(!this->write_block_header(header, block->block_offset, error)) { | ||||||
|  | 					logError(ref_server->getServerId(), "Failed to finish block because block header could not be written: {}", error); | ||||||
|  | 					block->flag_invalid = true; /* because we cant set the block size we've to declare that block as invalid */ | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			logError(ref_server->getServerId(), "Failed to finish block because block header could not be set: {}", error); | ||||||
|  | 			block->flag_invalid = true; /* because we cant set the block size we've to declare that block as invalid */ | ||||||
|  | 		} | ||||||
|  | 	} else { | ||||||
|  | 		block->flag_invalid = true; /* because we dont write the block we cant ensure a valid block */ | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	this->db_save_block(block); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool Conversation::write_block_header(const std::shared_ptr<fio::BlockHeader> &header, size_t index, std::string &error) { | ||||||
|  | 	auto code = this->fwrite(&*header, sizeof(fio::BlockHeader), index, false); | ||||||
|  | 	if(code == sizeof(fio::BlockHeader)) | ||||||
|  | 		return true; | ||||||
|  | 	error = "write returned " + to_string(code); | ||||||
|  | 	return false; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Conversation::process_write_queue(const std::chrono::system_clock::time_point &scheduled_time) { | ||||||
|  | 	unique_lock write_lock(this->_write_loop_lock, try_to_lock); | ||||||
|  | 	if(!write_lock.owns_lock()) /* we're already writing if this lock fails */ | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	unique_lock write_queue_lock(this->_write_queue_lock, defer_lock); | ||||||
|  | 	std::shared_ptr<ConversationEntry> write_entry; | ||||||
|  | 	fio::MessageHeader write_header{}; | ||||||
|  | 	std::shared_ptr<fio::BlockHeader> block_header; | ||||||
|  | 	auto handle = this->_ref_handle.lock(); | ||||||
|  | 	sassert(handle); | ||||||
|  | 	if(!handle) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = handle->ref_server(); | ||||||
|  | 	if(!ref_server) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	write_header.cookie = fio::MessageHeader::HEADER_COOKIE; | ||||||
|  | 	write_header.message_flags = 0; | ||||||
|  | 
 | ||||||
|  | 	while(true) { | ||||||
|  | 		write_queue_lock.lock(); | ||||||
|  | 		if(this->_write_queue.empty()) | ||||||
|  | 			break; | ||||||
|  | 		write_entry = this->_write_queue.front(); | ||||||
|  | 		this->_write_queue.pop_front(); | ||||||
|  | 		write_queue_lock.unlock(); | ||||||
|  | 
 | ||||||
|  | 		/* calculate the write message total length */ | ||||||
|  | 		write_header.message_length =  (uint16_t) min(write_entry->message.size(), (size_t) (65536 - 1)); | ||||||
|  | 		write_header.sender_name_length = (uint8_t) min(write_entry->sender_name.size(), (size_t) 255); | ||||||
|  | 		write_header.sender_unique_id_length = (uint8_t) min(write_entry->sender_unique_id.size(), (size_t) 255); | ||||||
|  | 		write_header.total_length = sizeof(write_header) + write_header.sender_unique_id_length + write_header.sender_name_length + write_header.message_length; | ||||||
|  | 
 | ||||||
|  | 		/* verify last block */ | ||||||
|  | 		if(this->last_message_block) { | ||||||
|  | 			block_header = this->last_message_block->block_header; | ||||||
|  | 
 | ||||||
|  | 			if(!block_header) { | ||||||
|  | 				logError(ref_server->getServerId(), "[Conversations][{}] Current last block contains no header! Try to finish it and creating a new one.", this->_channel_id); | ||||||
|  | 				this->finish_block(this->last_message_block, true); | ||||||
|  | 				this->last_message_block = nullptr; | ||||||
|  | 			} else if(this->last_message_block->flag_finished) | ||||||
|  | 				this->last_message_block = nullptr; | ||||||
|  | 			else { | ||||||
|  | 				if(this->last_message_block->begin_timestamp + hours(24) < scheduled_time) { | ||||||
|  | 					debugMessage(ref_server->getServerId(), "[Conversations][{}] Beginning new block. Old block is older than 24hrs. ({})", this->_channel_id, this->last_message_block->begin_timestamp.time_since_epoch().count()); | ||||||
|  | 					this->finish_block(this->last_message_block, true); | ||||||
|  | 					this->last_message_block = nullptr; | ||||||
|  | 				} else if((block_header->block_max_size != 0 && block_header->block_size + write_header.total_length >= block_header->block_max_size) || block_header->block_size > fio::BlockHeader::MAX_BLOCK_SIZE){ | ||||||
|  | 					debugMessage(ref_server->getServerId(), "[Conversations][{}] Beginning new block. Old block would exceed his space (Current index: {}, Max index: {}, Soft cap: {}, Message size: {}).", | ||||||
|  | 					             this->_channel_id, block_header->block_size, block_header->block_max_size, fio::BlockHeader::MAX_BLOCK_SIZE, write_header.total_length); | ||||||
|  | 					this->finish_block(this->last_message_block, true); | ||||||
|  | 					this->last_message_block = nullptr; | ||||||
|  | 				} else if(block_header->message_version != 1){ | ||||||
|  | 					debugMessage(ref_server->getServerId(), "[Conversations][{}] Beginning new block. Old block had another message version (Current version: {}, Block version: {}).", this->_channel_id, 1, block_header->message_version); | ||||||
|  | 					this->finish_block(this->last_message_block, true); | ||||||
|  | 					this->last_message_block = nullptr; | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		/* test if we have a block or create a new one at the begin of the file */ | ||||||
|  | 		if(!this->last_message_block) { | ||||||
|  | 			//Note: If we reuse blocks we've to reorder them within message_blocks (newest blocks are at the end)
 | ||||||
|  | 			//TODO: Find "free" blocks and use them! (But do not use indirectly finished blocks, their max size could be invalid)
 | ||||||
|  | 
 | ||||||
|  | 			unique_lock file_lock(this->file_handle_lock); | ||||||
|  | 			auto result = fseek(this->file_handle, 0, SEEK_END); | ||||||
|  | 			if(result != 0) { | ||||||
|  | 				logError(ref_server->getServerId(), "[Conversations][{}] failed to seek to the end (" + to_string(result) + " " + to_string(errno) + "). Could not create new block. Dropping message!", this->_channel_id); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			auto file_size = ftell(this->file_handle); | ||||||
|  | 			if(file_size < 0) { | ||||||
|  | 				logError(ref_server->getServerId(), "[Conversations][{}] failed to tell the end position (" + to_string(result) + " " + to_string(errno) + "). Could not create new block. Dropping message!", this->_channel_id); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			file_lock.unlock(); | ||||||
|  | 			this->last_message_block = this->db_create_block((uint64_t) file_size); | ||||||
|  | 			if(!this->last_message_block) { | ||||||
|  | 				logError(ref_server->getServerId(), "[Conversations][{}] Failed to create a new block within database. Dropping message!", this->_channel_id); | ||||||
|  | 				return; | ||||||
|  | 			} | ||||||
|  | 			block_header = make_shared<fio::BlockHeader>(); | ||||||
|  | 			memset(&*block_header, 0, sizeof(fio::BlockHeader)); | ||||||
|  | 
 | ||||||
|  | 			block_header->version = 1; | ||||||
|  | 			block_header->message_version = 1; | ||||||
|  | 			block_header->cookie = fio::BlockHeader::HEADER_COOKIE; | ||||||
|  | 			block_header->first_message_timestamp = (uint64_t) duration_cast<milliseconds>(write_entry->message_timestamp.time_since_epoch()).count(); | ||||||
|  | 			block_header->block_size = sizeof(fio::BlockHeader); | ||||||
|  | 
 | ||||||
|  | 			//block_header->message_encrypted = true; /* May add some kind of hidden debug option? */
 | ||||||
|  | 			this->last_message_block->block_header = block_header; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		auto entry_offset = this->last_message_block->block_offset + sizeof(fio::BlockHeader) + block_header->last_message_offset; | ||||||
|  | 		write_header.sender_database_id = write_entry->sender_database_id; | ||||||
|  | 		write_header.message_timestamp = (uint64_t) duration_cast<milliseconds>(write_entry->message_timestamp.time_since_epoch()).count(); | ||||||
|  | 		block_header->last_message_timestamp = write_header.message_timestamp; | ||||||
|  | 
 | ||||||
|  | 		/* first write the header */ | ||||||
|  | 		if(this->fwrite(&write_header, sizeof(write_header), entry_offset, true) != sizeof(write_header)) { | ||||||
|  | 			logError(ref_server->getServerId(), "[Conversations][{}] Failed to write message header. Dropping message!", this->_channel_id); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		entry_offset += sizeof(write_header); | ||||||
|  | 
 | ||||||
|  | 		/* then write the sender unique id */ | ||||||
|  | 		if(this->fwrite(write_entry->sender_unique_id.data(), write_header.sender_unique_id_length, entry_offset, true) != write_header.sender_unique_id_length) { | ||||||
|  | 			logError(ref_server->getServerId(), "[Conversations][{}] Failed to write message sender unique id. Dropping message!", this->_channel_id); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		entry_offset += write_header.sender_unique_id_length; | ||||||
|  | 
 | ||||||
|  | 		/* then write the sender name */ | ||||||
|  | 		if(this->fwrite(write_entry->sender_name.data(), write_header.sender_name_length, entry_offset, true) != write_header.sender_name_length) { | ||||||
|  | 			logError(ref_server->getServerId(), "[Conversations][{}] Failed to write message sender name. Dropping message!", this->_channel_id); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		entry_offset += write_header.sender_name_length; | ||||||
|  | 
 | ||||||
|  | 		/* then write the message */ | ||||||
|  | 		bool message_result; | ||||||
|  | 		if(block_header->message_encrypted) { | ||||||
|  | 			uint64_t crypt_key = this->last_message_block->block_offset ^ write_header.message_timestamp; | ||||||
|  | 			size_t length_left = write_entry->message.size(); | ||||||
|  | 			auto ptr = (char*) write_entry->message.data(); | ||||||
|  | 			char* target_buffer = (char*) malloc(length_left); | ||||||
|  | 			char* target_buffer_ptr = target_buffer; | ||||||
|  | 			assert(target_buffer); | ||||||
|  | 
 | ||||||
|  | 			while(length_left >= 8) { | ||||||
|  | 				crypt_key ^= (crypt_key << (length_left & 0x7)) ^ length_left; | ||||||
|  | 				*(uint64_t*) target_buffer_ptr = crypt_key; | ||||||
|  | 				ptr += 8; | ||||||
|  | 				target_buffer_ptr += 8; | ||||||
|  | 				length_left -= 8; | ||||||
|  | 			} | ||||||
|  | 			while(length_left > 0) { | ||||||
|  | 				crypt_key ^= (crypt_key << (length_left & 0x7)) ^ length_left; | ||||||
|  | 				*target_buffer_ptr = *ptr ^ (uint8_t) crypt_key; | ||||||
|  | 				length_left--; | ||||||
|  | 				ptr++; | ||||||
|  | 				target_buffer_ptr++; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			message_result = this->fwrite(target_buffer, write_header.message_length, entry_offset, true) == write_header.message_length; | ||||||
|  | 			free(target_buffer); | ||||||
|  | 		} else { | ||||||
|  | 			message_result = this->fwrite(write_entry->message.data(), write_header.message_length, entry_offset, true) == write_header.message_length; | ||||||
|  | 		} | ||||||
|  | 		if(!message_result) { | ||||||
|  | 			logError(ref_server->getServerId(), "[Conversations][{}] Failed to write message. Dropping message!", this->_channel_id); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  | 		entry_offset += write_header.message_length; | ||||||
|  | 
 | ||||||
|  | 		block_header->last_message_offset = (uint32_t) (entry_offset - this->last_message_block->block_offset - sizeof(fio::BlockHeader)); | ||||||
|  | 		block_header->block_size += write_header.total_length; | ||||||
|  | 		block_header->message_count += 1; | ||||||
|  | 
 | ||||||
|  | 		auto indexed_block = this->last_message_block->indexed_block; | ||||||
|  | 		if(indexed_block) { | ||||||
|  | 			lock_guard lock(indexed_block->message_index_lock); | ||||||
|  | 			indexed_block->message_index.push_back(fio::IndexedMessage{(uint32_t) (entry_offset - this->last_message_block->block_offset), write_entry->message_timestamp, nullptr}); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if(write_header.total_length != 0) {/* will be set when at least one message has been written */ | ||||||
|  | 		this->db_save_block(this->last_message_block); | ||||||
|  | 
 | ||||||
|  | 		string error; | ||||||
|  | 		if(!this->write_block_header(block_header, this->last_message_block->block_offset, error)) { | ||||||
|  | 			logWarning(ref_server->getServerId(), "[Conversations][{}] Failed to write block header after message write ({}). This could cause data loss!", this->_channel_id, error); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::shared_ptr<db::MessageBlock> Conversation::db_create_block(uint64_t offset) { | ||||||
|  | 	auto result = make_shared<db::MessageBlock>(); | ||||||
|  | 	result->block_offset = offset; | ||||||
|  | 	result->begin_timestamp = system_clock::now(); | ||||||
|  | 	result->end_timestamp = system_clock::now(); | ||||||
|  | 	result->flags = 0; | ||||||
|  | 	result->flag_used = true; | ||||||
|  | 	result->flag_invalid = true; /* first set it to invalid for the database so it becomes active as soon somebody uses it */ | ||||||
|  | 
 | ||||||
|  | 	auto handle = this->_ref_handle.lock(); | ||||||
|  | 	assert(handle); | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = handle->ref_server(); | ||||||
|  | 	if(!ref_server) | ||||||
|  | 		return nullptr; | ||||||
|  | 
 | ||||||
|  | 	auto sql = ref_server->getSql(); | ||||||
|  | 	if(!sql) | ||||||
|  | 		return nullptr; | ||||||
|  | 
 | ||||||
|  | 	//`server_id` INT, `conversation_id` INT, `begin_timestamp` INT, `end_timestamp` INT, `block_offset` INT, `flags` INT
 | ||||||
|  | 	auto sql_result = sql::command(sql, "INSERT INTO `conversation_blocks` (`server_id`, `conversation_id`, `begin_timestamp`, `end_timestamp`, `block_offset`, `flags`) VALUES (:sid, :cid, :bt, :et, :bo, :f);", | ||||||
|  | 			variable{":sid", ref_server->getServerId()}, | ||||||
|  | 			variable{":cid", this->_channel_id}, | ||||||
|  | 			variable{":bt", duration_cast<milliseconds>(result->begin_timestamp.time_since_epoch()).count()}, | ||||||
|  |             variable{":et", duration_cast<milliseconds>(result->end_timestamp.time_since_epoch()).count()}, | ||||||
|  |             variable{":bo", offset}, | ||||||
|  |             variable{":f", result->flags} | ||||||
|  | 	).executeLater(); | ||||||
|  | 	sql_result.waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); | ||||||
|  | 
 | ||||||
|  | 	{ | ||||||
|  | 		lock_guard lock(this->message_block_lock); | ||||||
|  | 		this->message_blocks.push_back(result); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	result->flag_invalid = false; | ||||||
|  | 	return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Conversation::db_save_block(const std::shared_ptr<db::MessageBlock> &block) { | ||||||
|  | 	auto handle = this->_ref_handle.lock(); | ||||||
|  | 	assert(handle); | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = handle->ref_server(); | ||||||
|  | 	if(!ref_server) { | ||||||
|  | 		logWarning(ref_server->getServerId(), "[Conversations][{}] Failed to update block db info (server expired)", this->_channel_id); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	auto sql = ref_server->getSql(); | ||||||
|  | 	if(!sql) { | ||||||
|  | 		logWarning(ref_server->getServerId(), "[Conversations][{}] Failed to update block db info (sql expired)", this->_channel_id); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	auto sql_result = sql::command(sql, "UPDATE `conversation_blocks` SET `end_timestamp` = :et, `flags` = :f WHERE `server_id` = :sid AND `conversation_id` = :cid AND `begin_timestamp` = :bt AND `block_offset` = :bo", | ||||||
|  | 	                               variable{":sid", ref_server->getServerId()}, | ||||||
|  | 	                               variable{":cid", this->_channel_id}, | ||||||
|  | 	                               variable{":bt", duration_cast<milliseconds>(block->begin_timestamp.time_since_epoch()).count()}, | ||||||
|  | 	                               variable{":et", duration_cast<milliseconds>(block->end_timestamp.time_since_epoch()).count()}, | ||||||
|  | 	                               variable{":bo", block->block_offset}, | ||||||
|  | 	                               variable{":f", block->flags} | ||||||
|  | 	).executeLater(); | ||||||
|  | 	sql_result.waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void Conversation::register_message(ts::ClientDbId sender_database_id, const std::string &sender_unique_id, const std::string &sender_name, const std::string &message) { | ||||||
|  | 	auto entry = make_shared<ConversationEntry>(); | ||||||
|  | 	entry->message_timestamp = system_clock::now(); | ||||||
|  | 	this->_last_message_timestamp = entry->message_timestamp; | ||||||
|  | 	entry->message = message; | ||||||
|  | 	entry->sender_name = sender_name; | ||||||
|  | 	entry->sender_unique_id = sender_unique_id; | ||||||
|  | 	entry->sender_database_id = sender_database_id; | ||||||
|  | 
 | ||||||
|  | 	{ | ||||||
|  | 		lock_guard lock(this->_last_messages_lock); | ||||||
|  | 		this->_last_messages.push_back(entry); | ||||||
|  | 		while(this->_last_messages.size() > this->_last_messages_limit) | ||||||
|  | 			this->_last_messages.pop_front(); /* TODO: Use a iterator for delete to improve performance */ | ||||||
|  | 	} | ||||||
|  | 	if(!this->volatile_only()) { | ||||||
|  | 		{ | ||||||
|  | 			lock_guard lock(this->_write_queue_lock); | ||||||
|  | 			this->_write_queue.push_back(entry); | ||||||
|  | 		} | ||||||
|  | 		auto executor = serverInstance->getConversationIo(); | ||||||
|  | 		executor->schedule(this->_write_event); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::deque<std::shared_ptr<ConversationEntry>> Conversation::message_history(const std::chrono::system_clock::time_point &end_timestamp, size_t message_count, const std::chrono::system_clock::time_point &begin_timestamp) { | ||||||
|  | 	std::deque<std::shared_ptr<ConversationEntry>> result; | ||||||
|  | 	if(message_count == 0) | ||||||
|  | 		return result; | ||||||
|  | 
 | ||||||
|  | 	/* first try to fillout the result with the cached messages */ | ||||||
|  | 	{ | ||||||
|  | 		lock_guard lock(this->_last_messages_lock); | ||||||
|  | 		//TODO: May just insert the rest of the iterator instead of looping?
 | ||||||
|  | 		for(auto it = this->_last_messages.rbegin(); it != this->_last_messages.rend(); it++) { | ||||||
|  | 			if((*it)->message_timestamp > end_timestamp) /* message has been send after the search timestamp */ | ||||||
|  | 				continue; | ||||||
|  | 			result.push_back(*it); | ||||||
|  | 			if(--message_count == 0) | ||||||
|  | 				return result; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if(!this->volatile_only()) { | ||||||
|  | 		auto timestamp = result.empty() ? end_timestamp : result.back()->message_timestamp; | ||||||
|  | 
 | ||||||
|  | 		unique_lock lock(this->message_block_lock); | ||||||
|  | 		auto rit = this->message_blocks.end(); | ||||||
|  | 		if(rit != this->message_blocks.begin()) { | ||||||
|  | 			bool found = false; | ||||||
|  | 			do { | ||||||
|  | 				rit--; | ||||||
|  | 				if((*rit)->begin_timestamp < timestamp) { | ||||||
|  | 					found = true; | ||||||
|  | 					break; /* we found the first block which is created before the point we're searching from */ | ||||||
|  | 				} | ||||||
|  | 			} while(rit != this->message_blocks.begin()); | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 			string error; | ||||||
|  | 			if(found) { | ||||||
|  | 				vector<shared_ptr<db::MessageBlock>> relevant_entries{this->message_blocks.begin(), ++rit}; | ||||||
|  | 				lock.unlock(); | ||||||
|  | 
 | ||||||
|  | 				auto _rit = --relevant_entries.end(); | ||||||
|  | 				do { | ||||||
|  | 					auto block = *_rit; | ||||||
|  | 					/* lets search for messages */ | ||||||
|  | 					if(!this->load_message_block_index(block, error)) { | ||||||
|  | 						//TODO: Log error
 | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  | 					auto index = (*_rit)->indexed_block; | ||||||
|  | 					if(!index) { | ||||||
|  | 						//TODO Log error
 | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					lock_guard index_lock{index->message_index_lock}; | ||||||
|  | 					auto rmid = index->message_index.end(); | ||||||
|  | 					if(rmid == index->message_index.begin()) | ||||||
|  | 						continue; /* Empty block? Funny */ | ||||||
|  | 
 | ||||||
|  | 					auto block_found = false; | ||||||
|  | 					do { | ||||||
|  | 						rmid--; | ||||||
|  | 						if((*rmid).timestamp < timestamp) { | ||||||
|  | 							block_found = true; | ||||||
|  | 							break; /* we found the first block which is created before the point we're searching from */ | ||||||
|  | 						} | ||||||
|  | 					} while(rmid != index->message_index.begin()); | ||||||
|  | 					if(!block_found) | ||||||
|  | 						continue; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | 					if(!this->load_messages(block, 0, std::distance(index->message_index.begin(), rmid) + 1, error)) { | ||||||
|  | 						//TODO: Log error
 | ||||||
|  | 						continue; | ||||||
|  | 					} | ||||||
|  | 					do { | ||||||
|  | 						auto data = rmid->message_data; | ||||||
|  | 						if(!data) | ||||||
|  | 							continue; | ||||||
|  | 
 | ||||||
|  | 						if(begin_timestamp.time_since_epoch().count() != 0 && rmid->timestamp < begin_timestamp) | ||||||
|  | 							return result; | ||||||
|  | 						/*
 | ||||||
|  | 							std::chrono::system_clock::time_point message_timestamp; | ||||||
|  | 
 | ||||||
|  | 							ClientDbId  sender_database_id; | ||||||
|  | 							std::string sender_unique_id; | ||||||
|  | 							std::string sender_name; | ||||||
|  | 
 | ||||||
|  | 							std::string message; | ||||||
|  | 						 */ | ||||||
|  | 						result.push_back(make_shared<ConversationEntry>(ConversationEntry{ | ||||||
|  | 								rmid->timestamp, | ||||||
|  | 
 | ||||||
|  | 								(ClientDbId) data->header.sender_database_id, | ||||||
|  | 								data->sender_unique_id, | ||||||
|  | 								data->sender_name, | ||||||
|  | 
 | ||||||
|  | 								data->message | ||||||
|  | 						})); | ||||||
|  | 						if(--message_count == 0) | ||||||
|  | 							return result; | ||||||
|  | 					} while(rmid-- != index->message_index.begin()); | ||||||
|  | 				} while(_rit-- != relevant_entries.begin()); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return result; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ConversationManager::ConversationManager(const std::shared_ptr<ts::server::TSServer> &server) : _ref_server(server) { } | ||||||
|  | 
 | ||||||
|  | ConversationManager::~ConversationManager() { } | ||||||
|  | 
 | ||||||
|  | void ConversationManager::initialize(const std::shared_ptr<ConversationManager> &_this) { | ||||||
|  | 	assert(&*_this == this); | ||||||
|  | 	this->_ref_this = _this; | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = this->ref_server(); | ||||||
|  | 	assert(ref_server); | ||||||
|  | 	auto sql = ref_server->getSql(); | ||||||
|  | 	assert(sql); | ||||||
|  | 
 | ||||||
|  | 	auto result = sql::command(sql, "SELECT `conversation_id`, `file_path` FROM `conversations` WHERE `server_id` = :sid", variable{":sid", ref_server->getServerId()}).query([&](int count, std::string* values, std::string* names) { | ||||||
|  | 		ChannelId conversation_id = 0; | ||||||
|  | 		std::string file_path; | ||||||
|  | 
 | ||||||
|  | 		try { | ||||||
|  | 			for(int index = 0; index < count; index++) { | ||||||
|  | 				if(names[index] == "conversation_id") | ||||||
|  | 					conversation_id += stoll(values[index]); | ||||||
|  | 				else if(names[index] == "file_path") | ||||||
|  | 					file_path += values[index]; | ||||||
|  | 			} | ||||||
|  | 		} catch(std::exception& ex) { | ||||||
|  | 			logError(ref_server->getServerId(), "[Conversations] Failed to parse conversation entry! Exception: {}", ex.what()); | ||||||
|  | 			return 0; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		auto conversation = make_shared<Conversation>(_this, conversation_id, file_path); | ||||||
|  | 		conversation->set_ref_self(conversation); | ||||||
|  | 		string error; | ||||||
|  | 		if(!conversation->initialize(error)) { | ||||||
|  | 			logError(ref_server->getServerId(), "[Conversations] Failed to load conversation for channel {}: {}. Conversation is in volatile mode", conversation_id, error); | ||||||
|  | 		} | ||||||
|  | 		this->_conversations.push_back(conversation); | ||||||
|  | 		return 0; | ||||||
|  | 	}); | ||||||
|  | 	LOG_SQL_CMD(result); | ||||||
|  | 	debugMessage(ref_server->getServerId(), "[Conversations] Loaded {} conversations", this->_conversations.size()); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | bool ConversationManager::conversation_exists(ts::ChannelId channel_id) { | ||||||
|  | 	lock_guard lock(this->_conversations_lock); | ||||||
|  | 	return find_if(this->_conversations.begin(), this->_conversations.end(), [&](const shared_ptr<Conversation>& conv){ return conv->channel_id() == channel_id; })!= this->_conversations.end(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::shared_ptr<Conversation> ConversationManager::get(ts::ChannelId channel_id) { | ||||||
|  | 	unique_lock lock(this->_conversations_lock); | ||||||
|  | 	auto found = find_if(this->_conversations.begin(), this->_conversations.end(), [&](const shared_ptr<Conversation>& conv){ return conv->channel_id() == channel_id; }); | ||||||
|  | 	if(found != this->_conversations.end()) | ||||||
|  | 		return *found; | ||||||
|  | 	return nullptr; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | std::shared_ptr<Conversation> ConversationManager::get_or_create(ts::ChannelId channel_id) { | ||||||
|  | 	unique_lock lock(this->_conversations_lock); | ||||||
|  | 	auto found = find_if(this->_conversations.begin(), this->_conversations.end(), [&](const shared_ptr<Conversation>& conv){ return conv->channel_id() == channel_id; }); | ||||||
|  | 	if(found != this->_conversations.end()) | ||||||
|  | 		return *found; | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = this->ref_server(); | ||||||
|  | 	assert(ref_server); | ||||||
|  | 
 | ||||||
|  | 	//TODO: More configurable!
 | ||||||
|  | 	auto file_path = "files/server_" + to_string(ref_server->getServerId()) + "/conversations/conversation_" + to_string(channel_id) + ".cvs"; | ||||||
|  | 	auto conversation = make_shared<Conversation>(this->_ref_this.lock(), channel_id, file_path); | ||||||
|  | 	conversation->set_ref_self(conversation); | ||||||
|  | 	this->_conversations.push_back(conversation); | ||||||
|  | 	lock.unlock(); | ||||||
|  | 
 | ||||||
|  | 	{ | ||||||
|  | 		auto sql_result = sql::command(ref_server->getSql(), "INSERT INTO `conversations` (`server_id`, `channel_id`, `conversation_id`, `file_path`) VALUES (:sid, :cid, :cid, :fpath)", | ||||||
|  | 				variable{":sid", ref_server->getServerId()}, variable{":cid", channel_id}, variable{":fpath", file_path}).executeLater(); | ||||||
|  | 		sql_result.waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	string error; | ||||||
|  | 	if(!conversation->initialize(error)) | ||||||
|  | 		logError(ref_server->getServerId(), "[Conversations] Failed to load conversation for channel {}: {}. Conversation is in volatile mode", channel_id, error); | ||||||
|  | 	return conversation; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConversationManager::delete_conversation(ts::ChannelId channel_id) { | ||||||
|  | 	{ | ||||||
|  | 		lock_guard lock(this->_conversations_lock); | ||||||
|  | 		this->_conversations.erase(remove_if(this->_conversations.begin(), this->_conversations.end(), [&](const shared_ptr<Conversation>& conv){ return conv->channel_id() == channel_id; }), this->_conversations.end()); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	auto ref_server = this->ref_server(); | ||||||
|  | 	if(!ref_server) { | ||||||
|  | 		logWarning(ref_server->getServerId(), "[Conversations][{}] Failed to delete conversation (server expired)", channel_id); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	auto sql = ref_server->getSql(); | ||||||
|  | 	if(!sql) { | ||||||
|  | 		logWarning(ref_server->getServerId(), "[Conversations][{}] Failed to delete conversation (sql expired)", channel_id); | ||||||
|  | 		return; | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	{ | ||||||
|  | 		auto sql_result = sql::command(sql, "DELETE FROM `conversations` WHERE `server_id` = :sid AND `channel_id` = :cid AND `conversation_id` = :cid", variable{":sid", ref_server->getServerId()}, variable{":cid", channel_id}).executeLater(); | ||||||
|  | 		sql_result.waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); | ||||||
|  | 	} | ||||||
|  | 	{ | ||||||
|  | 		auto sql_result = sql::command(sql, "DELETE FROM `conversation_blocks` WHERE `server_id` = :sid AND `conversation_id` = :cid", variable{":sid", ref_server->getServerId()}, variable{":cid", channel_id}).executeLater(); | ||||||
|  | 		sql_result.waitAndGetLater(LOG_SQL_CMD, {-1, "future error"}); | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	//TODO: More configurable!
 | ||||||
|  | 	auto file_path = "files/server_" + to_string(ref_server->getServerId()) + "/conversations/conversation_" + to_string(channel_id) + ".cvs"; | ||||||
|  | 	try { | ||||||
|  | 		fs::remove(fs::u8path(file_path)); | ||||||
|  | 	} catch(fs::filesystem_error& error) { | ||||||
|  | 		logWarning(ref_server->getServerId(), "[Conversations][{}] Failed to delete data file ({}): {}|{}", channel_id, file_path, error.code().value(), error.what()); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConversationManager::cleanup_channels() { | ||||||
|  | 	//TODO: Check if the channel for the conversation still exists!
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | void ConversationManager::cleanup_cache() { | ||||||
|  | 	unique_lock lock(this->_conversations_lock); | ||||||
|  | 	std::vector<std::shared_ptr<Conversation>> conversations{this->_conversations.begin(), this->_conversations.end()}; | ||||||
|  | 	lock.unlock(); | ||||||
|  | 
 | ||||||
|  | 	for(auto& conversation : conversations) | ||||||
|  | 		conversation->cleanup_cache(); | ||||||
|  | } | ||||||
							
								
								
									
										246
									
								
								server/src/manager/ConversationManager.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										246
									
								
								server/src/manager/ConversationManager.h
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,246 @@ | |||||||
|  | #pragma once | ||||||
|  | 
 | ||||||
|  | #include <memory> | ||||||
|  | #include <cassert> | ||||||
|  | #include <chrono> | ||||||
|  | #include <deque> | ||||||
|  | #include <Definitions.h> | ||||||
|  | #include <mutex> | ||||||
|  | #include <vector> | ||||||
|  | #include <EventLoop.h> | ||||||
|  | 
 | ||||||
|  | namespace ts { | ||||||
|  | 	namespace server { | ||||||
|  | 		class TSServer; | ||||||
|  | 		namespace conversation { | ||||||
|  | 			struct ConversationEntry { | ||||||
|  | 				std::chrono::system_clock::time_point message_timestamp; | ||||||
|  | 
 | ||||||
|  | 				ClientDbId  sender_database_id; | ||||||
|  | 				std::string sender_unique_id; | ||||||
|  | 				std::string sender_name; | ||||||
|  | 
 | ||||||
|  | 				std::string message; | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			namespace fio { | ||||||
|  | 				#pragma pack(push, 1) | ||||||
|  | 				struct BlockHeader { | ||||||
|  | 					static constexpr uint64_t HEADER_COOKIE = 0xC0FFEEBABE; | ||||||
|  | 					static constexpr uint64_t MAX_BLOCK_SIZE = 0xFF00; | ||||||
|  | 
 | ||||||
|  | 					uint8_t version; /* every time 1 */ | ||||||
|  | 					uint64_t cookie /* const 0xC0FFEEBABE */; | ||||||
|  | 					uint8_t message_version; /* every time 1; Version for the containing messages */ | ||||||
|  | 
 | ||||||
|  | 					uint32_t block_size; /* size of the full block (with data) incl. header! */ | ||||||
|  | 					uint32_t block_max_size; /* size of the full block incl. header! 0 if the block is located at the end and could be extended */ | ||||||
|  | 
 | ||||||
|  | 					uint32_t message_count; /* message count */ | ||||||
|  | 					uint32_t last_message_offset; /* offset to the last message. Offset begins after header (first message has offset of 0) */ | ||||||
|  | 					union { | ||||||
|  | 						uint8_t flags; | ||||||
|  | 						struct { | ||||||
|  | 							uint8_t _padding : 5; | ||||||
|  | 
 | ||||||
|  | 							bool message_encrypted: 1; /* 0x04 */ | ||||||
|  | 							bool meta_encrypted: 1; /* 0x02 */ /* Not implemented */ | ||||||
|  | 							bool finished: 1; /* 0x01 */ /* if this block is finally finished; Most the time a next block follows directly */ | ||||||
|  | 						}; | ||||||
|  | 					}; | ||||||
|  | 
 | ||||||
|  | 					uint64_t first_message_timestamp; | ||||||
|  | 					uint64_t last_message_timestamp; | ||||||
|  | 				}; | ||||||
|  | 				static_assert(__BYTE_ORDER == __LITTLE_ENDIAN); | ||||||
|  | 				static_assert(sizeof(BlockHeader) == 43); | ||||||
|  | 
 | ||||||
|  | 				struct MessageHeader { | ||||||
|  | 					static constexpr uint16_t HEADER_COOKIE = 0xAFFE; | ||||||
|  | 					uint16_t cookie; /* const 0xAFFE */ | ||||||
|  | 					uint16_t total_length; /* Total length of the full message data. Includes this header! */ | ||||||
|  | 					uint64_t message_timestamp; /* milliseconds since epoch */ | ||||||
|  | 					uint64_t sender_database_id; | ||||||
|  | 					uint8_t sender_unique_id_length; /* directly followed by this header */ | ||||||
|  | 					uint8_t sender_name_length; /* directly followed after the unique id */ | ||||||
|  | 					uint16_t message_length; /* directly followed after the name */ | ||||||
|  | 					uint16_t message_flags; /* could be later something like deleted etc.... */ | ||||||
|  | 				}; | ||||||
|  | 				static_assert(sizeof(MessageHeader) == 26); | ||||||
|  | 				#pragma pack(pop) | ||||||
|  | 
 | ||||||
|  | 				struct IndexedMessageData { | ||||||
|  | 					MessageHeader header; | ||||||
|  | 					std::string sender_unique_id; | ||||||
|  | 					std::string sender_name; | ||||||
|  | 					std::string message; | ||||||
|  | 				}; | ||||||
|  | 
 | ||||||
|  | 				struct IndexedMessage { | ||||||
|  | 					uint32_t offset; | ||||||
|  | 					std::chrono::system_clock::time_point timestamp; | ||||||
|  | 
 | ||||||
|  | 					std::shared_ptr<IndexedMessageData> message_data; | ||||||
|  | 				}; | ||||||
|  | 
 | ||||||
|  | 				struct IndexedBlock { | ||||||
|  | 					bool successfully; | ||||||
|  | 					/*
 | ||||||
|  | 					 * message_index[0] := index of the message (including the header!) | ||||||
|  | 					 * message_index[1] := timestamp of the message | ||||||
|  | 					 */ | ||||||
|  | 					std::deque<IndexedMessage> message_index; | ||||||
|  | 					std::mutex message_index_lock; | ||||||
|  | 				}; | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			namespace db { | ||||||
|  | 				struct MessageBlock { | ||||||
|  | 					std::chrono::system_clock::time_point begin_timestamp; | ||||||
|  | 					std::chrono::system_clock::time_point end_timestamp; | ||||||
|  | 
 | ||||||
|  | 					uint64_t block_offset; | ||||||
|  | 
 | ||||||
|  | 					union { | ||||||
|  | 						uint16_t flags; | ||||||
|  | 						struct { | ||||||
|  | 							//Attention: Order matters!
 | ||||||
|  | 							bool flag__unused_0 : 1; | ||||||
|  | 							bool flag__unused_1 : 1; | ||||||
|  | 							bool flag__unused_2 : 1; | ||||||
|  | 							bool flag__unused_3 : 1; | ||||||
|  | 							bool flag__unused_4 : 1; | ||||||
|  | 							bool flag__unused_5 : 1; | ||||||
|  | 							bool flag__unused_6 : 1; | ||||||
|  | 							bool flag__unused_7 : 1; | ||||||
|  | 							bool flag__unused_8 : 1; | ||||||
|  | 							bool flag__unused_9 : 1; | ||||||
|  | 							bool flag__unused_10 : 1; | ||||||
|  | 							bool flag__unused_11 : 1; | ||||||
|  | 
 | ||||||
|  | 							bool flag_finished : 1; | ||||||
|  | 							bool flag_finished_later : 1; /* if true the block has been closed because we've a newer block. */ | ||||||
|  | 
 | ||||||
|  | 							bool flag_invalid : 1; /* this block is considered as invalid and will be ignored */ | ||||||
|  | 							bool flag_used : 1; | ||||||
|  | 						}; | ||||||
|  | 					}; | ||||||
|  | 
 | ||||||
|  | 					std::shared_ptr<fio::BlockHeader> block_header; | ||||||
|  | 					std::shared_ptr<fio::IndexedBlock> indexed_block; | ||||||
|  | 				}; | ||||||
|  | 				static_assert(__BYTE_ORDER == __LITTLE_ENDIAN); | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			class ConversationManager; | ||||||
|  | 			class Conversation { | ||||||
|  | 				public: | ||||||
|  | 					Conversation(const std::shared_ptr<ConversationManager>& /* handle */, ChannelId /* channel id */, const std::string& /* file name */); | ||||||
|  | 					~Conversation(); | ||||||
|  | 
 | ||||||
|  | 					bool initialize(std::string& error); | ||||||
|  | 					void finalize(); | ||||||
|  | 
 | ||||||
|  | 					inline ChannelId channel_id() { return this->_channel_id; } | ||||||
|  | 					/* if for some reason we're not able to open the file then we're in volatile mode */ | ||||||
|  | 					inline bool volatile_only() { return !this->file_handle; } | ||||||
|  | 					void cleanup_cache(); | ||||||
|  | 
 | ||||||
|  | 					//void set_history_length(ssize_t /* save length */);
 | ||||||
|  | 					//ssize_t history_length();
 | ||||||
|  | 
 | ||||||
|  | 					inline std::chrono::system_clock::time_point last_message() { return this->_last_message_timestamp; } | ||||||
|  | 					void register_message(ClientDbId sender_database_id, const std::string& sender_unique_id, const std::string& sender_name, const std::string& message); | ||||||
|  | 					/* Lookup n messages since end timestamp. Upper time limit is begin timestamp */ | ||||||
|  | 					std::deque<std::shared_ptr<ConversationEntry>> message_history(const std::chrono::system_clock::time_point& /* end timestamp */, size_t /* limit */, const std::chrono::system_clock::time_point& /* begin timestamp */); | ||||||
|  | 
 | ||||||
|  | 					std::deque<std::shared_ptr<ConversationEntry>> message_history(size_t limit) { | ||||||
|  | 						return this->message_history(std::chrono::system_clock::now(), limit, std::chrono::system_clock::time_point{}); | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					ts_always_inline void set_ref_self(const std::shared_ptr<Conversation>& pointer) { | ||||||
|  | 						this->_ref_self = pointer; | ||||||
|  | 					} | ||||||
|  | 				private: | ||||||
|  | 					std::weak_ptr<Conversation> _ref_self; | ||||||
|  | 					std::weak_ptr<ConversationManager> _ref_handle; | ||||||
|  | 					ts_always_inline std::shared_ptr<ConversationManager> ref_handle() { | ||||||
|  | 						return this->_ref_handle.lock(); | ||||||
|  | 					} | ||||||
|  | 					inline ssize_t fread(void* target, size_t length, ssize_t index); | ||||||
|  | 					inline ssize_t fwrite(void* target, size_t length, ssize_t index, bool extend_file); | ||||||
|  | 
 | ||||||
|  | 					/* block db functions */ | ||||||
|  | 					void db_save_block(const std::shared_ptr<db::MessageBlock>& /* block */); | ||||||
|  | 					std::shared_ptr<db::MessageBlock> db_create_block(uint64_t /* block offset */); | ||||||
|  | 
 | ||||||
|  | 					/* message blocks */ | ||||||
|  | 					std::mutex message_block_lock; | ||||||
|  | 					/* blocks sorted desc (newest blocks last in list (push_back)) */ | ||||||
|  | 					std::deque<std::shared_ptr<db::MessageBlock>> message_blocks; | ||||||
|  | 					/* Access last_message_block only within the write queue or while initializing! */ | ||||||
|  | 					std::shared_ptr<db::MessageBlock> last_message_block; /* is registered within message_blocks,but musnt be the last! */ | ||||||
|  | 					bool load_message_block_header(const std::shared_ptr<db::MessageBlock>& /* block */, std::string& /* error */); | ||||||
|  | 					bool load_message_block_index(const std::shared_ptr<db::MessageBlock>& /* block */, std::string& /* error */); | ||||||
|  | 					bool load_messages(const std::shared_ptr<db::MessageBlock>& /* block */, size_t /* begin index */, size_t /* end index */, std::string& /* error */); | ||||||
|  | 
 | ||||||
|  | 					/* message blocks write stuff */ | ||||||
|  | 					std::shared_ptr<db::MessageBlock> create_new_block(std::string& /* error */); | ||||||
|  | 					void finish_block(const std::shared_ptr<db::MessageBlock>& /* block */, bool write_file); | ||||||
|  | 					bool write_block_header(const std::shared_ptr<fio::BlockHeader>& /* header */, size_t /* header index */, std::string& /* error */); | ||||||
|  | 
 | ||||||
|  | 					/* cached messages */ | ||||||
|  | 					std::mutex _last_messages_lock; | ||||||
|  | 					std::deque<std::shared_ptr<ConversationEntry>> _last_messages; | ||||||
|  | 					size_t _last_messages_limit = 100; /* cache max 100 messages */ | ||||||
|  | 
 | ||||||
|  | 					/* write handler */ | ||||||
|  | 					std::mutex _write_loop_lock; | ||||||
|  | 					std::mutex _write_queue_lock; | ||||||
|  | 					std::deque<std::shared_ptr<ConversationEntry>> _write_queue; | ||||||
|  | 					std::shared_ptr<event::ProxiedEventEntry<Conversation>> _write_event; | ||||||
|  | 					void process_write_queue(const std::chrono::system_clock::time_point&); | ||||||
|  | 
 | ||||||
|  | 					/* basic file stuff */ | ||||||
|  | 					std::string file_name; | ||||||
|  | 					std::mutex file_handle_lock; | ||||||
|  | 					FILE* file_handle = nullptr; | ||||||
|  | 					ChannelId _channel_id; | ||||||
|  | 
 | ||||||
|  | 					std::chrono::system_clock::time_point _last_message_timestamp; | ||||||
|  | 			}; | ||||||
|  | 
 | ||||||
|  | 			class ConversationManager { | ||||||
|  | 				public: | ||||||
|  | 					ConversationManager(const std::shared_ptr<TSServer>& /* server */); | ||||||
|  | 					virtual ~ConversationManager(); | ||||||
|  | 
 | ||||||
|  | 					void initialize(const std::shared_ptr<ConversationManager>& _this); | ||||||
|  | 					void cleanup_channels(); | ||||||
|  | 					void cleanup_cache(); | ||||||
|  | 
 | ||||||
|  | 					bool conversation_exists(ChannelId /* channel */); | ||||||
|  | 					std::shared_ptr<Conversation> get(ChannelId /* channel */); | ||||||
|  | 					std::shared_ptr<Conversation> get_or_create(ChannelId /* channel */); | ||||||
|  | 					void delete_conversation(ChannelId /* channel */); | ||||||
|  | 
 | ||||||
|  | 					inline const std::deque<std::shared_ptr<Conversation>> conversations() { | ||||||
|  | 						std::lock_guard lock(this->_conversations_lock); | ||||||
|  | 						return this->_conversations; | ||||||
|  | 					} | ||||||
|  | 
 | ||||||
|  | 					ts_always_inline std::shared_ptr<TSServer> ref_server() { | ||||||
|  | 						return this->_ref_server.lock(); | ||||||
|  | 					} | ||||||
|  | 				private: | ||||||
|  | 					std::weak_ptr<ConversationManager> _ref_this; | ||||||
|  | 					std::weak_ptr<TSServer> _ref_server; | ||||||
|  | 
 | ||||||
|  | 					std::mutex _conversations_lock; | ||||||
|  | 					std::deque<std::shared_ptr<Conversation>> _conversations; | ||||||
|  | 
 | ||||||
|  | 					std::string file_path; | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @ -44,7 +44,7 @@ if(!result && result.msg().find(ignore) == string::npos){ | |||||||
| 
 | 
 | ||||||
| #define RESIZE_COLUMN(tblName, rowName, size) up vote EXECUTE("Could not change column size", "ALTER TABLE " tblName " ALTER COLUMN " rowName " varchar(" size ")"); | #define RESIZE_COLUMN(tblName, rowName, size) up vote EXECUTE("Could not change column size", "ALTER TABLE " tblName " ALTER COLUMN " rowName " varchar(" size ")"); | ||||||
| 
 | 
 | ||||||
| #define CURRENT_VERSION 10 | #define CURRENT_VERSION 11 | ||||||
| 
 | 
 | ||||||
| #define CLIENT_UID_LENGTH "64" | #define CLIENT_UID_LENGTH "64" | ||||||
| #define CLIENT_NAME_LENGTH "128" | #define CLIENT_NAME_LENGTH "128" | ||||||
| @ -345,6 +345,12 @@ ROLLBACK; | |||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
| 				this->changeVersion(10); | 				this->changeVersion(10); | ||||||
|  | 			case 10: | ||||||
|  | 				CREATE_TABLE("conversations", "`server_id` INT, `channel_id` INT, `conversation_id` INT, `file_path` TEXT", command_append_utf8); | ||||||
|  | 				CREATE_TABLE("conversation_blocks", "`server_id` INT, `conversation_id` INT, `begin_timestamp` INT, `end_timestamp` INT, `block_offset` INT, `flags` INT", command_append_utf8); | ||||||
|  | 				CREATE_INDEX("conversations", "server_id"); | ||||||
|  | 				CREATE_INDEX2R("conversation_blocks", "server_id", "conversation_id"); | ||||||
|  | 				this->changeVersion(11); | ||||||
| 			default: | 			default: | ||||||
| 				if(manager->getType() == sql::TYPE_SQLITE) { | 				if(manager->getType() == sql::TYPE_SQLITE) { | ||||||
| 					result = sql::command(this->sql(), "COMMIT;").execute(); | 					result = sql::command(this->sql(), "COMMIT;").execute(); | ||||||
|  | |||||||
| @ -121,7 +121,14 @@ void VoiceServer::triggerWrite(const std::shared_ptr<VoiceClient>& client) { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void VoiceServer::schedule_execute(const ts::server::VoiceClient *client) { | void VoiceServer::schedule_execute(const ts::server::VoiceClient *client) { | ||||||
| 	serverInstance->getVoiceServerManager()->get_executor_loop()->schedule(client->event_handle_packet); | 	auto vmanager = serverInstance->getVoiceServerManager(); | ||||||
|  | 	if(!vmanager) | ||||||
|  | 		return; | ||||||
|  | 	auto evloop = vmanager->get_executor_loop(); | ||||||
|  | 	if(!evloop) | ||||||
|  | 		return; | ||||||
|  | 
 | ||||||
|  | 	evloop->schedule(client->event_handle_packet); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| void VoiceServer::tickHandshakingClients() { | void VoiceServer::tickHandshakingClients() { | ||||||
|  | |||||||
							
								
								
									
										2
									
								
								shared
									
									
									
									
									
								
							
							
								
								
								
								
								
								
									
									
								
							
						
						
									
										2
									
								
								shared
									
									
									
									
									
								
							| @ -1 +1 @@ | |||||||
| Subproject commit 6ee207aaadeb9a57a9e1fc4fd19ebeb1be81f1fd | Subproject commit d9ddc2c06d7731b14cae47f67a3414ce47e34bae | ||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user