diff --git a/CMakeLists.txt b/CMakeLists.txt deleted file mode 100644 index c193751..0000000 --- a/CMakeLists.txt +++ /dev/null @@ -1,72 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -project(singlechat.wt LANGUAGES CXX) - -add_compile_options(-Wno-deprecated-declarations) -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_COMPILER "/usr/bin/g++-12") - -# Set default build type if not specified -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -# Set compiler flags based on build type -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - add_compile_options(-g) -else() - add_compile_options(-O2) -endif() - -add_executable(${PROJECT_NAME} - src/main.cpp - src/broadcast.cpp - src/app.cpp -) - -target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) - -find_package(Threads REQUIRED) -target_link_libraries(${PROJECT_NAME} - wt - wthttp - curl - xml2 - ${CMAKE_THREAD_LIBS_INIT} -) - -find_package(Boost COMPONENTS system filesystem REQUIRED) -if(Boost_FOUND) - include_directories(${Boost_INCLUDE_DIRS}) - target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES} GraphicsMagick++ GraphicsMagick) -endif() - -if(UNIX) - find_package(PkgConfig) - pkg_check_modules(XML2 libxml-2.0) - if(XML2_FOUND) - target_include_directories(${PROJECT_NAME} PRIVATE ${XML2_INCLUDE_DIRS}) - endif() -endif() - -if(UNIX) - find_package(PkgConfig) - pkg_check_modules(XML2 libxml-2.0) - if(XML2_FOUND) - target_include_directories(${PROJECT_NAME} PRIVATE ${XML2_INCLUDE_DIRS} /usr/include/GraphicsMagick) - endif() -endif() - -include(GNUInstallDirs) -install(TARGETS ${PROJECT_NAME} - RUNTIME DESTINATION /opt/ypchat/bin -) - -# Install docroot directory and handle logs setup -install(DIRECTORY docroot/ - DESTINATION /opt/ypchat/docroot -) -install(CODE "file(MAKE_DIRECTORY /opt/ypchat/logs)") -install(CODE "execute_process(COMMAND chmod 777 /opt/ypchat/logs)") - diff --git a/CMakeLists.txt.old b/CMakeLists.txt.old deleted file mode 100644 index 6a223f8..0000000 --- a/CMakeLists.txt.old +++ /dev/null @@ -1,71 +0,0 @@ -cmake_minimum_required(VERSION 3.5) - -project(singlechat.wt LANGUAGES CXX) -add_compile_options(-Wno-deprecated-declarations) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_COMPILER "/usr/bin/g++-12") -set(CMAKE_PREFIX_PATH "/usr") - -# Set default build type if not specified -if(NOT CMAKE_BUILD_TYPE) - set(CMAKE_BUILD_TYPE Release) -endif() - -# Set compiler flags based on build type -if(CMAKE_BUILD_TYPE STREQUAL "Debug") - add_compile_options(-g) -else() - add_compile_options(-O2) -endif() - -add_executable(${PROJECT_NAME} - src/main.cpp - src/broadcast.h src/broadcast.cpp - src/app.h src/app.cpp - docroot/text.xml - docroot/ads.txt - docroot/links.csv -) -target_compile_features(${PROJECT_NAME} PRIVATE cxx_std_20) - -find_package(Threads REQUIRED) - -target_link_libraries(${PROJECT_NAME} - wt - wthttp - curl - xml2 - ${CMAKE_THREAD_LIBS_INIT} -) - -find_package(Boost COMPONENTS system filesystem REQUIRED) -if(UNIX) - # Ubuntu-spezifische Include-Verzeichnisse - find_package(PkgConfig) - pkg_check_modules(XML2 libxml-2.0) - if(XML2_FOUND) - target_include_directories(${PROJECT_NAME} PRIVATE /usr/include/GraphicsMagick ${XML2_INCLUDE_DIRS}) - endif() -elseif(EXISTS "/etc/os-release" AND ("${CMAKE_SYSTEM_NAME}" STREQUAL "Linux")) - # Tumbleweed-spezifische Include-Verzeichnisse - target_include_directories(${PROJECT_NAME} PRIVATE /usr/include/GraphicsMagick) -endif() - -if(Boost_FOUND) - include_directories(${Boost_INCLUDE_DIRS}) - target_link_libraries(${PROJECT_NAME} ${Boost_LIBRARIES} GraphicsMagick++ GraphicsMagick) -endif() - -include(GNUInstallDirs) - -install(TARGETS ${PROJECT_NAME} - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} - RUNTIME DESTINATION /opt/ypchat/bin -) -install(DIRECTORY docroot/ - DESTINATION /opt/ypchat/docroot -) -install(CODE "file(MAKE_DIRECTORY /opt/ypchat/logs)") -install(CODE "execute_process(COMMAND chmod 777 /opt/ypchat/logs)") diff --git a/CMakeLists.txt.user b/CMakeLists.txt.user deleted file mode 100644 index 7a64514..0000000 --- a/CMakeLists.txt.user +++ /dev/null @@ -1,517 +0,0 @@ - - - - - - EnvironmentId - {551ef6b3-a39b-43e2-9ee3-ad56e19ff4f4} - - - ProjectExplorer.Project.ActiveTarget - 0 - - - ProjectExplorer.Project.EditorSettings - - true - true - true - - Cpp - - CppGlobal - - - - QmlJS - - QmlJSGlobal - - - 2 - UTF-8 - false - 4 - false - 0 - 80 - true - true - 1 - 0 - false - true - false - 2 - true - true - 0 - 8 - true - false - 1 - true - true - true - *.md, *.MD, Makefile - false - true - true - - - - ProjectExplorer.Project.PluginSettings - - - true - false - true - true - true - true - - false - - - 0 - true - - true - true - Builtin.DefaultTidyAndClazy - 8 - true - - - - true - - - - - ProjectExplorer.Project.Target.0 - - Desktop - true - Importiertes Kit - Importiertes Kit - {c305b734-2e7c-49c6-bd3a-cd5bcb4ae45e} - 0 - 0 - 0 - - Debug - 2 - false - - -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} - /mnt/share/torsten/Programs/SingleChat - /home/torsten/Programs/SingleChat/build - - - - - all - - false - - true - Erstellen - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - Erstellen - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Debug (importiert) - CMakeProjectManager.CMakeBuildConfiguration - 0 - 0 - - - 0 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ProjectExplorer.DefaultDeployConfiguration - - - - - - - - - false - - true - ApplicationManagerPlugin.Deploy.CMakePackageStep - - - install-package --acknowledge - true - Application Manager-Paket installieren - ApplicationManagerPlugin.Deploy.InstallPackageStep - - - - - - - - 2 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ApplicationManagerPlugin.Deploy.Configuration - - 2 - - true - true - 0 - true - - 2 - - false - -e cpu-cycles --call-graph dwarf,4096 -F 250 - singlechat.wt - CMakeProjectManager.CMakeRunConfiguration. - singlechat.wt - false - true - true - true - /home/torsten/Programs/SingleChat/build/Debug - - 1 - - - Release - 2 - false - - -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} - /mnt/share/torsten/Programs/SingleChat - /home/torsten/Programs/SingleChat/build - - - - - all - - false - - true - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Release (importiert) - CMakeProjectManager.CMakeBuildConfiguration - 0 - -1 - - - 0 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ProjectExplorer.DefaultDeployConfiguration - - - - - - - install - - false - - true - ApplicationManagerPlugin.Deploy.CMakePackageStep - - - install-package --acknowledge - true - Application Manager-Paket installieren - ApplicationManagerPlugin.Deploy.InstallPackageStep - - - - - - - - 2 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ApplicationManagerPlugin.Deploy.Configuration - - 2 - 0 - - - RelWithDebInfo - 2 - false - - -DCMAKE_COLOR_DIAGNOSTICS:BOOL=ON --DCMAKE_CXX_COMPILER:FILEPATH=%{Compiler:Executable:Cxx} --DQT_QMAKE_EXECUTABLE:FILEPATH=%{Qt:qmakeExecutable} --DCMAKE_PROJECT_INCLUDE_BEFORE:FILEPATH=%{BuildConfig:BuildDirectory:NativeFilePath}/.qtc/package-manager/auto-setup.cmake --DCMAKE_C_COMPILER:FILEPATH=%{Compiler:Executable:C} --DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DCMAKE_PREFIX_PATH:PATH=%{Qt:QT_INSTALL_PREFIX} - /mnt/share/torsten/Programs/SingleChat - /home/torsten/Programs/SingleChat/build - - - - - all - - false - - true - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Release mit Debuginformationen (importiert) - CMakeProjectManager.CMakeBuildConfiguration - 0 - -1 - - - 0 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ProjectExplorer.DefaultDeployConfiguration - - - - - - - install - - false - - true - ApplicationManagerPlugin.Deploy.CMakePackageStep - - - install-package --acknowledge - true - Application Manager-Paket installieren - ApplicationManagerPlugin.Deploy.InstallPackageStep - - - - - - - - 2 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ApplicationManagerPlugin.Deploy.Configuration - - 2 - 0 - - 3 - - - 0 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ProjectExplorer.DefaultDeployConfiguration - - - - - - - - - false - - true - ApplicationManagerPlugin.Deploy.CMakePackageStep - - - install-package --acknowledge - true - Application Manager-Paket installieren - ApplicationManagerPlugin.Deploy.InstallPackageStep - - - - - - - - 2 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ApplicationManagerPlugin.Deploy.Configuration - - 2 - - true - true - 0 - true - - 2 - - false - -e cpu-cycles --call-graph dwarf,4096 -F 250 - singlechat.wt - CMakeProjectManager.CMakeRunConfiguration. - singlechat.wt - false - true - true - true - /home/torsten/Programs/SingleChat/build/Debug - - 1 - - - - ProjectExplorer.Project.TargetCount - 1 - - - ProjectExplorer.Project.Updater.FileVersion - 22 - - - Version - 22 - - diff --git a/CMakeLists.txt.user.d36652f b/CMakeLists.txt.user.d36652f deleted file mode 100644 index 6c5d833..0000000 --- a/CMakeLists.txt.user.d36652f +++ /dev/null @@ -1,387 +0,0 @@ - - - - - - EnvironmentId - {d36652ff-969b-426b-a63f-1edd325096c5} - - - ProjectExplorer.Project.ActiveTarget - 0 - - - ProjectExplorer.Project.EditorSettings - - true - false - true - - Cpp - - CppGlobal - - - - QmlJS - - QmlJSGlobal - - - 2 - UTF-8 - false - 4 - false - 80 - true - true - 1 - false - true - false - 0 - true - true - 0 - 8 - true - false - 1 - true - true - true - *.md, *.MD, Makefile - false - true - true - - - - ProjectExplorer.Project.PluginSettings - - - true - false - true - true - true - true - - - 0 - true - - true - true - Builtin.DefaultTidyAndClazy - 8 - - - - true - - - - - ProjectExplorer.Project.Target.0 - - Desktop - Desktop - Desktop - {15eefee5-3141-47ab-aab0-72db66b5555b} - 0 - 0 - 0 - - Release - false - - -DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} --DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable} - /home/torsten/Programs/SingleChat/build - - - - - all - - false - - true - Erstellen - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - Erstellen - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Debug - CMakeProjectManager.CMakeBuildConfiguration - - - Release - false - - -DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} --DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable} - /home/torsten/Programs/SingleChat/../build-SingleChat-Desktop - - - - - all - - false - - true - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Release - CMakeProjectManager.CMakeBuildConfiguration - - - RelWithDebInfo - false - - -DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} --DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable} - /home/torsten/Programs/SingleChat/../build-SingleChat-Desktop - - - - - all - - false - - true - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Release mit Debuginformationen - CMakeProjectManager.CMakeBuildConfiguration - - - RelWithDebInfo - false - - -DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} --DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable} - 0 - /home/torsten/Programs/SingleChat/../build-SingleChat-Desktop - - - - - all - - false - - true - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Profile - CMakeProjectManager.CMakeBuildConfiguration - - - MinSizeRel - false - - -DCMAKE_GENERATOR:STRING=Ninja Multi-Config --DQT_QMAKE_EXECUTABLE:STRING=%{Qt:qmakeExecutable} --DCMAKE_CXX_COMPILER:STRING=%{Compiler:Executable} - /home/torsten/Programs/SingleChat/../build-SingleChat-Desktop - - - - - all - - false - - true - CMakeProjectManager.MakeStep - - 1 - Erstellen - Erstellen - ProjectExplorer.BuildSteps.Build - - - - - - clean - - false - - true - CMakeProjectManager.MakeStep - - 1 - Bereinigen - Bereinigen - ProjectExplorer.BuildSteps.Clean - - 2 - false - - false - - Release (kleinstmöglich) - CMakeProjectManager.CMakeBuildConfiguration - - 5 - - - 0 - Deployment - Deployment - ProjectExplorer.BuildSteps.Deploy - - 1 - - false - ProjectExplorer.DefaultDeployConfiguration - - 1 - - true - true - true - /usr/bin/valgrind - - 2 - - singlechat.wt - CMakeProjectManager.CMakeRunConfiguration.singlechat.wt - singlechat.wt - --docroot="../../docroot" --http-address=0.0.0.0 --http-port=5050 - false - true - true - false - true - /home/torsten/Programs/SingleChat/build/Release - - 1 - - - - ProjectExplorer.Project.TargetCount - 1 - - - ProjectExplorer.Project.Updater.FileVersion - 22 - - - Version - 22 - - diff --git a/client/src/components/ChatInput.vue b/client/src/components/ChatInput.vue index 5746226..5f0111d 100644 --- a/client/src/components/ChatInput.vue +++ b/client/src/components/ChatInput.vue @@ -67,9 +67,12 @@ const smileys = { }; function sendMessage() { - if (!message.value.trim() || !chatStore.currentConversation) return; + const trimmed = message.value.trim(); + if (!trimmed) return; + const isCommand = trimmed.startsWith('/'); + if (!isCommand && !chatStore.currentConversation) return; - chatStore.sendMessage(chatStore.currentConversation, message.value.trim()); + chatStore.sendMessage(chatStore.currentConversation, trimmed); message.value = ''; } diff --git a/client/src/stores/chat.js b/client/src/stores/chat.js index 5f12e72..5ff960d 100644 --- a/client/src/stores/chat.js +++ b/client/src/stores/chat.js @@ -185,6 +185,10 @@ export const useChatStore = defineStore('chat', () => { handleWebSocketMessage({ type: 'historyResults', ...data }); }); + socketInstance.on('commandResult', (data) => { + handleWebSocketMessage({ type: 'commandResult', ...data }); + }); + socketInstance.on('unreadChats', (data) => { handleWebSocketMessage({ type: 'unreadChats', ...data }); }); @@ -287,6 +291,27 @@ export const useChatStore = defineStore('chat', () => { case 'historyResults': historyResults.value = data.results; break; + case 'commandResult': { + const lines = Array.isArray(data.lines) ? data.lines : []; + if (!currentConversation.value) { + errorMessage.value = lines.join(' | '); + setTimeout(() => { + errorMessage.value = null; + }, 5000); + break; + } + const timestamp = new Date().toISOString(); + for (const line of lines) { + messages.value.push({ + from: 'System', + message: String(line), + timestamp, + self: false, + isImage: false + }); + } + break; + } case 'unreadChats': unreadChatsCount.value = data.count || 0; break; @@ -354,19 +379,23 @@ export const useChatStore = defineStore('chat', () => { } const messageId = Date.now().toString(); + const trimmed = message.trim(); + const isCommand = trimmed.startsWith('/'); socket.value.emit('message', { toUserName, - message, + message: trimmed, messageId }); - // Lokal hinzufügen - messages.value.push({ - from: userName.value, - message, - timestamp: new Date().toISOString(), - self: true - }); + // Lokal hinzufügen (außer bei Commands, die serverseitig beantwortet werden) + if (!isCommand) { + messages.value.push({ + from: userName.value, + message: trimmed, + timestamp: new Date().toISOString(), + self: true + }); + } // Timeout zurücksetzen bei Aktivität resetTimeoutTimer(); diff --git a/logs/chat-users.json b/logs/chat-users.json new file mode 100644 index 0000000..ac20e45 --- /dev/null +++ b/logs/chat-users.json @@ -0,0 +1,7 @@ +[ + { + "username": "admin", + "passwordHash": "sha256:494a715f7e9b4071aca61bac42ca858a309524e5864f0920030862a4ae7589be", + "rights": ["stat", "kick"] + } +] diff --git a/server/broadcast.js b/server/broadcast.js index 47cde5f..fbabcc9 100644 --- a/server/broadcast.js +++ b/server/broadcast.js @@ -18,6 +18,8 @@ class Client { this.loginTimeStamp = new Date(); this.blockedUsers = new Set(); this.socket = null; // Socket.IO Socket-Objekt + this.chatAuth = null; // { username, rights: Set } + this.pendingChatLogin = null; // { step: 'username'|'password', username: string } } setActivity() { @@ -50,6 +52,104 @@ class Client { } } +const CHAT_USERS_FILE_NAME = 'chat-users.json'; +const CHAT_RIGHTS = { + STAT: 'stat', + KICK: 'kick' +}; + +function getLogsDir(__dirname) { + return join(__dirname, '../logs'); +} + +function ensureLogsDir(__dirname) { + const logsDir = getLogsDir(__dirname); + if (!existsSync(logsDir)) { + mkdirSync(logsDir, { recursive: true }); + } + return logsDir; +} + +function getChatUsersPath(__dirname) { + return join(ensureLogsDir(__dirname), CHAT_USERS_FILE_NAME); +} + +function sha256(value) { + return crypto.createHash('sha256').update(value).digest('hex'); +} + +function ensureChatUsersFile(__dirname) { + const usersPath = getChatUsersPath(__dirname); + if (existsSync(usersPath)) { + return; + } + + const defaultUsers = [ + { + username: 'admin', + passwordHash: `sha256:${sha256('changeme123')}`, + rights: [CHAT_RIGHTS.STAT, CHAT_RIGHTS.KICK] + } + ]; + writeFileSync(usersPath, JSON.stringify(defaultUsers, null, 2), 'utf-8'); +} + +function loadChatUsers(__dirname) { + ensureChatUsersFile(__dirname); + const usersPath = getChatUsersPath(__dirname); + const raw = readFileSync(usersPath, 'utf-8').trim(); + if (!raw) return []; + + let users = []; + try { + users = JSON.parse(raw); + } catch (error) { + throw new Error(`Ungültige ${CHAT_USERS_FILE_NAME}: ${error.message}`); + } + + if (!Array.isArray(users)) { + throw new Error(`${CHAT_USERS_FILE_NAME} muss ein Array sein`); + } + + return users + .filter((entry) => entry && typeof entry.username === 'string') + .map((entry) => ({ + username: entry.username.trim(), + passwordHash: typeof entry.passwordHash === 'string' ? entry.passwordHash.trim() : '', + rights: Array.isArray(entry.rights) ? entry.rights.map((r) => String(r).toLowerCase()) : [] + })) + .filter((entry) => entry.username && entry.passwordHash); +} + +function parseLoginRecord(line) { + const trimmed = line.trim(); + if (!trimmed) return null; + const parts = trimmed.split(','); + if (parts.length < 5) return null; + + const timestamp = parts[0].trim(); + const userName = parts[1].trim(); + const gender = parts[parts.length - 1].trim(); + const ageRaw = parts[parts.length - 2].trim(); + const country = parts.slice(2, parts.length - 2).join(',').trim(); + const age = Number.parseInt(ageRaw, 10); + + const date = new Date(timestamp); + if (!timestamp || !userName || !country || Number.isNaN(age) || Number.isNaN(date.getTime())) { + return null; + } + + return { + timestamp, + date, + day: timestamp.slice(0, 10), + userName, + country, + age, + gender + }; +} + let clients = new Map(); let conversations = new Map(); // Key: "user1:user2" (alphabetisch sortiert) @@ -223,6 +323,313 @@ export function setupBroadcast(io, __dirname) { downloadCountries(); setInterval(downloadCountries, 24 * 60 * 60 * 1000); // Täglich aktualisieren + function sendCommandResult(socket, lines) { + const payload = Array.isArray(lines) ? lines : [String(lines)]; + socket.emit('commandResult', { lines: payload }); + } + + function hasRight(client, right) { + return !!client.chatAuth && client.chatAuth.rights instanceof Set && client.chatAuth.rights.has(right); + } + + function getLoginsPath() { + return join(ensureLogsDir(__dirname), 'logins.log'); + } + + function readLoginRecords() { + const logPath = getLoginsPath(); + if (!existsSync(logPath)) return []; + const raw = readFileSync(logPath, 'utf-8'); + return raw + .split('\n') + .map(parseLoginRecord) + .filter(Boolean); + } + + function aggregateTop(items, keySelector, limit = 10) { + const counts = new Map(); + for (const item of items) { + const key = keySelector(item); + counts.set(key, (counts.get(key) || 0) + 1); + } + return Array.from(counts.entries()) + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .slice(0, limit); + } + + function buildAllStats(records) { + if (records.length === 0) { + return ['Keine Login-Daten vorhanden.']; + } + + const today = new Date().toISOString().slice(0, 10); + const todayCount = records.filter((r) => r.day === today).length; + const uniqueNames = new Set(records.map((r) => r.userName)).size; + const ages = records.map((r) => r.age); + const minAge = Math.min(...ages); + const maxAge = Math.max(...ages); + const youngest = records.find((r) => r.age === minAge); + const oldest = records.find((r) => r.age === maxAge); + const topCountries = aggregateTop(records, (r) => r.country, 5) + .map(([name, count]) => `${name}(${count})`) + .join(', '); + + return [ + `Logins gesamt: ${records.length}`, + `Logins heute (${today}): ${todayCount}`, + `Unterschiedliche Namen: ${uniqueNames}`, + `Jüngster Nutzer: ${youngest.userName} (${youngest.age})`, + `Ältester Nutzer: ${oldest.userName} (${oldest.age})`, + `Top Länder: ${topCountries || 'keine'}` + ]; + } + + function executeStatsCommand(socket, client, parts) { + if (!hasRight(client, CHAT_RIGHTS.STAT)) { + sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.'); + return; + } + + const records = readLoginRecords(); + if (records.length === 0) { + sendCommandResult(socket, 'Keine Login-Daten vorhanden.'); + return; + } + + const sub = (parts[1] || '').toLowerCase(); + + if (!sub || sub === 'help') { + sendCommandResult(socket, [ + 'Stat-Befehle:', + '/stat today', + '/stat date YYYY-MM-DD', + '/stat range YYYY-MM-DD YYYY-MM-DD', + '/stat ages', + '/stat names', + '/stat countries', + '/all-stats' + ]); + return; + } + + if (sub === 'today') { + const day = new Date().toISOString().slice(0, 10); + const dayRecords = records.filter((r) => r.day === day); + sendCommandResult(socket, `Logins heute (${day}): ${dayRecords.length}`); + return; + } + + if (sub === 'date') { + const day = parts[2]; + if (!day || !/^\d{4}-\d{2}-\d{2}$/.test(day)) { + sendCommandResult(socket, 'Nutzung: /stat date YYYY-MM-DD'); + return; + } + const dayRecords = records.filter((r) => r.day === day); + sendCommandResult(socket, `Logins am ${day}: ${dayRecords.length}`); + return; + } + + if (sub === 'range') { + const from = parts[2]; + const to = parts[3]; + if (!from || !to || !/^\d{4}-\d{2}-\d{2}$/.test(from) || !/^\d{4}-\d{2}-\d{2}$/.test(to)) { + sendCommandResult(socket, 'Nutzung: /stat range YYYY-MM-DD YYYY-MM-DD'); + return; + } + const filtered = records.filter((r) => r.day >= from && r.day <= to); + const perDay = aggregateTop(filtered, (r) => r.day, 1000) + .sort((a, b) => a[0].localeCompare(b[0])) + .map(([day, count]) => `${day}: ${count}`); + sendCommandResult(socket, [`Logins ${from} bis ${to}: ${filtered.length}`, ...perDay]); + return; + } + + if (sub === 'ages') { + const ages = records.map((r) => r.age); + const minAge = Math.min(...ages); + const maxAge = Math.max(...ages); + const youngest = records.find((r) => r.age === minAge); + const oldest = records.find((r) => r.age === maxAge); + sendCommandResult(socket, [ + `Jüngster Nutzer: ${youngest.userName} (${youngest.age})`, + `Ältester Nutzer: ${oldest.userName} (${oldest.age})` + ]); + return; + } + + if (sub === 'names') { + const topNames = aggregateTop(records, (r) => r.userName, 20); + sendCommandResult(socket, [ + `Namen gesamt (verschieden): ${new Set(records.map((r) => r.userName)).size}`, + ...topNames.map(([name, count]) => `${name}: ${count}`) + ]); + return; + } + + if (sub === 'countries') { + const topCountries = aggregateTop(records, (r) => r.country, 20); + sendCommandResult(socket, topCountries.map(([country, count]) => `${country}: ${count}`)); + return; + } + + sendCommandResult(socket, 'Unbekannter /stat-Befehl. Nutze /stat help'); + } + + function executeAllStatsCommand(socket, client) { + if (!hasRight(client, CHAT_RIGHTS.STAT)) { + sendCommandResult(socket, 'Keine Berechtigung: Recht "stat" fehlt.'); + return; + } + sendCommandResult(socket, buildAllStats(readLoginRecords())); + } + + function executeKickCommand(socket, client, parts) { + if (!hasRight(client, CHAT_RIGHTS.KICK)) { + sendCommandResult(socket, 'Keine Berechtigung: Recht "kick" fehlt.'); + return; + } + + const targetName = (parts[1] || '').trim(); + if (!targetName) { + sendCommandResult(socket, 'Nutzung: /kick '); + return; + } + + let targetSessionId = null; + let targetClient = null; + for (const [sid, c] of clients.entries()) { + if (c.userName === targetName && c.socket && c.socket.connected) { + targetSessionId = sid; + targetClient = c; + break; + } + } + + if (!targetClient) { + sendCommandResult(socket, `User "${targetName}" ist nicht online.`); + return; + } + + if (targetClient.socket) { + targetClient.socket.emit('error', { message: 'Du wurdest vom Chat getrennt (kick).' }); + targetClient.socket.disconnect(true); + } + clients.delete(targetSessionId); + broadcastUserList(); + sendCommandResult(socket, `User "${targetName}" wurde gekickt.`); + } + + function verifyChatUser(username, password) { + const users = loadChatUsers(__dirname); + const user = users.find((u) => u.username.toLowerCase() === username.toLowerCase()); + if (!user) return null; + + const [algo, hash] = user.passwordHash.split(':'); + if (algo !== 'sha256' || !hash) return null; + const inputHash = sha256(password); + if (inputHash !== hash) return null; + + return { + username: user.username, + rights: new Set(user.rights) + }; + } + + function executeCommand(socket, client, rawInput) { + const input = rawInput.trim(); + + if (client.pendingChatLogin) { + if (client.pendingChatLogin.step === 'username') { + const enteredUser = input; + if (!enteredUser) { + sendCommandResult(socket, 'Username darf nicht leer sein. Bitte Username eingeben:'); + return true; + } + client.pendingChatLogin = { step: 'password', username: enteredUser }; + sendCommandResult(socket, 'Passwort eingeben:'); + return true; + } + + if (client.pendingChatLogin.step === 'password') { + const username = client.pendingChatLogin.username; + const auth = verifyChatUser(username, input); + client.pendingChatLogin = null; + if (!auth) { + sendCommandResult(socket, 'Login fehlgeschlagen. Benutzername oder Passwort falsch.'); + return true; + } + + client.chatAuth = auth; + sendCommandResult( + socket, + `Login erfolgreich als ${auth.username}. Rechte: ${Array.from(auth.rights).join(', ') || 'keine'}` + ); + return true; + } + } + + if (!input.startsWith('/')) return false; + + const parts = input.split(/\s+/); + const command = parts[0].toLowerCase(); + + if (command === '/login') { + const username = (parts[1] || '').trim(); + if (username) { + client.pendingChatLogin = { step: 'password', username }; + sendCommandResult(socket, 'Passwort eingeben:'); + } else { + client.pendingChatLogin = { step: 'username', username: '' }; + sendCommandResult(socket, 'Username eingeben:'); + } + return true; + } + + if (command === '/logout-admin') { + const wasLoggedIn = !!client.chatAuth; + client.chatAuth = null; + client.pendingChatLogin = null; + sendCommandResult( + socket, + wasLoggedIn + ? 'Admin/Command-Login wurde abgemeldet.' + : 'Es war kein Admin/Command-Login aktiv.' + ); + return true; + } + + if (command === '/whoami-rights') { + if (!client.chatAuth) { + sendCommandResult(socket, 'Nicht per Command-Login angemeldet.'); + return true; + } + sendCommandResult(socket, [ + `Angemeldet als: ${client.chatAuth.username}`, + `Rechte: ${Array.from(client.chatAuth.rights).join(', ') || 'keine'}` + ]); + return true; + } + + if (command === '/stat') { + executeStatsCommand(socket, client, parts); + return true; + } + + if (command === '/all-stats') { + executeAllStatsCommand(socket, client); + return true; + } + + if (command === '/kick') { + executeKickCommand(socket, client, parts); + return true; + } + + sendCommandResult(socket, `Unbekannter Befehl: ${command}`); + return true; + } + // Socket.IO-Verbindungshandler io.on('connection', (socket) => { const request = socket.handshake; @@ -598,6 +1005,11 @@ export function setupBroadcast(io, __dirname) { } const { toUserName, message, messageId, isImage, imageType, imageUrl } = data; + + // Chat-Befehle werden direkt serverseitig verarbeitet + if (!isImage && typeof message === 'string' && executeCommand(socket, client, message)) { + return; + } if (!toUserName) { socket.emit('error', { message: 'Empfänger fehlt' }); diff --git a/src/app.cpp b/src/app.cpp deleted file mode 100644 index 5398a87..0000000 --- a/src/app.cpp +++ /dev/null @@ -1,1692 +0,0 @@ -#include "app.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -App::App(const Wt::WEnvironment &env, Broadcast &server): - Wt::WApplication(env), - env_(env), - server_(server), - updateLocationSignal_(this, "updateLocationSignal") { - initLocale(); - initApp(); - updateLocation(); - enableUpdates(true); - showStandardPage(); - messageReceived_ = std::make_unique("newmessage.mp3"); - onInternalPathChanged(internalPath()); -} - -App::~App() { - server_.disconnect(this); -} - -void App::setMetaTags() { - removeMetaHeader(Wt::MetaHeaderType::Meta, "robots"); - addMetaHeader("keywords", "chat,single chat,images,no registration,anonymous,direct chat,search,searchable,international,free,no cost,private chat,private"); - // Erlaube Indexierung und das Folgen von Links (u.a. für /partners) - addMetaHeader(Wt::MetaHeaderType::Meta, "robots", "index,follow"); - addMetaHeader(Wt::MetaHeaderType::Meta, "google-adsense-account", "ca-pub-1104166651501135"); -} - -void App::initLocale() { - auto locale = determineLocaleFromBrowser(); - setLocale(locale); -} - -Wt::WLocale App::determineLocaleFromBrowser() const { - auto makeLocale = [](const std::string &code) -> Wt::WLocale { - return Wt::WLocale(code); - }; - - auto mapLanguageTag = [&makeLocale](std::string tag) -> std::string { - // trim - auto begin = tag.find_first_not_of(" \t"); - auto end = tag.find_last_not_of(" \t"); - if (begin == std::string::npos) { - return ""; - } - tag = tag.substr(begin, end - begin + 1); - - // nur Sprach-/Regionscode ohne q-Wert - auto semicolonPos = tag.find(';'); - if (semicolonPos != std::string::npos) { - tag = tag.substr(0, semicolonPos); - } - - std::string lower = tag; - std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower); - - // Sprache und Region trennen (Unterstützung für '-' und '_') - std::string lang = lower; - std::string region; - auto sepPos = lower.find_first_of("-_"); - if (sepPos != std::string::npos) { - lang = lower.substr(0, sepPos); - region = lower.substr(sepPos + 1); - } - - if (lang == "en") { - if (region == "us") { - return "en_US"; - } - // Standard: britisches Englisch - return "en_GB"; - } else if (lang == "de") { - return "de_DE"; - } else if (lang == "fr") { - return "fr_FR"; - } else if (lang == "es") { - return "es_ES"; - } else if (lang == "it") { - return "it_IT"; - } else if (lang == "tl" || lang == "fil") { - return "tl_PH"; - } else if (lang == "ja") { - return "ja_JP"; - } else if (lang == "zh") { - return "zh_CN"; - } else if (lang == "th") { - return "th_TH"; - } - - return ""; - }; - - // 1. Expliziter URL-Parameter ?lang=... hat Vorrang (z.B. de, de-DE, en, en-US, fr, es, it, tl, ja, zh, th) - if (const auto *langParam = env_.getParameter("lang")) { - auto localeName = mapLanguageTag(*langParam); - if (!localeName.empty()) { - return makeLocale(localeName); - } - } - - // 2. Accept-Language Header des Browsers auswerten - std::string accept = env_.headerValue("Accept-Language"); - if (!accept.empty()) { - std::stringstream ss(accept); - std::string part; - while (std::getline(ss, part, ',')) { - auto localeName = mapLanguageTag(part); - if (!localeName.empty()) { - return makeLocale(localeName); - } - } - } - - // 3. Sonst Locale verwenden, die Wt bereits aus dem Request ermittelt hat - auto envLocaleName = env_.locale().name(); // z.B. "de_DE", "en_GB" - auto mapped = mapLanguageTag(envLocaleName); - if (!mapped.empty()) { - return makeLocale(mapped); - } - - // 4. Fallback: britisches Englisch - return makeLocale("en_GB"); -} - -void App::initApp() { - setMetaTags(); - setTitle("YP Direct Chat"); - setCssTheme(""); - useStyleSheet("style.css"); - // Basis-Bundle (englisch) - messageResourceBundle().use("../docroot/text"); - - // Sprachspezifische Bundles explizit nachladen, abhängig von der gesetzten Locale - auto locName = locale().name(); // z.B. "de_DE", "en_GB", "en_US", "ja_JP" - if (locName.rfind("de", 0) == 0) { - messageResourceBundle().use("../docroot/text_de_DE"); - } else if (locName == "en_US") { - messageResourceBundle().use("../docroot/text_en_US"); - } else if (locName.rfind("en", 0) == 0) { - messageResourceBundle().use("../docroot/text_en_GB"); - } else if (locName.rfind("fr", 0) == 0) { - messageResourceBundle().use("../docroot/text_fr_FR"); - } else if (locName.rfind("es", 0) == 0) { - messageResourceBundle().use("../docroot/text_es_ES"); - } else if (locName.rfind("it", 0) == 0) { - messageResourceBundle().use("../docroot/text_it_IT"); - } else if (locName.rfind("tl", 0) == 0 || locName.rfind("fil", 0) == 0) { - messageResourceBundle().use("../docroot/text_tl_PH"); - } else if (locName.rfind("ja", 0) == 0) { - messageResourceBundle().use("../docroot/text_ja_JP"); - } else if (locName.rfind("zh", 0) == 0) { - messageResourceBundle().use("../docroot/text_zh_CN"); - } else if (locName.rfind("th", 0) == 0) { - messageResourceBundle().use("../docroot/text_th_TH"); - } - - internalPathChanged().connect(this, &App::onInternalPathChanged); -} - -void App::showStandardPage() { - userName = server_.userNameForSessionId(sessionId()); - if (root()) { - root()->clear(); - initApp(); - } - auto verticalContainer = createVerticalLayout(); - createHeadContainer(verticalContainer); - createMenuContainer(verticalContainer); - auto horizontalContainer = createActionLayout(verticalContainer); - createUserListContainer(horizontalContainer); - createContentContainer(horizontalContainer); - createImprintContainer(verticalContainer); - reSetUser(); - if (userName == "") { - showLogin(); - } else { - startChat(); - } -} - -void App::reSetUser() { - if (env_.cookies().contains("wtd")) { - auto userData = server_.reSetUser(env_.cookies().find("wtd")->second, sessionId()); - Wt::Json::Object emptyObject = {}; - if (userData != emptyObject) { - userName = (std::string)userData["username"]; - gender = (std::string)userData["gender"]; - country = (std::string)userData["country"]; - isoCountryCode = (std::string)userData["iso-country-code"]; - age = (int)userData["age"]; - } - } -} - -Wt::WVBoxLayout *App::createVerticalLayout() { - auto verticalBox = root()->addNew(); - verticalBox->setHeight(Wt::WLength(100, Wt::LengthUnit::Percentage)); - verticalBox->setWidth(Wt::WLength(100, Wt::LengthUnit::Percentage)); - auto verticalContainer = verticalBox->setLayout(std::make_unique()); - verticalContainer->setContentsMargins(0, 0, 0, 0); - verticalContainer->setSpacing(0); - return verticalContainer; -} - -Wt::WHBoxLayout *App::createActionLayout(Wt::WVBoxLayout *verticalContainer) { - auto horizontalBox = verticalContainer->addWidget(std::make_unique()); - horizontalBox->setStyleClass("horizontal-box"); - auto horizontalContainer = horizontalBox->setLayout(std::make_unique()); - horizontalContainer->setContentsMargins(0, 0, 0, 0); - horizontalContainer->setSpacing(0); - return horizontalContainer; -} - -void App::createHeadContainer(Wt::WVBoxLayout *layout) { - auto header = layout->addWidget(std::make_unique()); - header->addNew("

ypChat

")->setInline(true); - header->setStyleClass("header"); -} - -void App::createMenuContainer(Wt::WVBoxLayout *layout) { - menuContainer_ = layout->addWidget(std::make_unique()); - menuContainer_->setStyleClass("menu"); -} - -void App::showLogin() { - contentContainer_->clear(); - createLoginContainer(); -} - -void App::createLoginContainer() { - auto loginContainer = contentContainer_->addNew(); - loginContainer->resize(Wt::WLength(40, Wt::LengthUnit::FontEm), Wt::WLength::Auto); - auto contentGrid = loginContainer->setLayout(std::make_unique()); - auto userName = addUsernameInput(contentGrid); - auto gender = addGenderSelection(contentGrid); - auto age = addAgeInput(contentGrid); - auto countrySelection = addCountrySelection(contentGrid); - addStartChatButton(contentGrid, userName, countrySelection, age, gender); - contentContainer_->addNew(Wt::WString::tr("welcome")); -} - -Wt::WLineEdit *App::addUsernameInput(Wt::WGridLayout* contentGrid) { - contentGrid->addWidget(std::make_unique(Wt::WString::tr("label_nick")), 0, 0); - auto userName = contentGrid->addWidget(std::make_unique(), 0, 1); - userName->setTextSize(20); - userName->setMaxLength(30); - return userName; -} - -Wt::WComboBox *App::addGenderSelection(Wt::WGridLayout* contentGrid) { - contentGrid->addWidget(std::make_unique(Wt::WString::tr("label_gender")), 1, 0); - auto genderWidget = contentGrid->addWidget(std::make_unique(), 1, 1); - populateGenderComboBox(genderWidget); - return genderWidget; -} - -Wt::WSpinBox *App::addAgeInput(Wt::WGridLayout* contentGrid) { - contentGrid->addWidget(std::make_unique(Wt::WString::tr("label_age")), 2, 0); - auto ageWidget = contentGrid->addWidget(std::make_unique(), 2, 1); - ageWidget->setRange(18, 150); - ageWidget->setValue(18); - return ageWidget; -} - -Wt::WComboBox *App::addCountrySelection(Wt::WGridLayout* contentGrid) { - contentGrid->addWidget(std::make_unique(Wt::WString::tr("label_country")), 3, 0); - auto countryWidget = contentGrid->addWidget(std::make_unique(), 3, 1); - populateCountryComboBox(countryWidget); - return countryWidget; -} - -void App::addStartChatButton(Wt::WGridLayout* contentGrid, Wt::WLineEdit *userName, Wt::WComboBox *country, Wt::WSpinBox *age, Wt::WComboBox *gender) { - auto doLogin = contentGrid->addWidget(std::make_unique(Wt::WString::tr("button_start_chat")), 4, 1); - doLogin->clicked().connect([=, this]() { - handleLogin(userName, country, age, gender); - }); -} - -void App::populateCountryComboBox(Wt::WComboBox* countryWidget) { - auto countries = server_.countries(); - int countryIndex{-1}; - auto isoCode = isoCountryCode; - std::transform(isoCode.begin(), isoCode.end(), isoCode.begin(), ::tolower); - for (const auto &countryItem: countries) { - countryWidget->addItem(countryItem.first); - std::string countryCode = Wt::asString(countryItem.second).toUTF8(); - if (countryCode == isoCode) { - countryIndex = countryWidget->count(); - break; - } - } - countryWidget->setCurrentIndex(countryIndex); -} - -void App::handleLogin(Wt::WLineEdit* userName, Wt::WComboBox* countryWidget, Wt::WSpinBox *ageWidget, Wt::WComboBox *genderWidget) { - try { - std::string nick = extractTrimmedUserName(userName); - std::cout << "handleLogin() nick='" << nick << "'" << std::endl; - validateName(nick); - validateGender(genderWidget); - validateAge(ageWidget); - setUserData(nick, countryWidget, ageWidget, genderWidget); - connectToServer(); - startChat(); - setCookie("wtd", sessionId(), 21000); - } catch (const std::exception& e) { - std::cerr << "handleLogin() exception: " << e.what() << std::endl; - Wt::WMessageBox::show("Attention", e.what(), Wt::StandardButton::Ok); - } -} - -std::string App::extractTrimmedUserName(Wt::WLineEdit* userName) { - return userName->text().trim().toUTF8(); -} - -void App::validateName(const std::string& nick) { - if (isInvalidName(nick) || isNameAlreadyInUse(nick) || !isNickAllowed(nick)) { - throw std::runtime_error("The name you wish is already in use or not allowed. Please use another one."); - } -} - -bool App::isInvalidName(const std::string& nick) { - return nick.empty(); -} - -bool App::isNameAlreadyInUse(const std::string& nick) { - return !server_.nameIsFree(nick); -} - -void App::validateGender(Wt::WComboBox* genderWidget) { - if (!isGenderSelected(genderWidget)) { - throw std::runtime_error("Please select a gender."); - } -} - -bool App::isGenderSelected(Wt::WComboBox* genderWidget) { - return genderWidget->currentIndex() >= 0; -} - -void App::validateAge(Wt::WSpinBox *ageWidget) { - int ageValue = ageWidget->value(); - if (ageValue < 18 || ageValue > 150) { - throw std::runtime_error("This age isn't allowed."); - } -} - -void App::setUserData(const std::string& nick, Wt::WComboBox* countryWidget, Wt::WSpinBox *ageWidget, Wt::WComboBox *genderWidget) { - this->userName = nick; - country = countryWidget->currentText().toUTF8(); - isoCountryCode = server_.getCountryIsoCodeByCountry(country).toUTF8(); - age = ageWidget->value(); - gender = getGenderShortByGender(genderWidget->currentText().toUTF8()); -} - -void App::connectToServer() { - server_.connect(this, std::bind(&App::incomingBroadcast, this)); -} - -void App::populateGenderComboBox(Wt::WComboBox *genderWidget) { - int markIndex{-1}; - for (const auto &genderItem: genders_) { - auto label = Wt::WString::tr(genderItem.second.toUTF8()); - genderWidget->addItem(label); - if (genderItem.first == "F") { - markIndex = genderWidget->count(); - } - } - genderWidget->setCurrentIndex(markIndex); -} - -std::string App::getGenderShortByGender(std::string gender) { - for (const auto &genderItem: genders_) { - auto label = Wt::WString::tr(genderItem.second.toUTF8()).toUTF8(); - if (gender == label) { - return genderItem.first.toUTF8(); - } - } - return ""; -} - -void App::updateUserlist(Wt::Json::Array unsortedUserList, int size) { - userListContainer_->clear(); - userListContainer_->resize(Wt::WLength(15, Wt::LengthUnit::FontEm), Wt::WLength::Auto); - auto layout = userListContainer_->setLayout(std::make_unique()); - layout->setSpacing(1); - layout->setContentsMargins(0, 0, 0, 0); - userListContainer_->setOverflow(Wt::Overflow::Auto, Wt::Orientation::Vertical); - userListContainer_->setOverflow(Wt::Overflow::Hidden, Wt::Orientation::Horizontal); - layout->addWidget(std::make_unique(Wt::WString::tr("logged_in_count").arg(size))); - auto sortedUserList = sortUserList(unsortedUserList); - for (Wt::Json::Object &user: sortedUserList) { - addUserItemToLayout(layout, user); - } - layout->addWidget(std::make_unique(), 1)->setStyleClass("height-spacer userlist"); - triggerUpdate(); -} - -std::vector App::sortUserList(Wt::Json::Array unsortedUserList) { - std::vector sortedUserList; - for (const Wt::Json::Object &item : unsortedUserList) { - sortedUserList.push_back(item); - } - std::sort(sortedUserList.begin(), sortedUserList.end(), [this](const Wt::Json::Object& obj1, const Wt::Json::Object& obj2) { - auto compareResult = compareJsonObjects(obj1, obj2); - return compareResult; - }); - return sortedUserList; -} - -bool App::compareJsonObjects(const Wt::Json::Object& obj1, const Wt::Json::Object& obj2) { - auto standardCountry = country; - auto country1 = (std::string)obj1.get("country"); - auto country2 = (std::string)obj2.get("country"); - auto name1 = (std::string)obj1.get("name"); - auto name2 = (std::string)obj2.get("name"); - if (country1 == standardCountry && country2 == standardCountry) { - return name1 < name2; - } - if (country1 == standardCountry) { - return true; - } else if (country2 == standardCountry) { - return false; - } - if (country1 != country2) { - return country1 < country2; - } - return name1 < name2; -} - -void App::requestConversation(std::string conversationWith) { - if (conversationWith == userName) { - return; - } - currentConversationWith_ = conversationWith; - server_.requestConversation(sessionId(), conversationWith, userName); -} - -void App::showConversation(Wt::Json::Object data) { - try { - Wt::Json::Object userData = extractUserData(data); - if (!shouldShowConversation(userData)) { - return; - } - setupConversationUI(userData); - } catch (const std::exception &e) { - std::cout << __LINE__ << e.what() << std::endl; - } - triggerUpdate(); -} - -Wt::Json::Object App::extractUserData(Wt::Json::Object data) { - return (Wt::Json::Object)data["data"]; -} - -bool App::shouldShowConversation(Wt::Json::Object userData) { - return currentConversationWith_ == (std::string)userData["name"]; -} - -void App::setupConversationUI(Wt::Json::Object userData) { - inboxOpen_ = false; - searchFields.outputContainer = nullptr; - contentContainer_->clear(); - auto layout = contentContainer_->setLayout(std::make_unique()); - createInfoWidget(layout, userData); - layout->addWidget(std::make_unique(), 1)->setOverflow(Wt::Overflow::Auto); - createInputContainer(layout); -} - -Wt::WContainerWidget* App::createInfoWidget(Wt::WVBoxLayout *layout, Wt::Json::Object userData) { - auto infoWidget = layout->addWidget(std::make_unique()); - try { - auto infoLayout = infoWidget->setLayout(std::make_unique()); - infoWidget->setStyleClass(Wt::WString("user-conversation-info userlist-gender-{1}").arg((std::string)userData["gender"])); - auto flag = infoLayout->addWidget(std::make_unique( - Wt::WLink(std::make_shared("../docroot/flags/" + (std::string)userData["isoCountryCode"] + ".png")))); - flag->setToolTip(country); - flag->setStyleClass("flag-icon"); - infoLayout->addWidget(createInfoText(userData), 1); - auto blockButton = createBlockButton(userData); - infoLayout->addWidget(std::move(blockButton)); - } catch(const std::exception &e) { - std::cout << e.what() << std::endl; - } - return infoWidget; -} - -std::unique_ptr App::createInfoText(Wt::Json::Object userData) { - return std::make_unique(Wt::WString("{1} ({2}) - {3}, {4}").arg((std::string)userData["name"]) - .arg((int)userData["age"]).arg((std::string)userData["country"]).arg(genders_[(std::string)userData["gender"]])); -} - -std::unique_ptr App::createBlockButton(Wt::Json::Object userData) { - auto blockButton = std::make_unique( - (bool)userData["blocked"] - ? Wt::WString::tr("button_unblock_user") - : Wt::WString::tr("button_block_user")); - blockButton->clicked().connect([=, this]() mutable { - server_.toggleBlockUser(userName, (std::string)userData["name"], sessionId()); - }); - return blockButton; -} - -Wt::WContainerWidget* App::createInputContainer(Wt::WVBoxLayout* layout) { - auto inputContainer = layout->addWidget(std::make_unique()); - auto inputLayout = inputContainer->setLayout(std::make_unique()); - auto inputLine = createInputLine(inputLayout); - inputLine->setFocus(); - createSendImageButton(inputLayout); - messageCursorPosition_ = std::make_shared(0); - createSmileyButton(inputLayout, inputLine, messageCursorPosition_); - createSmileyBar(inputContainer, inputLine, messageCursorPosition_); - createSendButton(inputLayout, inputLine); - return inputContainer; -} - -Wt::WLineEdit* App::createInputLine(Wt::WHBoxLayout* inputLayout) { - auto inputLine = inputLayout->addWidget(std::make_unique(), 1); - inputLine->setMaxLength(250); - auto cursorPosition = std::make_shared(0); - auto updateCursorPosition = [=]() mutable { - *cursorPosition = inputLine->cursorPosition(); - }; - inputLine->keyPressed().connect(updateCursorPosition); - inputLine->clicked().connect(updateCursorPosition); - return inputLine; -} - -Wt::WImage* App::createSendImageButton(Wt::WHBoxLayout* inputLayout) { - auto sendImageButton = inputLayout->addWidget(std::make_unique(Wt::WLink("/image.png"))); - sendImageButton->setToolTip(Wt::WString::tr("tooltip_send_image")); - sendImageButton->clicked().connect(this, &App::sendImage); - return sendImageButton; -} - -void App::sendImage() { - auto fileDialog = root()->addNew(Wt::WString::tr("dialog_send_image_title")); - auto layout = fileDialog->contents()->setLayout(std::make_unique()); - layout->addWidget(std::make_unique(Wt::WString::tr("dialog_send_image_text"))); - auto fileWidget = layout->addWidget(std::make_unique()); - fileWidget->setFilters("image/*"); - auto image = layout->addWidget(std::make_unique()); - image->setMaximumSize(Wt::WLength(100, Wt::LengthUnit::Pixel), Wt::WLength(100, Wt::LengthUnit::Pixel)); - image->setHeight(Wt::WLength::Auto); - auto localImage = std::make_shared(); - auto buttonsContainer = layout->addWidget(std::make_unique()); - auto okButton = buttonsContainer->addNew(Wt::WString::tr("dialog_send_image_ok")); - fileWidget->uploaded().connect([=, this]() mutable { - imageUploaded(fileWidget, localImage, image, okButton); - }); - fileWidget->changed().connect([=]() { fileWidget->upload(); }); - fileWidget->fileTooLarge().connect([](){ - std::cout << "file too big" << std::endl; - }); - okButton->setDisabled(true); - okButton->clicked().connect([=, this]() { - server_.addImage(sessionId(), currentConversationWith_, localImage); - fileDialog->accept(); - }); - auto cancelButton = buttonsContainer->addNew(Wt::WString::tr("dialog_send_image_cancel")); - cancelButton->clicked().connect([=](){ fileDialog->reject(); }); - fileDialog->setClosable(true); - fileDialog->setModal(true); - fileDialog->show(); -} - -void App::imageUploaded(Wt::WFileUpload *fileWidget, std::shared_ptr localImage, Wt::WImage *image, Wt::WPushButton *okButton) { - try { - const std::string uploadedFile = fileWidget->spoolFileName(); - std::list originalImages; - Magick::readImages(&originalImages, uploadedFile); - std::list resizedForLocalImage; - std::list resizedForDisplayImage; - resizedForLocalImage = resizeImages(originalImages, 500, 500); - resizedForDisplayImage = resizeImages(originalImages, 100, 100); - std::string imageType = originalImages.front().magick(); - std::string mimeType = "image/" + imageType; - Magick::writeImages(resizedForLocalImage.begin(), resizedForLocalImage.end(), localImage.get(), true); - Magick::Blob displayBlob; - Magick::writeImages(resizedForDisplayImage.begin(), resizedForDisplayImage.end(), &displayBlob, true); - auto memoryResource = std::make_shared(mimeType); - memoryResource->setData(static_cast(displayBlob.data()), displayBlob.length()); - image->setImageLink(Wt::WLink(memoryResource)); - image->setAlternateText(Wt::WString::tr("image_uploaded_processed")); - okButton->setEnabled(true); - triggerUpdate(); - } catch (const std::exception& e) { - std::cerr << "Error processing uploaded image: " << e.what() << std::endl; - } -} - -std::list App::resizeImages(std::list &images, int maxWidth, int maxHeight) { - std::list resizedImages; - for (auto& img : images) { - double originalWidth = img.columns(); - double originalHeight = img.rows(); - double widthRatio = static_cast(maxWidth) / originalWidth; - double heightRatio = static_cast(maxHeight) / originalHeight; - double resizeRatio = std::min(widthRatio, heightRatio); // Nehme das kleinere Verhältnis, um innerhalb der Grenzen zu bleiben - int newWidth = static_cast(originalWidth * resizeRatio); - int newHeight = static_cast(originalHeight * resizeRatio); - if (resizeRatio < 1) { - img.resize(Magick::Geometry(newWidth, newHeight)); - } - resizedImages.push_back(img); - } - return resizedImages; -} - -void App::onInternalPathChanged(const std::string &path) { - std::cout << __LINE__ << "'" << path << "'" << std::endl << std::endl; - if (path.empty() || path == "/") { - showStandardPage(); - } else if (path == "/partners") { - showPartnerSites(); - } else if (path == "/adm/info/logins" || path == "/adm/info/starts") { - showAdminPage(path); - } else { - setInternalPath("/", true); - } -} - -void App::showAdminPage(std::string page) { - if (isLoggedInAsAdmin) { - if (page == "/adm/info/logins") { - showAdminLogins(); - } else if (page == "/adm/info/starts") { - showAdminStarts(); - } else { - showPageNotExists(); - } - return; - } - showAdminLogin(page); -} - -void App::showAdminLogin(std::string page) { - contentContainer_->clear(); - auto loginContainer = contentContainer_->addNew(); - loginContainer->addNew(Wt::WString::tr("admin_name")); - auto nameEdit = loginContainer->addNew(); - loginContainer->addNew(); - loginContainer->addNew(Wt::WString::tr("admin_password")); - auto passwordEdit = loginContainer->addNew(); - passwordEdit->setEchoMode(Wt::EchoMode::Password); - loginContainer->addNew(); - auto loginButton = loginContainer->addNew(Wt::WString::tr("admin_login_button")); - auto messageLabel = loginContainer->addNew(); - auto loginAction = [=, this] { - if (nameEdit->text().toUTF8() == adminName && passwordEdit->text().toUTF8() == adminPassword) { - isLoggedInAsAdmin = true; - messageLabel->setText(""); - showAdminPage(page); - } else { - isLoggedInAsAdmin = false; - messageLabel->setText(Wt::WString::tr("admin_login_error")); - messageLabel->decorationStyle().setForegroundColor(Wt::WColor("red")); - } - }; - loginButton->clicked().connect(loginAction); - passwordEdit->enterPressed().connect(loginAction); -} - -void App::showPageNotExists() { - contentContainer_->clear(); - auto errorMessageLabel = contentContainer_->addNew(Wt::WString::tr("error_page_not_found")); - errorMessageLabel->decorationStyle().setForegroundColor(Wt::WColor("red")); -} - -void App::showAdminLogins() { - contentContainer_->clear(); - contentContainer_->addNew("

Logins

", Wt::TextFormat::UnsafeXHTML); - std::ifstream file("../logs/logins.log"); - if (!file.is_open()) { - contentContainer_->addNew(Wt::WString::tr("error_opening_file")); - return; - } - std::stringstream buffer; - buffer << file.rdbuf(); - std::string fileContent = buffer.str(); - Wt::Json::Array jsonArray; - Wt::Json::parse(fileContent, jsonArray); - auto table = contentContainer_->addNew(); - table->setHeaderCount(1); - table->elementAt(0, 0)->addNew(Wt::WString::tr("label_name")); - table->elementAt(0, 1)->addNew(Wt::WString::tr("label_country")); - table->elementAt(0, 2)->addNew(Wt::WString::tr("label_gender")); - table->elementAt(0, 3)->addNew(Wt::WString::tr("label_age")); - table->elementAt(0, 4)->addNew("Datum"); - table->elementAt(0, 5)->addNew("Uhrzeit"); - int row = 1; - for (const auto& item : jsonArray) { - Wt::Json::Object jsonData = item; - std::string name = jsonData.get("name").orIfNull(""); - std::string country = jsonData.get("country").orIfNull(""); - std::string gender = jsonData.get("gender").orIfNull(""); - int age = jsonData.get("age").orIfNull(0); - std::string timestamp = jsonData.get("timestamp").orIfNull(""); - auto nameCell = table->elementAt(row, 0)->addNew(name); - nameCell->setStyleClass("padding-right"); - auto countryCell = table->elementAt(row, 1)->addNew(country); - countryCell->setStyleClass("padding-right"); - auto genderCell = table->elementAt(row, 2)->addNew(gender); - genderCell->setStyleClass("padding-right"); - auto ageCell = table->elementAt(row, 3)->addNew(std::to_string(age)); - ageCell->setStyleClass("padding-right"); - - // Zeitstempel in deutsches Format umwandeln (falls vorhanden und gültig) - if (!timestamp.empty()) { - std::istringstream ss(timestamp); - std::tm tm{}; - ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); - - if (!ss.fail()) { - std::ostringstream dateStream; - std::ostringstream timeStream; - dateStream.imbue(std::locale("de_DE.utf8")); - timeStream.imbue(std::locale("de_DE.utf8")); - dateStream << std::put_time(&tm, "%d.%m.%Y"); - timeStream << std::put_time(&tm, "%H:%M:%S"); - - auto dateCell = table->elementAt(row, 4)->addNew(dateStream.str()); - dateCell->setStyleClass("padding-right"); - table->elementAt(row, 5)->addNew(timeStream.str()); - } else { - // Fallback bei ungültigem Zeitstempel - auto dateCell = table->elementAt(row, 4)->addNew("-"); - dateCell->setStyleClass("padding-right"); - table->elementAt(row, 5)->addNew("-"); - } - } else { - // Fallback für alte Einträge ohne Zeitstempel - auto dateCell = table->elementAt(row, 4)->addNew("-"); - dateCell->setStyleClass("padding-right"); - table->elementAt(row, 5)->addNew("-"); - } - row++; - } -} - - -void App::showAdminStarts() { - contentContainer_->clear(); - contentContainer_->addNew("

Chat starts

", Wt::TextFormat::UnsafeXHTML); - std::ifstream file("../logs/starts.log"); - if (!file.is_open()) { - contentContainer_->addNew("Error opening file."); - return; - } - auto table = contentContainer_->addNew(); - table->setHeaderCount(1); - table->elementAt(0, 0)->addNew("Datum"); - table->elementAt(0, 1)->addNew("Uhrzeit"); - std::string line; - int row = 1; - while (std::getline(file, line)) { - std::istringstream ss(line); - std::tm tm{}; - ss >> std::get_time(&tm, "%Y-%m-%d %H:%M:%S"); - std::ostringstream dateStream; - std::ostringstream timeStream; - dateStream.imbue(std::locale("de_DE.utf8")); - timeStream.imbue(std::locale("de_DE.utf8")); - dateStream << std::put_time(&tm, "%d.%m.%Y"); - timeStream << std::put_time(&tm, "%H:%M:%S"); - auto dateCell = table->elementAt(row, 0)->addNew(dateStream.str()); - dateCell->setStyleClass("padding-right"); - table->elementAt(row, 1)->addNew(timeStream.str()); - row++; - } -} - -Wt::WContainerWidget* App::createSmileyButton(Wt::WHBoxLayout* inputLayout, Wt::WLineEdit* inputLine, std::shared_ptr cursorPosition) { - auto smileyButton = inputLayout->addWidget(std::make_unique()); - smileyButton->addNew(Wt::WLink("/smileys.png")); - smileyButton->setStyleClass("no-style"); - smileyButton->setToolTip("Add a smiley"); - smileyButton->setInline(true); - auto smileyBar = createSmileyBar(smileyButton, inputLine, cursorPosition); - smileyBar->setHidden(true); - smileyButton->clicked().connect([=]() { - smileyBar->setHidden(!smileyBar->isHidden()); - }); - return smileyButton; -} - -Wt::WContainerWidget* App::createSmileyBar(Wt::WContainerWidget* parent, Wt::WLineEdit* inputLine, std::shared_ptr cursorPosition) { - auto smileyBar = parent->addWidget(createSmileysBar(inputLine, cursorPosition)); - smileyBar->setHidden(true); - return smileyBar; -} - -Wt::WPushButton* App::createSendButton(Wt::WHBoxLayout* inputLayout, Wt::WLineEdit* inputLine) { - auto sendButton = inputLayout->addWidget(std::make_unique(Wt::WString::tr("button_send"))); - auto sendMessageFunction = [=, this]() { - sendMessage(inputLine); - }; - inputLine->enterPressed().connect(sendMessageFunction); - sendButton->clicked().connect(sendMessageFunction); - inputLine->setFocus(); - return sendButton; -} - -void App::sendMessage(Wt::WLineEdit *inputLine) { - setActivity(); - auto utf8String = inputLine->valueText().trim().toUTF8(); - if (utf8String == "") { - return; - } - size_t pos = 0; - while ((pos = utf8String.find("<", pos)) != std::string::npos) { - utf8String.replace(pos, 1, "<"); - pos += 4; - } - inputLine->setValueText(""); - auto sendString = Wt::WString(utf8String.substr(0, 250)); - *messageCursorPosition_ = 0; - server_.addMessage(sessionId(), currentConversationWith_, Broadcast::Message(sessionId(), sendString)); -} - -void App::renderConversation(Wt::Json::Object conversation) { - if (currentConversationWith_ != (std::string)conversation["user1"] && currentConversationWith_ != (std::string)conversation["user2"]) { - return; - } - updateOutputContainer(conversation); - server_.setConversationRead(sessionId(), currentConversationWith_); - triggerUpdate(); -} - -void App::updateOutputContainer(Wt::Json::Object conversation) { - auto containerLayout = (Wt::WVBoxLayout*)contentContainer_->layout(); - auto outputLayoutItem = containerLayout->itemAt(1); - auto outputContainer = (Wt::WContainerWidget*)outputLayoutItem->widget(); - outputContainer->setOverflow(Wt::Overflow::Auto); - renderChatLines(conversation, outputContainer); -} - -void App::renderChatLines(Wt::Json::Object conversation, Wt::WContainerWidget* outputContainer) { - Wt::Json::Array chat = conversation["data"]; - for (Wt::Json::Object &line : chat) { - try { - renderChatLine(line, conversation, outputContainer); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - } - } - doJavaScript(outputContainer->jsRef() + ".scrollTop += " - + outputContainer->jsRef() + ".scrollHeight;"); -} - -void App::renderChatLine(Wt::Json::Object &line, Wt::Json::Object conversation, Wt::WContainerWidget* outputContainer) { - std::string writer = getChatLineWriter(line, conversation); - Wt::WWebWidget* item; - auto id = (std::string)line["id"]; - for (const auto widget: outputContainer->children()) { - if (widget->attributeValue("dummy") == id) { - return; - } - } - if ((std::string)line["type"] == "text") { - item = createTextElement(writer, (std::string)line["string"], outputContainer, id); - } else if ((std::string)line["type"] == "image") { - item = createImageElement(line, writer, outputContainer, id); - } else { - item = new Wt::WText(""); - } - item->setToolTip(line["timestamp"]); - item->setStyleClass("output-box-format"); - item->addStyleClass(writer == "you" ? "ouput-box-format-self" : "output-box-format-other"); - item->setInline(false); -} - -Wt::WWebWidget* App::createTextElement(const std::string& writer, const std::string& text, Wt::WContainerWidget* outputContainer, std::string id) { - std::string outputText = replaceSmileys(text); - Wt::WString output = Wt::WString("{1}: {2}").arg(writer).arg(outputText); - auto line = outputContainer->addNew(output); - line->setAttributeValue("dummy", id); - return line; -} - -void App::createImprintContainer(Wt::WVBoxLayout *containerLayout) { - auto imprintContainer = containerLayout->addWidget(std::make_unique()); - imprintContainer->setContentAlignment(Wt::AlignmentFlag::Justify); - imprintContainer->setPadding(Wt::WLength(0.5, Wt::LengthUnit::FontEm)); - auto partnerPagesButton = imprintContainer->addNew("Partners"); - partnerPagesButton->clicked().connect(this, &App::showPartnerSites); - partnerPagesButton->setFloatSide(Wt::Side::Left); - auto imprintButton = imprintContainer->addNew("Imprint"); - imprintButton->setFloatSide(Wt::Side::Right); - Wt::WCssDecorationStyle imprintButtonDecorationStyle; - imprintButtonDecorationStyle.setForegroundColor(Wt::StandardColor::White); - imprintButtonDecorationStyle.setBackgroundColor(Wt::WColor(0x42, 0x90, 0x43)); - imprintButtonDecorationStyle.setCursor(Wt::Cursor::PointingHand); - imprintContainer->setDecorationStyle(imprintButtonDecorationStyle); - imprintButton->clicked().connect([=, this]() { - auto imprintDialog = root()->addNew("Imprint", "", Wt::Icon::None, Wt::StandardButton::Ok); - imprintDialog->contents()->addNew("

Imprint

"); - imprintDialog->contents()->addNew("

Information according to § 5 TMG

"); - imprintDialog->contents()->addNew("

Torsten Schulz
" - "Friedrich-Stampfer-Str. 21
" - "60437 Frankfurt
" - "

", Wt::TextFormat::UnsafeXHTML); - imprintDialog->contents()->addNew("

Represented by:
" - "Torsten Schulz
" - "

", Wt::TextFormat::UnsafeXHTML); - imprintDialog->contents()->addNew("

Contact:
" - "Phone: 069-95 64 17 10
" - "Email: tsschulz@tsschulz.de
" - "

", Wt::TextFormat::UnsafeXHTML); - imprintDialog->contents()->addNew("

Disclaimer:

Liability for Contents

" - "The contents of our pages were created with the greatest care. However, we cannot guarantee the correctness, completeness, and topicality of the contents. As a service provider, we are responsible for our own content on these pages in accordance with § 7 para.1 TMG (German Telemedia Act) and general laws. According to §§ 8 to 10 TMG, however, we are not obliged as service providers to monitor transmitted or stored third-party information or to investigate circumstances that indicate illegal activity. Obligations to remove or block the use of information under general law remain unaffected. However, liability in this regard is only possible from the time of knowledge of a concrete infringement. If we become aware of any such legal infringements, we will remove the content immediately.

Liability for Links

" - "Our offer contains links to external websites of third parties, on whose contents we have no influence. Therefore, we cannot assume any liability for these external contents. The respective provider or operator of the pages is always responsible for the contents of the linked pages. The linked pages were checked for possible legal violations at the time of linking. Illegal contents were not recognizable at the time of linking. However, permanent monitoring of the content of the linked pages is not reasonable without concrete evidence of a violation of the law. If we become aware of any infringements, we will remove such links immediately.

Data Protection

" - "The use of our website is usually possible without providing personal data. As far as personal data (e.g., name, address, or email addresses) is collected on our website, this is always done on a voluntary basis as far as possible. This data will not be passed on to third parties without your express consent.
" - "We would like to point out that data transmission over the Internet (e.g., communication by email) can have security gaps. A complete protection of data against access by third parties is not possible.
" - "The use of contact data published within the scope of the imprint obligation by third parties for sending unsolicited advertising and information materials is hereby expressly prohibited. The operators of these pages expressly reserve the right to take legal action in the event of unsolicited sending of advertising information, such as spam emails.
" - "

", Wt::TextFormat::UnsafeXHTML); - imprintDialog->contents()->addNew("
" - "Imprint from Imprint Generator of Kanzlei Hasselbach, Lawyers for Labor Law and Family Law ", Wt::TextFormat::UnsafeXHTML); - imprintDialog->contents()->addNew("
" - "Thanks for the flag icons to flagpedia.net", Wt::TextFormat::UnsafeXHTML); - imprintDialog->contents()->setMaximumSize(Wt::WLength(60, Wt::LengthUnit::FontEm), Wt::WLength(40, Wt::LengthUnit::FontEm)); - imprintDialog->contents()->setOverflow(Wt::Overflow::Auto); - imprintDialog->buttonClicked().connect([=]() { imprintDialog->accept(); }); - imprintDialog->show(); - }); -} - -Wt::WWebWidget* App::createImageElement(Wt::Json::Object& line, const std::string& writer, Wt::WContainerWidget* outputContainer, std::string id) { - Wt::Json::Object imageDescription = line["image"]; - auto imageLineItem = outputContainer->addNew(); - auto outputText = Wt::WString("{1}: ").arg(writer); - imageLineItem->addNew(outputText)->setStyleClass("output-line"); - auto image = imageLineItem->addNew(); - std::string base64Data = (std::string)imageDescription["imageblobbase64"]; - std::string decodedData = Wt::Utils::base64Decode(base64Data); - std::string imageType = imageDescription["type"].toString(); // Lese den Bildtyp - auto imageResource = std::make_shared( - "image/" + imageType, - std::vector(decodedData.begin(), decodedData.end())); - std::cout << __LINE__ << std::endl; - image->setImageLink(Wt::WLink(imageResource)); - imageLineItem->setAttributeValue("dummy", id); - return imageLineItem; -} - -std::string App::getChatLineWriter(Wt::Json::Object &line, Wt::Json::Object conversation) { - std::string sender = (std::string)line["sender"]; - if (sender == sessionId()) { - return "you"; - } else if (sender == (std::string)conversation["sessionid1"]) { - return (std::string)conversation["user1"]; - } else { - return (std::string)conversation["user2"]; - } -} - -std::string App::replaceSmileys(std::string outputText) { - for (const auto &smiley : smileys_) { - std::size_t pos = outputText.find(smiley.first); - while (pos != std::string::npos) { - outputText.replace(pos, smiley.first.length(), "&#x" + smiley.second.code + ";"); - pos = outputText.find(smiley.first, pos + smiley.second.code.length()); - } - } - return outputText; -} - -void App::showUnreadMessages(Wt::Json::Object data) { - Wt::WString base = Wt::WString::tr("menu_inbox"); - Wt::WString buttonText = (int)data["data"] == 0 ? base : Wt::WString("{1} ({2})").arg(base).arg((int)data["data"]); - auto currentText = inbox_->text(); - inbox_->setText(buttonText); - std::string pattern{"Posteingang\\(\\s*(\\d+)\\)"}; - std::regex regex(pattern); - std::string text = currentText.toUTF8(); - std::smatch match; - auto doPlay{false}; - if (std::regex_search(text, match, regex)) { - std::string numberStr = match[2].str(); - auto oldValue = std::stoi(numberStr); - if (oldValue < (int)data["data"] && (int)data["data"] > 0) { - doPlay = true; - } - } else if (text == Wt::WString::tr("menu_inbox") && buttonText != Wt::WString::tr("menu_inbox")) { - doPlay = true; - } - if (doPlay) { - messageReceived_->play(); - } - triggerUpdate(); - if (inboxOpen_) { - server_.sendOpenConversations(sessionId()); - } -} - -void App::showOpenInbox(Wt::Json::Object data) { - searchFields.outputContainer = nullptr; - contentContainer_->clear(); - contentContainer_->setPadding(Wt::WLength(1, Wt::LengthUnit::FontEm), Wt::Side::Top | Wt::Side::Bottom); - contentContainer_->setPadding(Wt::WLength(2, Wt::LengthUnit::FontEm), Wt::Side::Left | Wt::Side::Right); - contentContainer_->setOverflow(Wt::Overflow::Auto); - auto headline = contentContainer_->addNew("

Inbox

"); - headline->setInline(false); - auto conversationsTable = contentContainer_->addNew(); - conversationsTable->resize(Wt::WLength(15, Wt::LengthUnit::FontEm), Wt::WLength::Auto); - for (Wt::Json::Object &user: (Wt::Json::Array)data["data"]) { - auto row = conversationsTable->rowCount(); - auto userItem = conversationsTable->elementAt(row, 0)->addNew(); - Wt::WString partnerName = (std::string)user["name"]; - auto flag = userItem->addNew(Wt::WLink(std::make_shared("../docroot/flags/" + (std::string)user["isoCountryCode"] + ".png"))); - flag->setToolTip(country); - flag->setStyleClass("flag-icon"); - userItem->addNew(Wt::WString("{1} ({2})").arg(partnerName).arg((int)user["age"])); - userItem->setStyleClass(Wt::WString("userlist-item userlist-gender-{1}").arg((std::string)user["gender"])); - userItem->setHeight(Wt::WLength(2, Wt::LengthUnit::FontEm)); - userItem->setPadding(Wt::WLength(3, Wt::LengthUnit::Pixel)); - userItem->clicked().connect([=, this]() { - requestConversation(partnerName.toUTF8()); - }); - } - triggerUpdate(); -} - -void App::updateUserinfo(Wt::Json::Object data) { - Wt::Json::Object userData = data["data"]; - if ((std::string)userData["name"] != currentConversationWith_) { - return; - } - auto containerLayout = (Wt::WVBoxLayout*)contentContainer_->layout(); - auto infoLayoutItem = containerLayout->itemAt(0); - auto infoWidget = (Wt::WContainerWidget*)(infoLayoutItem->widget()); - auto infoWidgetLayout = (Wt::WHBoxLayout*)infoWidget->layout(); - auto infoTextWidget = dynamic_cast(infoWidgetLayout->itemAt(1)->widget()); - auto blockButton = dynamic_cast(infoWidgetLayout->itemAt(2)->widget()); - try { - infoWidget->setStyleClass(Wt::WString("user-conversation-info userlist-gender-{1}").arg((std::string)userData["gender"])); - if (infoTextWidget) { - infoTextWidget->setText(Wt::WString("{1} ({2}) - {3}, {4}").arg((std::string)userData["name"]) - .arg((int)userData["age"]).arg((std::string)userData["country"]).arg(genders_[(std::string)userData["gender"]])); - } - blockButton->setText((bool)userData["blocked"] ? "Unblock" : "Block"); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl << Wt::Json::serialize(userData) << std::endl; - } - triggerUpdate(); -} - -std::unique_ptr App::createSmileysBar(Wt::WLineEdit *inputLine, std::shared_ptr cursorPosition) { - auto smileyBar = std::make_unique(); - smileyBar->setHidden(true); - for (auto &smiley: smileys_) { - auto code = smileyPlaceholder_; - code = code.arg(smiley.second.code); - auto item = smileyBar->addNew(code, Wt::TextFormat::UnsafeXHTML); - item->setToolTip(smiley.second.tooltip); - item->clicked().connect([=]() { - try { - auto currentText = inputLine->text().toUTF8(); - currentText.insert(*cursorPosition, smiley.first); - inputLine->setText(currentText); - inputLine->setFocus(); - *cursorPosition += smiley.first.length(); - } catch (const std::exception &e) { - std::cout << e.what() << std::endl; - } - }); - } - smileyBar->setStyleClass("smiley-bar"); - return smileyBar; -} - -void App::toggleSmileysBar(Wt::WContainerWidget *smileyBar) { - smileyBar->setHidden(!smileyBar->isHidden()); -} - -void App::systemEvent(Wt::Json::Object broadcast) { - if ((std::string)broadcast["related-user"] != currentConversationWith_) { - return; - } - auto containerLayout = (Wt::WVBoxLayout*)contentContainer_->layout(); - auto infoLayoutItem = containerLayout->itemAt(0); - auto infoWidget = (Wt::WContainerWidget*)(infoLayoutItem->widget()); - auto infoWidgetLayout = (Wt::WHBoxLayout*)infoWidget->layout(); - auto blockButton = dynamic_cast(infoWidgetLayout->itemAt(2)->widget()); - if (broadcast["data"] == "blocked") { - blockButton->setText("Unblock"); - } else if (broadcast["data"] == "unblocked") { - blockButton->setText("Block"); - } -} - - -void App::createMenu() { - menuContainer_->clear(); - menuContainer_->setStyleClass("menu"); - addLeaveButton(); - addIdentifier(); - addSearchButton(); - addInboxButton(); - addHistoryButton(); - addLoginTimeView(); - addTimeoutView(); -} - -void App::addLeaveButton() { - auto leaveButton = menuContainer_->addNew(Wt::WString::tr("menu_leave")); - leaveButton->clicked().connect(this, &App::logout); -} - -void App::logout() { - server_.addToDisconnectList(this); - userListContainer_->clear(); - menuContainer_->clear(); - userName = ""; - showLogin(); - inboxOpen_ = false; - searchFields.outputContainer = nullptr; -} - -void App::addHistoryButton() { - auto history = menuContainer_->addNew(Wt::WString::tr("menu_history")); - history->clicked().connect(this, &App::requestHistory); -} - -void App::addIdentifier() { - menuContainer_->setPositionScheme(Wt::PositionScheme::Relative); - auto flag = menuContainer_->addNew(Wt::WLink(std::make_shared("../docroot/flags/" + isoCountryCode + ".png"))); - flag->setToolTip(country); - flag->setStyleClass("flag-icon"); - auto identifier = menuContainer_->addNew(Wt::WString("{1} ({2}), {3}").arg(userName).arg(isoCountryCode).arg(age)); - identifier->setMargin(Wt::WLength(0.3, Wt::LengthUnit::FontEm), Wt::Side::Bottom | Wt::Side::Top); - Wt::WCssDecorationStyle backgroundStyle; - backgroundStyle.setBackgroundColor(Wt::StandardColor::White); - identifier->setStyleClass("inline-block"); - identifier->setPadding(Wt::WLength(0.3, Wt::LengthUnit::FontEm), Wt::Side::Left | Wt::Side::Right); - identifier->setDecorationStyle(backgroundStyle); - identifier->setWidth(Wt::WLength(25, Wt::LengthUnit::FontEm)); -} - -void App::addSearchButton() { - auto searchButton = menuContainer_->addNew(Wt::WString::tr("menu_search")); - searchButton->clicked().connect(this, &App::showSearchWindow); -} - -void App::addInboxButton() { - inbox_ = menuContainer_->addNew(Wt::WString::tr("menu_inbox")); - inbox_->clicked().connect(this, &App::openInbox); -} - -void App::showSearchWindow() { - setActivity(); - auto contentLayout = resetSearchFields(); - auto userNameField = setupNameSearchField(contentLayout); - auto ageSearchFields = setupSearchFields(contentLayout); - auto countryFields = setupCountryDropDown(contentLayout); - auto gendersFields = setupGendersDropDown(contentLayout); - searchFields.outputContainer = setupSearchButton(contentLayout); - restoreSearchFields(searchFields.outputContainer, userNameField, ageSearchFields.first, ageSearchFields.second, - countryFields.second, gendersFields.second, countryFields.first, gendersFields.first); -} - -Wt::WVBoxLayout *App::resetSearchFields() { - currentConversationWith_ = ""; - contentContainer_->clear(); - inboxOpen_ = false; - auto contentLayout = contentContainer_->setLayout(std::make_unique()); - contentLayout->addWidget(std::make_unique(Wt::WString::tr("search_title"), Wt::TextFormat::UnsafeXHTML)); - return contentLayout; -} - -std::pair App::setupSearchFields(Wt::WVBoxLayout *contentLayout) { - auto minAgeEdit = addSearchItemLine(contentLayout, Wt::WString::tr("search_from_age").toUTF8()); - minAgeEdit->setRange(18, 150); - minAgeEdit->setValue(18); - minAgeEdit->changed().connect([=, this] { searchFields.minAge = minAgeEdit->value(); }); - auto maxAgeEdit = addSearchItemLine(contentLayout, Wt::WString::tr("search_to_age").toUTF8()); - maxAgeEdit->setRange(18, 150); - maxAgeEdit->setValue(150); - maxAgeEdit->changed().connect([=, this] { searchFields.maxAge = maxAgeEdit->value(); }); - return {minAgeEdit, maxAgeEdit}; -} - -std::pair App::setupCountryDropDown(Wt::WVBoxLayout *contentLayout) { - auto countryOpenList = addSearchItemLine(contentLayout, Wt::WString::tr("search_country").toUTF8()); - countryOpenList->addNew(Wt::WString::tr("search_all")); - countryOpenList->setStyleClass("selectBoxes-drop-down-trigger"); - auto countryDropDown = countryOpenList->addNew(); - countryDropDown->setStyleClass("selectBoxes-dropdown"); - countryOpenList->setToolTip(Wt::WString::tr("search_country_tooltip")); - std::map countries = server_.countries(); - addItem("All", countryDropDown, countryOpenList, &searchFields.countries, true); - addItem(country, countryDropDown, countryOpenList, &searchFields.countries); - for (const auto &itemCountry: countries) { - if (itemCountry.first.toUTF8() != country) { - addItem(itemCountry.first.toUTF8(), countryDropDown, countryOpenList, &searchFields.countries); - } - } - return {countryOpenList, countryDropDown}; -} - -std::pair App::setupGendersDropDown(Wt::WVBoxLayout *contentLayout) { - auto gendersOpenList = addSearchItemLine(contentLayout, Wt::WString::tr("search_genders").toUTF8()); - gendersOpenList->addNew(Wt::WString::tr("search_all")); - gendersOpenList->setStyleClass("selectBoxes-drop-down-trigger"); - auto gendersDropDown = gendersOpenList->addNew(); - gendersDropDown->setStyleClass("selectBoxes-dropdown"); - gendersOpenList->setToolTip(Wt::WString::tr("search_genders_tooltip")); - addItem("All", gendersDropDown, gendersOpenList, &searchFields.gender, true); - std::map swappedGenders; - for (const auto& pair : genders_) { - swappedGenders[pair.second] = pair.first; - } - for (const auto &itemGender: swappedGenders) { - addItem(itemGender.first.toUTF8(), gendersDropDown, gendersOpenList, &searchFields.gender); - } - return {gendersOpenList, gendersDropDown}; -} - - -void App::addItem(const std::string& country, Wt::WContainerWidget *dropDownContainer, Wt::WContainerWidget *container, std::unordered_set *saveItems, bool isSelected) { - auto menuItem = dropDownContainer->addNew(country); - menuItem->changed().connect([=, this]() mutable { itemChanged(menuItem, dropDownContainer, container, saveItems); }); - menuItem->setInline(false); - if (isSelected) { - menuItem->setChecked(); - } -} - -void App::addUserItemToLayout(Wt::WVBoxLayout *layout, Wt::Json::Object userObject) { - auto userName = (std::string)userObject["name"]; - auto line = layout->addWidget(std::make_unique()); - auto flag = line->addNew(Wt::WLink(std::make_shared("../docroot/flags/" + (std::string)userObject["isoCountryCode"] + ".png"))); - flag->setMaximumSize(Wt::WLength(60, Wt::LengthUnit::Pixel), Wt::WLength(20, Wt::LengthUnit::Pixel)); - flag->setHeight(Wt::WLength(2, Wt::LengthUnit::FontEm)); - flag->setStyleClass("flag-icon"); - flag->setToolTip(userObject["country"]); - line->addNew(Wt::WString("{1} ({2})").arg(userName).arg((int)userObject["age"])); - line->setStyleClass(Wt::WString("userlist-item userlist-gender-{1}").arg((std::string)userObject["gender"])); - line->setHeight(Wt::WLength(2, Wt::LengthUnit::FontEm)); - line->setPadding(Wt::WLength(3, Wt::LengthUnit::Pixel)); - line->clicked().connect([=, this]() { - requestConversation(userName); - }); - -} - -std::unordered_set App::gendersListToShortGendersList(std::unordered_set gendersList) { - std::unordered_set result; - for (const auto &gender: gendersList) { - result.insert(genderShortOfGender(gender)); - } - return result; -} - -std::string App::genderShortOfGender(const std::string incomingGender) { - for (const auto &genderItem: genders_) { - if (incomingGender == genderItem.second) { - return genderItem.first.toUTF8(); - } - } - return incomingGender; -} - -void App::extendSearchResultIfNeeded(Wt::Json::Object broadcast) { - if (searchFields.outputContainer == nullptr) { - return; - } - auto user = (Wt::Json::Object)broadcast["data"]; - auto age = (int)user["age"]; - auto country = (std::string)user["country"]; - auto gender = (std::string)user["gender"]; - if ( - (searchFields.userName.toUTF8() == "" || ((std::string)user["name"]).find(searchFields.userName.toUTF8()) != std::string::npos) - && (searchFields.minAge <= age) - && (searchFields.maxAge >= age) - && (searchFields.countries.contains("All") || searchFields.countries.contains(country) || searchFields.countries.size() == 0) - && (searchFields.gender.contains("All") || searchFields.gender.contains(gender) || searchFields.gender.size() == 0) - ) { - startSearch(); - } -} - -void App::removeUserFromSearch(Wt::Json::Object) { - if (searchFields.outputContainer == nullptr) { - return; - } - startSearch(); -} - -void App::requestHistory() { - setActivity(); - server_.sendHistory(sessionId()); -} - -void App::showHistory(Wt::Json::Object broadcast) { - try { - contentContainer_->clear(); - auto headerWidget = contentContainer_->addNew(Wt::WString::tr("history_title"), Wt::TextFormat::UnsafeXHTML); - headerWidget->setInline(false); - auto listWidget = contentContainer_->addNew(); - auto dataArray = (Wt::Json::Array)broadcast["data"]; - - if (dataArray.empty()) { - // Keine bisherigen Unterhaltungen vorhanden - contentContainer_->addNew(Wt::WString::tr("history_empty")); - } else { - for (Wt::Json::Object user: dataArray) { - auto userName = std::make_shared((std::string)user["name"]); - auto tableCell = listWidget->elementAt(listWidget->rowCount(), 0); - tableCell->addNew(Wt::WString("{1} ({2})").arg(*userName).arg((int)user["age"])); - tableCell->setStyleClass(Wt::WString("user-conversation-info userlist-item userlist-gender-{1}").arg((std::string)user["gender"])); - tableCell->clicked().connect([=, this]() { - requestConversation(*userName); - }); - } - } - } catch(std::exception &e) { - std::cout << e.what() << std::endl; - } - - triggerUpdate(); -} - -void App::connectionTimedOut() { - showLogin(); - loginTimer_->stop(); - timeoutRemainingTimer_->stop(); - menuContainer_->clear(); - userListContainer_->clear(); - triggerUpdate(); -} - -void App::addLoginTimeView() { - auto loggedinTimeWidget = menuContainer_->addNew(); - loggedinTimeWidget->setStyleClass("menu-info-text"); - loginTimer_ = root()->addChild(std::make_unique()); - loginTimer_->setInterval(std::chrono::seconds(1)); - auto loggedInRefresh = [=, this]() { - auto currentLoginSeconds = currentlyLoggedInSeconds(); - int hours = currentLoginSeconds / 3600; - int minutes = (currentLoginSeconds % 3600) / 60; - std::stringstream elapsedTimeStream; - elapsedTimeStream << std::setw(2) << std::setfill('0') << hours << ":" - << std::setw(2) << std::setfill('0') << minutes << " h"; - std::string elapsedTimeString = elapsedTimeStream.str(); - loggedinTimeWidget->setText(Wt::WString::tr("menu_in_chat_for").arg(elapsedTimeString)); - }; - loginTimer_->timeout().connect(loggedInRefresh); - loggedInRefresh(); - loginTimer_->start(); -} - -void App::addTimeoutView() { - auto timeoutRemainingWidget = menuContainer_->addNew(); - timeoutRemainingWidget->setStyleClass("menu-info-text"); - timeoutRemainingTimer_ = root()->addChild(std::make_unique()); - timeoutRemainingTimer_->setInterval(std::chrono::milliseconds(500)); - auto timeoutRemainingRefresh = [=, this]() { - auto remainingLoginSeconds = remainingSecondsToTimeout(); - int minutes = remainingLoginSeconds / 60; - int seconds = (remainingLoginSeconds % 60); - std::stringstream remainingTimeStream; - remainingTimeStream << std::setw(2) << std::setfill('0') << minutes << ":" - << std::setw(2) << std::setfill('0') << seconds << " m"; - std::string elapsedTimeString = remainingTimeStream.str(); - timeoutRemainingWidget->setText(Wt::WString::tr("menu_timeout_in").arg(elapsedTimeString)); - }; - timeoutRemainingTimer_->timeout().connect(timeoutRemainingRefresh); - timeoutRemainingRefresh(); - timeoutRemainingTimer_->start(); -} - -void App::showPartnerSites() { - setMetaTags(); - contentContainer_->clear(); - contentContainer_->setOverflow(Wt::Overflow::Auto); - auto linkContainer = contentContainer_->addNew(); - auto contentLayout = linkContainer->setLayout(std::make_unique()); - contentLayout->addWidget(std::make_unique("

Partners

")); - if (userName == "") { - auto hpLink = contentLayout->addWidget(std::make_unique("Back to main page")); - hpLink->clicked().connect(this, &App::showLogin); - } - rapidcsv::Document doc("../docroot/links.csv"); - for (size_t i = 0; i < doc.GetRowCount(); ++i) { - auto url = doc.GetCell(1, i); - auto name = doc.GetCell(0, i); - auto link = Wt::WLink(url); - link.setTarget(Wt::LinkTarget::NewWindow); - contentLayout->addWidget(std::make_unique(link, name)); - } -} - -void App::itemChanged(Wt::WCheckBox *item, Wt::WContainerWidget *dropDownContainer, Wt::WContainerWidget *container, std::unordered_set *saveItems) { - saveItems->clear(); - bool unselect = (item->text() == "All" && item->isChecked()); - for (auto &widgetItem: dropDownContainer->children()) { - auto widgetCheckBox = static_cast(widgetItem); - if (unselect && widgetCheckBox->text() != "All" && widgetCheckBox->isChecked()) { - widgetCheckBox->setChecked(false); - } - if (widgetCheckBox->isChecked()) { - saveItems->insert(widgetCheckBox->text().toUTF8()); - } - } - if (saveItems->size() > 1 && saveItems->find("All") != saveItems->end()) { - saveItems->erase(saveItems->find("All")); - static_cast(dropDownContainer->children().at(0))->setChecked(false); - } - if (saveItems->empty()) { - saveItems->insert("All"); - } - std::string result; - for (const auto &selected: *saveItems) { - result += (result.empty() ? "" : ", ") + selected; - } - auto outputWidget = (Wt::WText*)container->children().at(0); - outputWidget->setText(result); -} - -void App::restoreSearchFields(Wt::WContainerWidget *searchResultContainer, Wt::WLineEdit *userNameEdit, Wt::WSpinBox *minAgeEdit, - Wt::WSpinBox *maxAgeEdit, Wt::WContainerWidget *countryDropDownContainer, Wt::WContainerWidget *gendersDropDownContainer, - Wt::WContainerWidget *countryOpenList, Wt::WContainerWidget *gendersOpenList) { - if (!searchFields.set) { - searchFields = Search(searchResultContainer); - } else { - userNameEdit->setValueText(searchFields.userName); - minAgeEdit->setValue(searchFields.minAge); - maxAgeEdit->setValue(searchFields.maxAge); - for (auto countryWidget: countryDropDownContainer->children()) { - auto countryCheckBox = (Wt::WCheckBox*)countryWidget; - countryCheckBox->setChecked(searchFields.countries.find(countryCheckBox->text().toUTF8()) != searchFields.countries.end()); - } - itemChanged((Wt::WCheckBox*)*countryDropDownContainer->children().begin(), countryDropDownContainer, countryOpenList, &searchFields.countries); - for (auto genderWidget: gendersDropDownContainer->children()) { - auto genderCheckBox = (Wt::WCheckBox*)genderWidget; - genderCheckBox->setChecked(searchFields.gender.find(genderCheckBox->text().toUTF8()) != searchFields.gender.end()); - } - itemChanged((Wt::WCheckBox*)*gendersDropDownContainer->children().begin(), gendersDropDownContainer, gendersOpenList, &searchFields.gender); - startSearch(); - } -} - -Wt::WLineEdit *App::setupNameSearchField(Wt::WVBoxLayout *contentLayout) { - auto userNameEdit = addSearchItemLine(contentLayout, Wt::WString::tr("search_username_includes").toUTF8()); - userNameEdit->changed().connect([=, this] { searchFields.userName = userNameEdit->text().trim(); }); - return userNameEdit; -} - -Wt::WContainerWidget *App::setupSearchButton(Wt::WVBoxLayout *contentLayout) { - auto searchButton = addSearchItemLine(contentLayout, ""); - searchButton->setText(Wt::WString::tr("search_button")); - auto searchResultContainer = contentLayout->addWidget(std::make_unique(), 1); - searchResultContainer->addNew(Wt::WString::tr("search_no_results")); - searchButton->clicked().connect(this, &App::startSearch); - return searchResultContainer; -} - -void App::startSearch() { - if (searchFields.minAge > searchFields.maxAge) { - searchFields.outputContainer->clear(); - searchFields.outputContainer->addNew(Wt::WString::tr("search_min_age_error")); - return; - } - server_.userSearch(sessionId(), searchFields.userName.toUTF8(), searchFields.minAge, searchFields.maxAge, - searchFields.countries, gendersListToShortGendersList(searchFields.gender), userName); -} - -void App::showSearch(Wt::Json::Object broadcast) { - if (!searchFields.outputContainer) { - return; - } - searchFields.outputContainer->clear(); - auto searchResult = (Wt::Json::Array)broadcast["data"]; - if (searchResult.size() == 0) { - searchFields.outputContainer->addNew(Wt::WString::tr("search_no_results")); - } - auto searchListContainer = searchFields.outputContainer->addNew(); - auto searchList = searchListContainer->setLayout(std::make_unique()); - searchListContainer->setOverflow(Wt::Overflow::Auto); - for (const Wt::Json::Object &searchItem: searchResult) { - addUserItemToLayout(searchList, searchItem); - } - triggerUpdate(); -} - -void App::openInbox() { - setActivity(); - currentConversationWith_ = ""; - contentContainer_->clear(); - contentContainer_->addNew("

Inbox

"); - inboxOpen_ = true; - searchFields.outputContainer = nullptr; - server_.sendOpenConversations(sessionId()); -} - -void App::incomingBroadcast() { - auto broadcasts = server_.getBroadcastsForSession(sessionId()); - for (Wt::Json::Object &broadcast: broadcasts) { - if (broadcast["type"] == "userlist") { - updateUserlist(broadcast["data"], (int)broadcast["count"]); - } else if (broadcast["type"] == "logout") { - server_.disconnect(this); - } else if (broadcast["type"] == "messagequeue") { - renderConversation(broadcast); - } else if (broadcast["type"] == "unread-chats") { - showUnreadMessages(broadcast); - } else if (broadcast["type"] == "openconversations" && inboxOpen_) { - showOpenInbox(broadcast); - } else if (broadcast["type"] == "userinfo") { - updateUserinfo(broadcast); - } else if (broadcast["type"] == "system") { - systemEvent(broadcast); - } else if (broadcast["type"] == "conversation-start") { - showConversation(broadcast); - } else if (broadcast["type"] == "search-result") { - showSearch(broadcast); - } else if (broadcast["type"] == "newuser") { - extendSearchResultIfNeeded(broadcast); - } else if (broadcast["type"] == "userleft") { - removeUserFromSearch(broadcast); - } else if (broadcast["type"] == "history") { - showHistory(broadcast); - } else if (broadcast["type"] == "timedout") { - connectionTimedOut(); - } - } -} - -void App::startChat() { - createMenu(); - contentContainer_->clear(); - contentContainer_->addNew(Wt::WString::tr("introduction"), Wt::TextFormat::UnsafeXHTML); - setLoggedIn(); -} - -void App::createUserListContainer(Wt::WHBoxLayout *layout) { - userListContainer_ = layout->addWidget(std::make_unique()); - userListContainer_->setStyleClass("userlist"); -} - -void App::createContentContainer(Wt::WHBoxLayout *layout) { - contentContainer_ = layout->addWidget(std::make_unique()); - contentContainer_->setStyleClass("content"); -} - -void App::updateLocation() { - CURL *curl; - curl_global_init(CURL_GLOBAL_DEFAULT); - curl = curl_easy_init(); - try { - if (curl) { - std::string userIP = getUserIP(); - std::string apiUrl = buildApiUrl(userIP); - setCurlOptions(curl, apiUrl); - performCurlRequest(curl); - processCurlResponse(); - curl_easy_cleanup(curl); - } - } catch (std::exception &e) { - handleException(e); - } - curl_global_cleanup(); -} - -std::string App::getUserIP() { - std::string userIP = env_.clientAddress(); - return (userIP == "127.0.0.1") ? "77.189.91.196" : userIP; -} - -std::string App::buildApiUrl(const std::string &userIP) { - return "http://ip-api.com/xml/" + userIP; -} - -void App::setCurlOptions(CURL *curl, const std::string &apiUrl) { - curl_easy_setopt(curl, CURLOPT_URL, apiUrl.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &App::WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData_); -} - -void App::performCurlRequest(CURL *curl) { - CURLcode res = curl_easy_perform(curl); - if (res != CURLE_OK) { - throw std::runtime_error("Curl request failed"); - } -} - -void App::processCurlResponse() { - xmlDocPtr doc = xmlReadMemory(responseData_.c_str(), responseData_.size(), NULL, NULL, 0); - if (doc != NULL) { - parseXmlDocument(doc); - xmlFreeDoc(doc); - } -} - -void App::parseXmlDocument(xmlDocPtr doc) { - xmlNodePtr countryCodeNode = xmlDocGetRootElement(doc)->children; - while (countryCodeNode != nullptr) { - processXmlNode(countryCodeNode); - countryCodeNode = countryCodeNode->next; - } -} - -void App::processXmlNode(xmlNodePtr node) { - if (xmlStrEqual(node->name, BAD_CAST "countryCode")) { - isoCountryCode = reinterpret_cast(xmlNodeGetContent(node)); - } else if (xmlStrEqual(node->name, BAD_CAST "country")) { - country = reinterpret_cast(xmlNodeGetContent(node)); - } -} - -void App::handleException(const std::exception &e) { - std::cerr << "Error: " << e.what() << std::endl; -} - -bool App::isNickAllowed(const std::string& nick) { - std::string lowercaseNick = nick; - std::transform(lowercaseNick.begin(), lowercaseNick.end(), lowercaseNick.begin(), ::tolower); - return (lowercaseNick != "self") && - (lowercaseNick != "system") && - (lowercaseNick != "you") && - std::all_of(notAllowedNickPhrases_.begin(), notAllowedNickPhrases_.end(), - [&](const std::string& phrase) { - std::string lowercasePhrase = phrase; - std::transform(lowercasePhrase.begin(), lowercasePhrase.end(), lowercasePhrase.begin(), ::tolower); - return lowercaseNick.find(lowercasePhrase) == std::string::npos; - }); -} - -template -Class *App::addSearchItemLine(Wt::WVBoxLayout *layout, std::string label, std::unique_ptr additionalItem) { - auto lineContainer = layout->addWidget(std::make_unique()); - lineContainer->setStyleClass("search-line"); - lineContainer->setPositionScheme(Wt::PositionScheme::Relative); - auto lineLayout = lineContainer->setLayout(std::make_unique()); - lineContainer->setPadding(Wt::WLength("0")); - lineContainer->setMargin(Wt::WLength("0")); - auto labelWidget = lineLayout->addWidget(std::make_unique(label)); - labelWidget->setWidth(Wt::WLength(9, Wt::LengthUnit::FontEm)); - lineLayout->setContentsMargins(0, 0, 0, 0); - lineLayout->setSpacing(0); - auto input = std::make_unique(); - auto returnInput = input.get(); - lineLayout->addWidget(std::move(input), 1); - if (additionalItem) { - lineLayout->addWidget(std::move(additionalItem)); - lineContainer->setOverflow(Wt::Overflow::Visible); - } - return returnInput; -} diff --git a/src/app.h b/src/app.h deleted file mode 100644 index 1f53080..0000000 --- a/src/app.h +++ /dev/null @@ -1,229 +0,0 @@ -#ifndef APP_H -#define APP_H - -#include -#include -#include "broadcast.h" -#include "curl/curl.h" -#include -#include -#include - -namespace Magick { - class Image; - class Blob; -} - -class App : public Wt::WApplication, public Client { -public: - App(const Wt::WEnvironment& env, Broadcast& server); - ~App(); - -private: - const std::string adminName {"comiciusadmin"}; - const std::string adminPassword {"p3Lv9!7?+Qq"}; - - // Key: short gender code used in the protocol, Value: translation id for display text - std::map genders_ { - {"F", "gender_female"}, - {"M", "gender_male"}, - {"P", "gender_pair"}, - {"TF", "gender_trans_mf"}, - {"TM", "gender_trans_fm"} - }; - std::vector notAllowedNickPhrases_ { - "whore", - "hitler", - "nazi", - "admin" - }; - struct Smiley { - std::string code; - std::string tooltip; - Smiley()=default; - Smiley(std::string code_, std::string tooltip_): code(code_), tooltip(tooltip_) {}; - }; - std::unordered_map smileys_ { - {":)", Smiley("1F642", "Smile")}, - {":D", Smiley("1F600", "Laugh")}, - {":(", Smiley("1F641", "Sad")}, - {";)", Smiley("1F609", "Twinkle")}, - {":p", Smiley("1F60B", "Tongue")}, - {";p", Smiley("1F61C", "Twinkle tongue")}, - {"O)", Smiley("1F607", "Angel")}, - {":*", Smiley("1F617", "Kiss")}, - {"(h)", Smiley("1FA77", "Heart")}, - {"xD", Smiley("1F602", "Laughing hard")}, - {":@", Smiley("1F635", "Confused")}, - {":O", Smiley("1F632", "Surprised")}, - {":3", Smiley("1F63A", "Cat face")}, - {":|", Smiley("1F610", "Neutral")}, - {":/", Smiley("1FAE4", "Skeptical")}, - {":#", Smiley("1F912", "Sick")}, - {"#)", Smiley("1F973", "Partied")}, - {"%)", Smiley("1F974", "Drunk")}, - {"(t)", Smiley("1F44D", "Thumbs up")}, - {":'(", Smiley("1F622", "Cry")} - }; - struct Search { - Wt::WContainerWidget *outputContainer = nullptr; - Wt::WString userName{""}; - int minAge{18}; - int maxAge{150}; - std::unordered_set gender; - std::unordered_set countries; - Search()=default; - Search(Wt::WContainerWidget *outputContainer_): - outputContainer(outputContainer_) { - set = true; - gender.insert("All"); - countries.insert("All"); - } - bool set = false; - }; - Wt::WString smileyPlaceholder_ = "&#x{1};"; - const Wt::WEnvironment &env_; - Broadcast &server_; - Wt::WContainerWidget *menuContainer_; - Wt::WContainerWidget *userListContainer_; - Wt::WContainerWidget *contentContainer_; - Wt::JSignal updateLocationSignal_; - std::string responseData_{""}; - Wt::WPushButton *inbox_; - std::vector searchResults_; - std::string currentConversationWith_{""}; - bool inboxOpen_{false}; - std::shared_ptr messageCursorPosition_; - std::unique_ptr messageReceived_; - Search searchFields; - Wt::WTimer *loginTimer_; - Wt::WTimer *timeoutRemainingTimer_; - bool isLoggedInAsAdmin {false}; - void setMetaTags(); - void initLocale(); - Wt::WLocale determineLocaleFromBrowser() const; - void initApp(); - void reSetUser(); - Wt::WVBoxLayout *createVerticalLayout(); - Wt::WHBoxLayout *createActionLayout(Wt::WVBoxLayout *verticalContainer); - void createUserListContainer(Wt::WHBoxLayout *layout); - void createContentContainer(Wt::WHBoxLayout *layout); - void createHeadContainer(Wt::WVBoxLayout *layout); - void createMenuContainer(Wt::WVBoxLayout *layout); - void showLogin(); - void incomingBroadcast(); - void startChat(); - void updateLocation(); - static size_t WriteCallback(void *contents, size_t size, size_t nmemb, std::string *output) { - size_t totalSize = size * nmemb; - output->append(reinterpret_cast(contents), totalSize); - return totalSize; - }; - void populateCountryComboBox(Wt::WComboBox *countryWidget); - void handleLogin(Wt::WLineEdit *userName, Wt::WComboBox *countryWidget, Wt::WSpinBox *ageWidget, Wt::WComboBox *genderWidget); - void populateGenderComboBox(Wt::WComboBox *genderWidget); - std::string getGenderShortByGender(std::string gender); - void updateUserlist(Wt::Json::Array userList, int size); - std::vector sortUserList(Wt::Json::Array unsortedUserList); - void createMenu(); - void addLeaveButton(); - void logout(); - void addHistoryButton(); - void addIdentifier(); - void addSearchButton(); - void addInboxButton(); - void showSearchWindow(); - void startSearch(); - void showSearch(Wt::Json::Object broadcast); - template Class *addSearchItemLine(Wt::WVBoxLayout *layout, std::string label, std::unique_ptr additionalItem = nullptr); - Wt::WContainerWidget *addSearchItemContainer(Wt::WVBoxLayout *layout, std::string label); - void openInbox(); - bool isNickAllowed(const std::string &nick); - bool compareJsonObjects(const Wt::Json::Object &obj1, const Wt::Json::Object &obj2); - void requestConversation(std::string conversationWith); - void showConversation(Wt::Json::Object data); - void sendMessage(Wt::WLineEdit *inputLine); - void renderConversation(Wt::Json::Object conversation); - void showUnreadMessages(Wt::Json::Object data); - void showOpenInbox(Wt::Json::Object data); - void updateUserinfo(Wt::Json::Object data); - std::unique_ptr createSmileysBar(Wt::WLineEdit *inputLine, std::shared_ptr cursorPosition); - void toggleSmileysBar(Wt::WContainerWidget *smileyBar); - void systemEvent(Wt::Json::Object broadcast); - void addStartChatButton(Wt::WGridLayout *contentGrid, Wt::WLineEdit *userName, Wt::WComboBox *country, Wt::WSpinBox *age, Wt::WComboBox *gender); - Wt::WComboBox *addCountrySelection(Wt::WGridLayout *contentGrid); - Wt::WSpinBox *addAgeInput(Wt::WGridLayout *contentGrid); - Wt::WLineEdit *addUsernameInput(Wt::WGridLayout *contentGrid); - Wt::WComboBox *addGenderSelection(Wt::WGridLayout *contentGrid); - void createLoginContainer(); - void connectToServer(); - void setUserData(const std::string &nick, Wt::WComboBox *countryWidget, Wt::WSpinBox *ageWidget, Wt::WComboBox *genderWidget); - void validateAge(Wt::WSpinBox *ageWidget); - bool isGenderSelected(Wt::WComboBox *genderWidget); - void validateGender(Wt::WComboBox *genderWidget); - bool isNameAlreadyInUse(const std::string &nick); - bool isInvalidName(const std::string &nick); - void validateName(const std::string &nick); - std::string extractTrimmedUserName(Wt::WLineEdit *userName); - Wt::WPushButton *createSendButton(Wt::WHBoxLayout *inputLayout, Wt::WLineEdit *inputLine); - Wt::WContainerWidget *createSmileyBar(Wt::WContainerWidget *parent, Wt::WLineEdit *inputLine, std::shared_ptr cursorPosition); - Wt::WContainerWidget *createSmileyButton(Wt::WHBoxLayout *inputLayout, Wt::WLineEdit *inputLine, std::shared_ptr cursorPosition); - Wt::WImage *createSendImageButton(Wt::WHBoxLayout *inputLayout); - Wt::WLineEdit *createInputLine(Wt::WHBoxLayout *inputLayout); - Wt::WContainerWidget *createInputContainer(Wt::WVBoxLayout *layout); - std::unique_ptr createInfoText(Wt::Json::Object userData); - Wt::WContainerWidget *createInfoWidget(Wt::WVBoxLayout *layout, Wt::Json::Object userData); - void setupConversationUI(Wt::Json::Object userData); - bool shouldShowConversation(Wt::Json::Object userData); - Wt::Json::Object extractUserData(Wt::Json::Object data); - std::unique_ptr createBlockButton(Wt::Json::Object userData); - std::string replaceSmileys(std::string outputText); - void renderChatLine(Wt::Json::Object &line, Wt::Json::Object conversation, Wt::WContainerWidget *outputContainer); - std::string getChatLineWriter(Wt::Json::Object &line, Wt::Json::Object conversation); - void renderChatLines(Wt::Json::Object conversation, Wt::WContainerWidget *outputContainer); - void updateOutputContainer(Wt::Json::Object conversation); - void handleException(const std::exception &e); - void processXmlNode(xmlNodePtr node); - void parseXmlDocument(xmlDocPtr doc); - void processCurlResponse(); - void performCurlRequest(CURL *curl); - void setCurlOptions(CURL *curl, const std::string &apiUrl); - std::string buildApiUrl(const std::string &userIP); - std::string getUserIP(); - Wt::WWebWidget *createImageElement(Wt::Json::Object &line, const std::string &writer, Wt::WContainerWidget *outputContainer, std::string id); - Wt::WWebWidget *createTextElement(const std::string &writer, const std::string &text, Wt::WContainerWidget *outputContainer, std::string id); - void createImprintContainer(Wt::WVBoxLayout *containerLayout); - Wt::WContainerWidget *setupSearchButton(Wt::WVBoxLayout *contentLayout); - void restoreSearchFields(Wt::WContainerWidget *searchResultContainer, Wt::WLineEdit *userNameEdit, Wt::WSpinBox *minAgeEdit, Wt::WSpinBox *maxAgeEdit, Wt::WContainerWidget *countryDropDownContainer, Wt::WContainerWidget *gendersDropDownContainer, Wt::WContainerWidget *countryOpenList, Wt::WContainerWidget *gendersOpenList); - Wt::WLineEdit *setupNameSearchField(Wt::WVBoxLayout *contentLayout); - std::pair setupGendersDropDown(Wt::WVBoxLayout *contentLayout); - std::pair setupCountryDropDown(Wt::WVBoxLayout *contentLayout); - std::pair setupSearchFields(Wt::WVBoxLayout *contentLayout); - Wt::WVBoxLayout *resetSearchFields(); - void itemChanged(Wt::WCheckBox *item, Wt::WContainerWidget *dropDownContainer, Wt::WContainerWidget *openButton, std::unordered_set *saveItems); - void addItem(const std::string &country, Wt::WContainerWidget *dropDownContainer, Wt::WContainerWidget *container, std::unordered_set *saveItems, bool isSelected = false); - void addUserItemToLayout(Wt::WVBoxLayout *layout, Wt::Json::Object userObject); - std::unordered_set gendersListToShortGendersList(std::unordered_set gendersList); - std::string genderShortOfGender(const std::string incomingGender); - void extendSearchResultIfNeeded(Wt::Json::Object broadcast); - void removeUserFromSearch(Wt::Json::Object broadcast); - void requestHistory(); - void showHistory(Wt::Json::Object broadcast); - void connectionTimedOut(); - void addLoginTimeView(); - void addTimeoutView(); - void showPartnerSites(); - void sendImage(); - void imageUploaded(Wt::WFileUpload *fileWidget, std::shared_ptr localImage, Wt::WImage *image, Wt::WPushButton *okButton); - std::list resizeImages(std::list& images, int maxWidth, int maxHeight); - bool isAnimatedGIF(const Magick::Blob &blob); - void showStandardPage(); - void onInternalPathChanged(const std::string &path); - void showAdminPage(std::string page); - void showAdminLogin(std::string page); - void showPageNotExists(); - void showAdminLogins(); - void showAdminStarts(); -}; - -#endif // APP_H diff --git a/src/broadcast.cpp b/src/broadcast.cpp deleted file mode 100644 index 985085c..0000000 --- a/src/broadcast.cpp +++ /dev/null @@ -1,770 +0,0 @@ -#include "broadcast.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -Broadcast::Broadcast(Wt::WServer *server): - wtServer_(server) { - downloadCountries(); - thread_ = std::thread(std::bind(&Broadcast::run, this)); - lastTimeoutCheck_ = Wt::WDateTime::currentDateTime(); - checkAndLogStart(); -} - -void Broadcast::checkAndLogStart() { - const std::string logFilePath = "/opt/ypchat/logs/starts.log"; - std::ifstream infile(logFilePath); - if (!infile.good()) { - std::ofstream outfile(logFilePath); - outfile.close(); - } - infile.close(); - Wt::WDateTime now = Wt::WDateTime::currentDateTime(); - std::string timestamp = now.toString("yyyy-MM-dd HH:mm:ss").toUTF8(); - std::ofstream outfile(logFilePath, std::ios_base::app); - if (outfile.is_open()) { - outfile << timestamp << std::endl; - outfile.close(); - } else { - std::cerr << "Fehler beim Öffnen der Datei: " << logFilePath << std::endl; - } -} - -Broadcast::~Broadcast() { - stop_ = true; - thread_.join(); -} - -void Broadcast::connect(Client *client, const std::function &fct) { - connections_.push_back(std::make_unique(Wt::WApplication::instance()->sessionId(), client, fct)); - auto userlistBroadcast = createUserList(); - auto newUserBroadcast = Wt::Json::Object{ - {"type", "newuser"}, - {"data", client->json()}, - }; - for (auto &connection: connections_) { - connection->addBroadcast(userlistBroadcast); - connection->addBroadcast(newUserBroadcast); - } - logClientLogin(client->json()); -} - -void Broadcast::logClientLogin(const Wt::Json::Object &clientJson) { - const std::string logFilePath = "../logs/logins.log"; - Wt::Json::Array logArray; - std::ifstream infile(logFilePath); - if (infile.is_open()) { - std::stringstream buffer; - buffer << infile.rdbuf(); - std::string fileContent = buffer.str(); - infile.close(); - if (!fileContent.empty()) { - Wt::Json::parse(fileContent, logArray); - } - } - - // Zeitstempel hinzufügen - Wt::Json::Object logEntry = clientJson; - Wt::WDateTime now = Wt::WDateTime::currentDateTime(); - std::string timestamp = now.toString("yyyy-MM-dd HH:mm:ss").toUTF8(); - logEntry["timestamp"] = Wt::Json::Value(timestamp); - - logArray.push_back(logEntry); - std::ofstream outfile(logFilePath); - if (outfile.is_open()) { - outfile << Wt::Json::serialize(logArray) << std::endl; - outfile.close(); - } else { - std::cerr << "Fehler beim Öffnen der Datei: " << logFilePath << std::endl; - } -} - -void Broadcast::disconnect(Client *client) { - std::unique_lock lock(mutex_); - for (unsigned int i = 0; i < connections_.size(); ++ i) { - if (connections_[i]->client() == client) { - connections_.erase(connections_.begin() + i); - break; - } - } - Wt::Json::Object leftBroadcast{ - {"type", "userleft"}, - {"data", Wt::Json::Value(client->userName)} - }; - auto userlistBroadcast = createUserList(); - for (auto &connection: connections_) { - connection->addBroadcast(userlistBroadcast); - connection->addBroadcast(leftBroadcast); - } - auto sessionId = sessionIdForUserName(client->userName); - for (auto it = conversations_.begin(); it != conversations_.end();) { - if (it->first.find(sessionId + "/") == 0 || it->first.rfind("/" + sessionId) == it->first.size() - sessionId.length() - 1) { - it = conversations_.erase(it); - } else { - ++it; - } - } -} - -void Broadcast::addToDisconnectList(Client *client) { - toDisconnect_.push_back(client); -} - -Wt::Json::Object Broadcast::reSetUser(std::string oldSessionId, std::string newSessionId) { - for (auto& connection : connections_) { - if (connection->setSessionId(oldSessionId, newSessionId)) { - Wt::Json::Object userData { - {"gender", Wt::Json::Value(connection->gender())}, - {"country", Wt::Json::Value(connection->country())}, - {"iso-country-code", Wt::Json::Value(getCountryIsoCodeByCountry(connection->country()))}, - {"username", Wt::Json::Value(connection->userName())}, - {"age", Wt::Json::Value(connection->age())}, - }; - reSetSessionIdInMessages(oldSessionId, newSessionId); - return userData; - } - } - return {}; -} - -int Broadcast::count() const { - std::unique_lock lock(mutex_); - return connections_.size(); -} - -std::string Broadcast::userNameForSessionId(std::string sessionId) { - for (const auto &connection: connections_) { - if (sessionId == connection->sessionId()) { - return connection->userName(); - } - } - return ""; -} - -std::string Broadcast::sessionIdForUserName(std::string userName) { - for (const auto &connection: connections_) { - if (userName == connection->userName()) { - return connection->sessionId(); - } - } - return ""; -} - -Wt::Json::Object Broadcast::userForSessionId(std::string sessionId) { - for (const auto &connection: connections_) { - if (sessionId == connection->sessionId()) { - return connection->client()->json(); - } - } - return Wt::Json::Object{}; -} - -Wt::Json::Object Broadcast::userForUserName(std::string userName) { - for (const auto &connection: connections_) { - if (userName == connection->userName()) { - return connection->client()->json(); - } - } - return Wt::Json::Object{}; -} - -void Broadcast::changeClientForSessionId(std::string sessionId, Client *client) { - std::unique_lock lock(mutex_); - for (auto &connection: connections_) { - if (sessionId == connection->sessionId()) { - connection->setClient(client); - } - } -} - -bool Broadcast::nameIsFree(std::string userNameToCheck) { - std::unique_lock lock(mutex_); - for (auto &connection: connections_) { - if (connection->userName() == userNameToCheck) { - return false; - } - } - return true; -} - -void Broadcast::run() { - Wt::Json::Object timedoutBroadcast { - {"type", "timedout"}, - {"data", "dummy"} - }; - for (;;) { - std::this_thread::sleep_for(std::chrono::milliseconds(200)); - for (auto clientToDisconnect: toDisconnect_) { - disconnect(clientToDisconnect); - } - toDisconnect_.clear(); - if (stop_) { - return; - } - std::unique_lock lock(mutex_); - for (auto &connection: connections_) { - if (connection->client()->activitiesTimedOut()) { - auto sessionId = connection->sessionId(); - toDisconnect_.push_back(connection->client()); - addMessageToSessionBroadcast(sessionId, timedoutBroadcast); - } - if (connection->getBroadcasts().size() == 0) { - continue; - } - Wt::WServer::instance()->post(connection->sessionId(), connection->fct()); - } - } -} - -Wt::Json::Object Broadcast::logoutBroadcast() { - return Wt::Json::Object{ - {"type", "logout"} - }; -} - -void Broadcast::downloadCountries() { - CURL* curl = curl_easy_init(); - if (!curl) { - std::cerr << "Failed to initialize CURL." << std::endl; - } - curl_easy_setopt(curl, CURLOPT_URL, countriesDownloadUrl_.toUTF8().c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &responseData_); - CURLcode res = curl_easy_perform(curl); - curl_easy_cleanup(curl); - if (res != CURLE_OK) { - std::cerr << "Failed to download data: " << curl_easy_strerror(res) << std::endl; - } - parseCountriesData(); -} - -std::map Broadcast::countries() { - return countriesMap_; -} - -Wt::WString Broadcast::getCountryIsoCodeByCountry(Wt::WString country) { - for (const auto &countryItem: countriesMap_) { - if (countryItem.first == country) { - return countryItem.second; - } - } - return ""; -} - -std::list Broadcast::getBroadcastsForSession(std::string sessionId) { - std::unique_lock lock(mutex_); - for (auto &connection: connections_) { - if (connection->sessionId() == sessionId) { - return connection->getBroadcasts(true); - } - } - return std::list(); -} - -Wt::Json::Object Broadcast::addMessage(std::string fromSessionId, std::string toUserName, Message message) { - auto toSessionId = sessionIdForUserName(toUserName); - if (toSessionId == "") { - return Wt::Json::Object{ - {"type", "system"}, - {"data", "User left the chat."} - }; - } - auto fromUserName = userNameForSessionId(fromSessionId); - if (blockings_[toUserName].find(fromUserName) != blockings_[toUserName].end()) { - sendBlockedMessage(fromSessionId, toUserName); - } - auto id1 = fromSessionId + "/" + toSessionId; - auto id2 = toSessionId + "/" + fromSessionId; - auto it = conversations_.find(id1); - if (it == conversations_.end()) { - it = conversations_.find(id2); - } - if (it == conversations_.end()) { - MessageQueue messageQueue; - messageQueue.messages.push_back(message); - auto [newIt, success] = conversations_.emplace(id1, MessageQueue{}); - it = newIt; - } - it->second.messages.push_back(message); - it->second.user1Read = false; - it->second.user2Read = false; - sendMessageQueueToSession(fromSessionId, userNameForSessionId(fromSessionId), toUserName, it->second.messages); - sendMessageQueueToSession(toSessionId, userNameForSessionId(fromSessionId), toUserName, it->second.messages); - sendMessageCount(toSessionId); - return Wt::Json::Object{ - {"type", "dummy"}, - {"data", "Message added to users queues."} - }; -} - -Wt::Json::Object Broadcast::addImage(std::string fromSessionId, std::string toUserName, std::shared_ptr image) { - auto message = Message(fromSessionId, image); - return addMessage(fromSessionId, toUserName, message); -} - -void Broadcast::setConversationRead(std::string readingUserSessionId, std::string secondUserName) { - auto toSessionId = sessionIdForUserName(secondUserName); - std::unique_lock lock(mutex_); - if (toSessionId == "") { - return; - } - auto id1 = readingUserSessionId + "/" + toSessionId; - auto id2 = toSessionId + "/" + readingUserSessionId; - auto it = conversations_.find(id1); - if (it != conversations_.end()) { - it->second.user1Read = true; - } else { - it = conversations_.find(id2); - if (it != conversations_.end()) { - it->second.user2Read = true; - } - } - sendMessageCount(readingUserSessionId); -} - -void Broadcast::sendConversation(std::string sessionId1, std::string userName2) { - auto sessionId2 = sessionIdForUserName(userName2); - auto userName1 = userNameForSessionId(sessionId1); - std::unique_lock lock(mutex_); - auto id1 = sessionId1 + "/" + sessionId2; - auto id2 = sessionId2 + "/" + sessionId1; - auto it = conversations_.find(id1); - if (it == conversations_.end()) { - it = conversations_.find(id2); - if (it == conversations_.end()) { - return; - } - } - sendMessageQueueToSession(sessionId1, userName1, userName2, it->second.messages); -} - -void Broadcast::sendOpenConversations(std::string forSessionId) { - std::unique_lock lock(mutex_); - Wt::Json::Array conversationsWithList; - for (const auto &conversation: conversations_) { - const auto key = conversation.first; - const auto data = conversation.second; - if ((key.compare(0, forSessionId.length() + 1, forSessionId + "/") == 0 - && !data.user1Read) - || (key.compare(key.length() - forSessionId.length() - 1, forSessionId.length() + 1, "/" + forSessionId) == 0 - && !data.user2Read)) { - std::size_t found = key.find("/"); - if (found != std::string::npos) { - std::string firstSessionId = key.substr(0, found); - std::string secondSessionId = key.substr(found + 1); - auto userData = forSessionId == firstSessionId ? userForSessionId(secondSessionId) : userForSessionId(firstSessionId); - conversationsWithList.push_back(userData); - } - } - } - Wt::Json::Object data{ - {"type", "openconversations"}, - {"data", conversationsWithList} - }; - addMessageToSessionBroadcast(forSessionId, data); -} - -void Broadcast::sendUserInformation(std::string sendToSessionId, std::string userName, std::string requestingUserName) { - std::unique_lock lock(mutex_); - auto userData = userForUserName(userName); - if (blockings_.find(userName) == blockings_.end()) { - blockings_[userName] = std::set(); - } - userData["blocked"] = (blockings_.find(userName) != blockings_.end() && blockings_[userName].find(requestingUserName) != blockings_[userName].end()); - Wt::Json::Object broadcast{ - {"type", "userinfo"}, - {"data", userData} - }; - addMessageToSessionBroadcast(sendToSessionId, broadcast); -} - -void Broadcast::toggleBlockUser(std::string blockingUserName, std::string blockedUser, std::string blockingUserSessionId) { - std::unique_lock lock(mutex_); - if (blockings_.find(blockedUser) == blockings_.end()) { - blockings_[blockedUser] = std::set(); - } - if (blockings_[blockedUser].find(blockingUserName) != blockings_[blockedUser].end()) { - blockings_[blockedUser].erase(blockingUserName); - sendUnblockDone(blockingUserSessionId, blockedUser); - } else { - blockings_[blockedUser].insert(blockingUserName); - sendBlockDone(blockingUserSessionId, blockedUser); - } -} - -void Broadcast::requestConversation(std::string sendToSessionId, std::string withUserName, std::string requestingUserName) { - auto userData = userForUserName(withUserName); - std::unique_lock lock(mutex_); - if (blockings_.find(withUserName) == blockings_.end()) { - blockings_[withUserName] = std::set(); - } - userData["blocked"] = !(blockings_.find(withUserName) == blockings_.end() || blockings_[withUserName].find(requestingUserName) == blockings_[withUserName].end()); - auto sessionId2 = sessionIdForUserName(withUserName); - auto id1 = sendToSessionId + "/" + sessionId2; - auto id2 = sessionId2 + "/" + sendToSessionId; - auto it = conversations_.find(id1); - if (it == conversations_.end()) { - it = conversations_.find(id2); - if (it == conversations_.end()) { - MessageQueue messageQueue; - auto [newIt, success] = conversations_.emplace(id1, MessageQueue{}); - it = newIt; - } - } - Wt::Json::Object broadcast{ - {"type", "conversation-start"}, - {"data", userData}, - }; - addMessageToSessionBroadcast(sendToSessionId, broadcast); - Wt::Json::Array messagesJson; - for (Broadcast::Message &message: it->second.messages) { - messagesJson.push_back(message.json()); - } - broadcast = { - {"type", "messagequeue"}, - {"user1", Wt::Json::Value(requestingUserName)}, - {"user2", Wt::Json::Value(withUserName)}, - {"sessionid1", Wt::Json::Value(sendToSessionId)}, - {"sessionid2", Wt::Json::Value(sessionId2)}, - {"data", messagesJson} - }; - addMessageToSessionBroadcast(sendToSessionId, broadcast); -} - -void Broadcast::userSearch(std::string toSession, std::string nameIncludes, int minAge, int maxAge, std::unordered_set countries, std::unordered_set genders, std::string excludeName) { - Wt::Json::Array searchResult; - for (const auto &user: connections_) { - if ( - (nameIncludes == "" || user->userName().find(nameIncludes) != std::string::npos) - && (minAge <= user->age()) - && (maxAge >= user->age()) - && (countries.contains("All") || countries.contains(user->country()) || countries.size() == 0) - && (genders.contains("All") || genders.contains(user->gender()) || genders.size() == 0) - && (user->userName() != excludeName) - ) { - searchResult.push_back(user->client()->json()); - } - } - Wt::Json::Object broadcast{ - {"type", "search-result"}, - {"data", searchResult} - }; - addMessageToSessionBroadcast(toSession, broadcast); -} - -void Broadcast::sendHistory(std::string toSession) { - std::unique_lock lock(mutex_); - Wt::Json::Array filteredConversationsPartners; - for (const auto& pair : conversations_) { - const std::string& key = pair.first; - if (key.rfind(toSession + "/", 0) == 0 || key.find("/" + toSession) == key.size() - toSession.length() - 1) { - std::string partnerId; - if (key.rfind(toSession + "/", 0) == 0) { - partnerId = key.substr(toSession.size() + 1); - } else { - partnerId = key.substr(0, key.size() - toSession.size() - 1); - } - auto partner = userForSessionId(partnerId); - filteredConversationsPartners.push_back(Wt::Json::Value(partner)); - } - } - Wt::Json::Object broadcast{ - {"type", "history"}, - {"data", filteredConversationsPartners} - }; - addMessageToSessionBroadcast(toSession, broadcast); -} - -bool Broadcast::parseCountriesData() { - std::istringstream iss(responseData_); - std::string line; - while (std::getline(iss, line)) { - line.erase(std::remove_if(line.begin(), line.end(), [](char c) { return !std::isprint(c) && c != ' '; }), line.end()); - std::vector tokens; - boost::split(tokens, line, boost::is_any_of(",")); - if (tokens.size() == 2) { - auto name = tokens[0]; - auto code = tokens[1]; - std::transform(code.begin(), code.end(), code.begin(), ::tolower); - countriesMap_[name] = code; - } - } - return true; -} - -void Broadcast::reSetSessionIdInMessages(std::string oldSessionId, std::string newSessionId) { - for (auto &conversation: conversations_) { - conversation.second.setNewSesionId(oldSessionId, newSessionId); - } -} - -Wt::Json::Object Broadcast::createUserList() { - Wt::Json::Array userList; - for (const auto &connection: connections_) { - Wt::Json::Object user; - user["name"] = Wt::asString(connection->userName()); - user["age"] = connection->age(); - user["gender"] = Wt::asString(connection->gender()); - user["country"] = Wt::asString(connection->country()); - user["isoCountryCode"] = getCountryIsoCodeByCountry(connection->country()); - userList.push_back(user); - } - Wt::Json::Object data; - data["data"] = userList; - data["type"] = "userlist"; - data["count"] = (int)userList.size(); - return data; -} - -void Broadcast::sendMessageQueueToSession(std::string receiverSessionId, std::string user1, std::string user2, std::vector messages) { - Wt::Json::Array messagesJson; - for (Broadcast::Message &message: messages) { - messagesJson.push_back(message.json()); - } - Wt::Json::Object broadcast{ - {"type", "messagequeue"}, - {"user1", Wt::Json::Value(user1)}, - {"user2", Wt::Json::Value(user2)}, - {"sessionid1", Wt::Json::Value(sessionIdForUserName(user1))}, - {"sessionid2", Wt::Json::Value(sessionIdForUserName(user2))}, - {"data", messagesJson} - }; - addMessageToSessionBroadcast(receiverSessionId, broadcast); -} - -void Broadcast::addMessageToSessionBroadcast(std::string sessionId, Wt::Json::Object message) { - for (auto &connection: connections_) { - if (connection->sessionId() == sessionId) { - connection->addBroadcast(message); - } - } -} - -void Broadcast::sendMessageCount(std::string sessionId) { - int count{0}; - for (const auto &conversation: conversations_) { - const auto key = conversation.first; - const auto data = conversation.second; - if ((key.compare(0, sessionId.length() + 1, sessionId + "/") == 0 - && !data.user1Read) - || (key.compare(key.length() - sessionId.length() - 1, sessionId.length() + 1, "/" + sessionId) == 0 - && !data.user2Read)) { - ++count; - } - } - Wt::Json::Object broadcast{ - {"type", "unread-chats"}, - {"data", count} - }; - addMessageToSessionBroadcast(sessionId, broadcast); -} - -void Broadcast::sendBlockedMessage(std::string sessionId, std::string toUserName) { - Wt::Json::Object broadcast{ - {"type", "system"}, - {"related-user", Wt::WString(toUserName)}, - {"data", "The user has blocked you."} - }; - addMessageToSessionBroadcast(sessionId, broadcast); -} - -void Broadcast::sendBlockDone(std::string sessionId, std::string toUserName) { - addMessage(sessionId, toUserName, Message("Conversation is blocked.")); - Wt::Json::Object broadcast{ - {"type", "system"}, - {"related-user", Wt::WString(toUserName)}, - {"data", "blocked"} - }; - addMessageToSessionBroadcast(sessionId, broadcast); - -} - -void Broadcast::sendUnblockDone(std::string sessionId, std::string toUserName) { - addMessage(sessionId, toUserName, Message("Conversation is unblocked.")); - Wt::Json::Object broadcast{ - {"type", "system"}, - {"related-user", Wt::WString(toUserName)}, - {"data", "unblocked"} - }; - addMessageToSessionBroadcast(sessionId, broadcast); -} - -size_t Broadcast::WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output) { - size_t total_size = size * nmemb; - output->append(reinterpret_cast(contents), total_size); - return total_size; -} - -Broadcast::Connection::Connection(const std::string &id, Client *client, const std::function &fct): - sessionId_(id), - client_(client), - fct_(fct) { -} - -Client *Broadcast::Connection::client() const { - return client_; -} - -std::string Broadcast::Connection::sessionId() const { - return sessionId_; -} - -std::string Broadcast::Connection::userName() const { - return client_->userName; -} - -std::string Broadcast::Connection::country() const { - return client_->country; -} - -int Broadcast::Connection::age() const { - return client_->age; -} - -std::string Broadcast::Connection::gender() const { - return client_->gender; -} - -void Broadcast::Connection::setClient(Client *client) { - client_ = client; -} - -void Broadcast::Connection::addBroadcast(Wt::Json::Object broadcast) { - broadcastFifo.push_back(broadcast); -} - -std::list Broadcast::Connection::getBroadcasts(bool clear) { - auto broadcastCopy = broadcastFifo; - if (clear) { - broadcastFifo.clear(); - } - return broadcastCopy; -} - -bool Broadcast::Connection::setSessionId(std::string searchedSessionId, std::string newSessionId) { - if (searchedSessionId == sessionId_) { - sessionId_ = newSessionId; - return true; - } - return false; -} - -std::function Broadcast::Connection::fct() { - return fct_; -} - - -void Client::setActivity() { - lastActivity_ = Wt::WDateTime::currentDateTime(); -} - -bool Client::activitiesTimedOut() { - auto timeDifference = lastActivity_.secsTo(Wt::WDateTime::currentDateTime()); - return timeDifference > timeoutSeconds; -} - -int Client::currentlyLoggedInSeconds() { - auto currentDateTime = Wt::WDateTime::currentDateTime(); - return loginTimeStamp_.secsTo(currentDateTime); -} - -Wt::Json::Object Client::json() { - return Wt::Json::Object{ - {"name", Wt::WString(userName)}, - {"gender", Wt::WString(gender)}, - {"isoCountryCode", Wt::WString(isoCountryCode)}, - {"country", Wt::WString(country)}, - {"age", age} - }; -} - -int Client::remainingSecondsToTimeout(){ - auto timeDifference = lastActivity_.secsTo(Wt::WDateTime::currentDateTime()); - return timeoutSeconds - timeDifference; -} - -void Client::setLoggedIn() { - loginTimeStamp_ = Wt::WDateTime::currentDateTime(); - lastActivity_ = loginTimeStamp_; -} - -Broadcast::Message::Message(std::string fromSessionId_, Wt::WString message_): - fromSessionId(fromSessionId_), - sendType("text"), - message(message_) { - auto timestamp = std::time(nullptr); - auto combinedString = std::to_string(timestamp) + fromSessionId_; - Wt::Auth::SHA1HashFunction hashObj; - auto hash = hashObj.compute(combinedString, "salt"); - messageId = Wt::WString(hash); - sendTime = Wt::WDateTime::currentDateTime(); -} - -Broadcast::Message::Message(std::string fromSessionId_, std::shared_ptr imageBlob): - fromSessionId(fromSessionId_), - sendType("image") { - auto timestamp = std::time(nullptr); - auto combinedString = std::to_string(timestamp) + fromSessionId_; - Wt::Auth::SHA1HashFunction hashObj; - auto hash = hashObj.compute(combinedString, "salt"); - messageId = Wt::WString(hash); - sendTime = Wt::WDateTime::currentDateTime(); - Magick::Image image_; - image_.read(*imageBlob); - std::string imageData(reinterpret_cast(imageBlob->data()), imageBlob->length()); - std::string imageFormat = image_.magick(); - image = { - {"width", Wt::Json::Value((int)image_.columns())}, - {"height", Wt::Json::Value((int)image_.rows())}, - {"imageblobbase64", Wt::Json::Value(Wt::Utils::base64Encode(imageData))}, - {"type", Wt::Json::Value(imageFormat)} - }; -} - -Broadcast::Message::Message(Wt::WString message_): - systemMessage(true), - sendType("text"), - message (message_) { - auto timestamp = std::time(nullptr); - auto combinedString = std::to_string(timestamp) + "system09715"; - Wt::Auth::SHA1HashFunction hashObj; - auto hash = hashObj.compute(combinedString, "salt"); - messageId = Wt::WString(hash); - sendTime = Wt::WDateTime::currentDateTime(); -} - -Wt::Json::Object Broadcast::Message::json() { - auto json = Wt::Json::Object(); - json["sender"] = Wt::Json::Value(fromSessionId); - json["type"] = Wt::Json::Value(sendType); - json["string"] = Wt::Json::Value(message); - json["image"] = Wt::Json::Value(image); - json["id"] = Wt::Json::Value(messageId); - json["timestamp"] = sendTime.toString(); - return json; -} - -void Broadcast::Message::setNewSesionId(std::string oldSessionId, std::string newSessionId) { - if (fromSessionId == oldSessionId) { - fromSessionId = newSessionId; - } -} - -void Broadcast::MessageQueue::setNewSesionId(std::string oldSessionId, std::string newSessionId) { - for (auto &message: messages) { - message.setNewSesionId(oldSessionId, newSessionId); - } -} diff --git a/src/broadcast.h b/src/broadcast.h deleted file mode 100644 index d791ac8..0000000 --- a/src/broadcast.h +++ /dev/null @@ -1,154 +0,0 @@ -#ifndef BROADCAST_H -#define BROADCAST_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace Magick { - class Image; - class Blob; -} - -struct ConversationItem { - Wt::WDateTime timestamp; - bool selfSend; - Wt::WString text; -}; - -class Client { -public: - std::string gender; - std::string country; - std::string isoCountryCode; - std::string userName; - int age; - std::unordered_map > conversations; - void setActivity(); - bool activitiesTimedOut(); - int currentlyLoggedInSeconds(); - virtual void incomingBroadcast() { std::cout << "incoming" << std::endl;}; - Wt::Json::Object json(); - int remainingSecondsToTimeout(); -protected: - void setLoggedIn(); - void setLoggedOut(); -private: - const int timeoutSeconds {1800}; - Wt::WDateTime lastActivity_; - Wt::WDateTime loginTimeStamp_; -}; - -class Broadcast { -public: - struct Message { - Message()= default; - Message(std::string fromSessionId_, Wt::WString message_); - Message(std::string fromSessionId_, std::shared_ptr image_); - Message(Wt::WString message_); - bool systemMessage{false}; - std::string fromSessionId; - std::string sendType; - Wt::WDateTime sendTime; - Wt::WString message; - Wt::WString messageId; - Wt::Json::Object image; - Wt::Json::Object json(); - void setNewSesionId(std::string oldSessionId, std::string newSessionId); - }; - struct MessageQueue { - bool user1Read{false}; - bool user2Read{false}; - std::vector messages; - void setNewSesionId(std::string oldSessionId, std::string newSessionId); - }; - Broadcast(Wt::WServer *server); - ~Broadcast(); - void connect(Client *client, const std::function &fct); - void disconnect(Client *client); - void addToDisconnectList(Client *client); - Wt::Json::Object reSetUser(std::string oldSessionId, std::string newSessionId); - int count() const; - std::string userNameForSessionId(std::string sessionId); - std::string sessionIdForUserName(std::string userName); - Wt::Json::Object userForSessionId(std::string sessionId); - Wt::Json::Object userForUserName(std::string userName); - void changeClientForSessionId(std::string sessionId, Client *client); - bool nameIsFree(std::string userNameToCheck); - void downloadCountries(); - std::map countries(); - Wt::WString getCountryIsoCodeByCountry(Wt::WString country); - std::list getBroadcastsForSession(std::string sessionId); - Wt::Json::Object addMessage(std::string fromSessionId, std::string toUserName, Message message); - Wt::Json::Object addImage(std::string fromSessionId, std::string toUserName, std::shared_ptr image); - void setConversationRead(std::string readingUserSessionId, std::string secondUserName); - void sendConversation(std::string sessionId1, std::string userName2); - void sendOpenConversations(std::string forSessionId); - void sendUserInformation(std::string sendToSessionId, std::string userName, std::string requestingUserName); - void toggleBlockUser(std::string blockingUserName, std::string blockedUser, std::string blockingUserSessionId); - void requestConversation(std::string sendToSessionId, std::string withUserName, std::string requestingUserName); - void userSearch(std::string toSession, std::string nameIncludes, int minAge, int maxAge, - std::unordered_set countries, std::unordered_set genders, std::string excludeName); - void sendHistory(std::string toSession); -protected: - struct Connection { - Connection(const std::string &id, Client *client, const std::function &fct); - public: - Client *client() const; - std::string sessionId() const; - std::function fct(); - std::string userName() const; - std::string country() const; - int age() const; - std::string gender() const; - void setClient(Client *client); - void addBroadcast(Wt::Json::Object broadcast); - std::list getBroadcasts(bool clear = false); - std::unordered_map > blockings_; - bool setSessionId(std::string searchedSessionId, std::string newSessionId); - private: - std::string sessionId_; - Client *client_; - std::function fct_; - int age_; - std::list broadcastFifo; - }; -private: - Wt::WServer *wtServer_; - bool stop_{false}; - mutable std::mutex mutex_; - std::thread thread_; - bool stop; - std::vector > connections_; - Wt::WDate lastCountriesDownload_; - Wt::WString countriesDownloadUrl_{"https://pkgstore.datahub.io/core/country-list/data_csv/data/d7c9d7cfb42cb69f4422dec222dbbaa8/data_csv.csv"}; - std::map countriesMap_; - std::string responseData_; - Wt::WDateTime lastTimeoutCheck_; - std::unordered_map conversations_; - std::unordered_map > blockings_; - std::vector toDisconnect_; - void run(); - Wt::Json::Object logoutBroadcast(); - static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output); - bool parseCountriesData(); - void reSetSessionIdInMessages(std::string oldSessionId, std::string newSessionId); - Wt::Json::Object createUserList(); - void sendMessageQueueToSession(std::string receiverSessionId, std::string user1, std::string user2, std::vector messages); - void addMessageToSessionBroadcast(std::string sessionId, Wt::Json::Object message); - void sendMessageCount(std::string toSessionId); - void sendBlockedMessage(std::string sessionId, std::string toUserName); - void sendBlockDone(std::string sessionId, std::string toUserName); - void sendUnblockDone(std::string sessionId, std::string toUserName); - void checkAndLogStart(); - void logClientLogin(const Wt::Json::Object &clientJson); -}; - -#endif // BROADCAST_H diff --git a/src/main.cpp b/src/main.cpp deleted file mode 100644 index 23cfed7..0000000 --- a/src/main.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "broadcast.h" -#include "app.h" -#include -#include - -std::unique_ptr createApplication(const Wt::WEnvironment& env, Broadcast& server) -{ - return std::make_unique(env, server); -} - -int main(int argc, char **argv) { - Wt::WServer server(argc, argv, WTHTTP_CONFIGURATION); - Broadcast chatServer(&server); - server.addEntryPoint(Wt::EntryPointType::Application, - std::bind(createApplication, std::placeholders::_1, - std::ref(chatServer))); - if (server.start()) { - int sig = Wt::WServer::waitForShutdown(); - std::cerr << "Shutting down: (signal = " << sig << ")" << std::endl; - server.stop(); - } - return 0; -}