This commit is contained in:
Torsten Schulz
2024-01-26 08:31:10 +01:00
parent 2a673b50ef
commit fc21a7e772
5 changed files with 1771 additions and 0 deletions

909
src/app.cpp Normal file
View File

@@ -0,0 +1,909 @@
#include "app.h"
#include <algorithm>
#include <string>
#include <regex>
#include <curl/curl.h>
#include <libxml2/libxml/parser.h>
#include <libxml2/libxml/tree.h>
#include <libxml2/libxml/xpath.h>
#include <Magick++.h>
#include <Wt/WMemoryResource.h>
#include <Wt/WContainerWidget.h>
#include <Wt/WTemplate.h>
#include <Wt/WVBoxLayout.h>
#include <Wt/WHBoxLayout.h>
#include <Wt/WGridLayout.h>
#include <Wt/WCssDecorationStyle.h>
#include <Wt/WText.h>
#include <Wt/WPushButton.h>
#include <Wt/WLineEdit.h>
#include <Wt/WMessageBox.h>
#include <Wt/WComboBox.h>
#include <Wt/WEvent.h>
#include <Wt/WAny.h>
#include <Wt/WSpinBox.h>
#include <Wt/Json/Array.h>
#include <Wt/Json/Object.h>
#include <Wt/Json/Serializer.h>
#include <Wt/WLink.h>
#include <Wt/WFileResource.h>
#include <Wt/WTable.h>
#include <Wt/WPopupMenu.h>
#include <Wt/WDialog.h>
#include <Wt/WFileUpload.h>
#include <Wt/Utils.h>
#include <Wt/WSound.h>
App::App(const Wt::WEnvironment &env, Broadcast &server):
Wt::WApplication(env),
env_(env),
server_(server),
updateLocationSignal_(this, "updateLocationSignal") {
initApp();
updateLocation();
enableUpdates(true);
userName = server.userNameForSessionId(sessionId());
auto verticalContainer = createVerticalLayout();
createHeadContainer(verticalContainer);
createMenuContainer(verticalContainer);
auto horizontalContainer = createActionLayout(verticalContainer);
createUserListContainer(horizontalContainer);
createContentContainer(horizontalContainer);
createImprintContainer(verticalContainer);
if (userName == "") {
showLogin();
} else {
startChat();
}
messageReceived_ = std::make_unique<Wt::WSound>("newmessage.mp3");
}
void App::initApp() {
setTitle("YP Direct Chat");
setCssTheme("");
useStyleSheet("style.css");
}
Wt::WVBoxLayout *App::createVerticalLayout() {
auto verticalBox = root()->addNew<Wt::WContainerWidget>();
verticalBox->setHeight(Wt::WLength(100, Wt::LengthUnit::Percentage));
verticalBox->setWidth(Wt::WLength(100, Wt::LengthUnit::Percentage));
auto verticalContainer = verticalBox->setLayout(std::make_unique<Wt::WVBoxLayout>());
verticalContainer->setContentsMargins(0, 0, 0, 0);
verticalContainer->setSpacing(0);
return verticalContainer;
}
Wt::WHBoxLayout *App::createActionLayout(Wt::WVBoxLayout *verticalContainer) {
auto horizontalBox = verticalContainer->addNew<Wt::WContainerWidget>();
horizontalBox->setStyleClass("horizontal-box");
auto horizontalContainer = horizontalBox->setLayout(std::make_unique<Wt::WHBoxLayout>());
horizontalContainer->setContentsMargins(0, 0, 0, 0);
horizontalContainer->setSpacing(0);
return horizontalContainer;
}
void App::createHeadContainer(Wt::WVBoxLayout *layout) {
auto header = layout->addNew<Wt::WContainerWidget>();
header->addNew<Wt::WText>("<h1>ypChat</h1>");
header->setStyleClass("header");
}
void App::createMenuContainer(Wt::WVBoxLayout *layout) {
menuContainer_ = layout->addNew<Wt::WContainerWidget>();
menuContainer_->setStyleClass("menu");
}
void App::showLogin() {
contentContainer_->clear();
createLoginContainer();
}
void App::createLoginContainer() {
auto loginContainer = contentContainer_->addNew<Wt::WContainerWidget>();
loginContainer->resize(Wt::WLength(40, Wt::LengthUnit::FontEm), Wt::WLength::Auto);
auto contentGrid = loginContainer->setLayout(std::make_unique<Wt::WGridLayout>());
auto userName = addUsernameInput(contentGrid);
auto gender = addGenderSelection(contentGrid);
auto age = addAgeInput(contentGrid);
auto countrySelection = addCountrySelection(contentGrid);
addStartChatButton(contentGrid, userName, countrySelection, age, gender);
}
Wt::WLineEdit *App::addUsernameInput(Wt::WGridLayout* contentGrid) {
contentGrid->addWidget(std::make_unique<Wt::WText>("Please type in your nick for the chat: "), 0, 0);
auto userName = contentGrid->addWidget(std::make_unique<Wt::WLineEdit>(), 0, 1);
userName->setTextSize(20);
userName->setMaxLength(30);
return userName;
}
Wt::WComboBox *App::addGenderSelection(Wt::WGridLayout* contentGrid) {
contentGrid->addWidget(std::make_unique<Wt::WText>("Gender: "), 1, 0);
auto genderWidget = contentGrid->addWidget(std::make_unique<Wt::WComboBox>(), 1, 1);
populateGenderComboBox(genderWidget);
return genderWidget;
}
Wt::WSpinBox *App::addAgeInput(Wt::WGridLayout* contentGrid) {
contentGrid->addWidget(std::make_unique<Wt::WText>("Age: "), 2, 0);
auto ageWidget = contentGrid->addWidget(std::make_unique<Wt::WSpinBox>(), 2, 1);
ageWidget->setRange(18, 150);
ageWidget->setValue(18);
return ageWidget;
}
Wt::WComboBox *App::addCountrySelection(Wt::WGridLayout* contentGrid) {
contentGrid->addWidget(std::make_unique<Wt::WText>("Country: "), 3, 0);
auto countryWidget = contentGrid->addWidget(std::make_unique<Wt::WComboBox>(), 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::WPushButton>("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);
validateName(nick);
validateGender(genderWidget);
validateAge(ageWidget);
setUserData(nick, countryWidget, ageWidget, genderWidget);
connectToServer();
startChat();
} catch (const std::exception& e) {
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_) {
genderWidget->addItem(genderItem.second);
if (genderItem.first == "F") {
markIndex = genderWidget->count();
}
}
genderWidget->setCurrentIndex(markIndex);
}
std::string App::getGenderShortByGender(std::string gender) {
for (const auto &genderItem: genders_) {
if (gender == genderItem.second) {
return genderItem.first;
}
}
return "";
}
void App::updateUserlist(Wt::Json::Array unsortedUserList) {
userListContainer_->clear();
userListContainer_->resize(Wt::WLength(15, Wt::LengthUnit::FontEm), Wt::WLength::Auto);
auto layout = userListContainer_->setLayout(std::make_unique<Wt::WVBoxLayout>());
layout->setSpacing(1);
layout->setContentsMargins(0, 0, 0, 0);
userListContainer_->setOverflow(Wt::Overflow::Auto);
auto sortedUserList = sortUserList(unsortedUserList);
for (Wt::Json::Object &user: sortedUserList) {
auto userName = (std::string)user["name"];
auto userItem = layout->addNew<Wt::WText>(Wt::WString("{1} ({2})").arg(userName).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(userName);
});
}
layout->addWidget(std::make_unique<Wt::WText>(), 1)->setStyleClass("height-spacer");
triggerUpdate();
}
std::vector<Wt::Json::Object> App::sortUserList(Wt::Json::Array unsortedUserList) {
std::vector<Wt::Json::Object> 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;
contentContainer_->clear();
auto layout = contentContainer_->setLayout(std::make_unique<Wt::WVBoxLayout>());
createInfoWidget(layout, userData);
layout->addWidget(std::make_unique<Wt::WContainerWidget>(), 1)->setOverflow(Wt::Overflow::Auto);
createInputContainer(layout);
}
Wt::WContainerWidget* App::createInfoWidget(Wt::WVBoxLayout *layout, Wt::Json::Object userData) {
auto infoWidget = layout->addNew<Wt::WContainerWidget>();
auto infoLayout = infoWidget->setLayout(std::make_unique<Wt::WHBoxLayout>());
infoWidget->setStyleClass("user-conversation-info");
infoWidget->setStyleClass(Wt::WString("user-conversation-info userlist-gender-{1}").arg((std::string)userData["gender"]));
infoLayout->addWidget(createInfoText(userData), 1);
auto blockButton = createBlockButton(userData);
infoLayout->addWidget(std::move(blockButton));
return infoWidget;
}
std::unique_ptr<Wt::WText> App::createInfoText(Wt::Json::Object userData) {
return std::make_unique<Wt::WText>(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<Wt::WPushButton> App::createBlockButton(Wt::Json::Object userData) {
auto blockButton = std::make_unique<Wt::WPushButton>((bool)userData["blocked"] ? "Unblock user" : "Block user");
blockButton->clicked().connect([=, this, &userData]() {
server_.toggleBlockUser(userName, (std::string)userData["name"], userName);
});
return blockButton;
}
Wt::WContainerWidget* App::createInputContainer(Wt::WVBoxLayout* layout) {
auto inputContainer = layout->addNew<Wt::WContainerWidget>();
auto inputLayout = inputContainer->setLayout(std::make_unique<Wt::WHBoxLayout>());
auto inputLine = createInputLine(inputLayout);
createSendImageButton(inputLayout);
auto cursorPosition = std::make_shared<int>(0);
createSmileyButton(inputLayout, inputLine, cursorPosition);
createSmileyBar(inputContainer, inputLine, cursorPosition);
createSendButton(inputLayout, inputLine);
return inputContainer;
}
Wt::WLineEdit* App::createInputLine(Wt::WHBoxLayout* inputLayout) {
auto inputLine = inputLayout->addWidget(std::make_unique<Wt::WLineEdit>(), 1);
inputLine->setMaxLength(250);
auto cursorPosition = std::make_shared<int>(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->addNew<Wt::WImage>(Wt::WLink(std::make_shared<Wt::WFileResource>("../docroot/image.png")));
sendImageButton->setToolTip("Send an image");
sendImageButton->clicked().connect([=, this]() {
auto fileDialog = root()->addNew<Wt::WDialog>("Send Image to User");
auto layout = fileDialog->contents()->setLayout(std::make_unique<Wt::WVBoxLayout>());
layout->addNew<Wt::WText>("Please select an immage");
auto fileWidget = layout->addNew<Wt::WFileUpload>();
fileWidget->setFilters("image/*");
auto image = layout->addNew<Wt::WImage>();
image->setMaximumSize(Wt::WLength(100, Wt::LengthUnit::Pixel), Wt::WLength(100, Wt::LengthUnit::Pixel));
image->setHeight(Wt::WLength::Auto);
auto localImage = std::make_shared<Magick::Image>();
auto buttonsContainer = layout->addNew<Wt::WContainerWidget>();
auto okButton = buttonsContainer->addNew<Wt::WPushButton>("Send image");
fileWidget->uploaded().connect([=, this]() mutable {
try {
Magick::Image originalImage;
originalImage.quiet(true);
originalImage.read(fileWidget->spoolFileName());
*localImage = scaleImage(originalImage, 500);
auto smallImage = scaleImage(originalImage, 100);
Magick::Blob previewBlob;
smallImage.write(&previewBlob);
auto previewBase64 = previewBlob.base64();
auto previewImage = Wt::Utils::base64Decode(previewBase64);
auto imageResource = std::make_shared<Wt::WMemoryResource>(
"image/png",
std::vector<unsigned char>(previewImage.begin(), previewImage.end()));
image->setImageLink(Wt::WLink(imageResource));
image->resize(Wt::WLength(smallImage.columns(), Wt::LengthUnit::Pixel), Wt::WLength(smallImage.rows(), Wt::LengthUnit::Pixel));
okButton->setEnabled(true);
triggerUpdate();
} catch (const std::exception& e) {
std::cerr << "Error processing uploaded image: " << e.what() << std::endl;
}
});
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::WPushButton>("Cancel");
cancelButton->clicked().connect([=](){ fileDialog->reject(); });
fileDialog->setClosable(true);
fileDialog->setModal(true);
fileDialog->show();
});
return sendImageButton;
}
Magick::Image App::scaleImage(const Magick::Image& originalImage, int maxSize) const {
int scale = 100;
int maxDimension = std::max(originalImage.size().width(), originalImage.size().height());
if (maxDimension > maxSize) {
scale = (maxSize * 100) / maxDimension;
}
scale = (scale < 100) ? scale : 100;
Magick::Image scaledImage(originalImage);
scaledImage.scale(Magick::Geometry(originalImage.size().width() * scale / 100, originalImage.size().height() * scale / 100));
return scaledImage;
}
Wt::WContainerWidget* App::createSmileyButton(Wt::WHBoxLayout* inputLayout, Wt::WLineEdit* inputLine, std::shared_ptr<int> cursorPosition) {
auto smileyButton = inputLayout->addNew<Wt::WContainerWidget>();
smileyButton->addNew<Wt::WImage>(Wt::WLink(std::make_shared<Wt::WFileResource>("../docroot/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<int> 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->addNew<Wt::WPushButton>("Send");
auto sendMessageFunction = [=, this]() {
sendMessage(inputLine);
};
inputLine->enterPressed().connect(sendMessageFunction);
sendButton->clicked().connect(sendMessageFunction);
return sendButton;
}
void App::sendMessage(Wt::WLineEdit *inputLine) {
auto utf8String = inputLine->valueText().toUTF8();
size_t pos = 0;
while ((pos = utf8String.find("<", pos)) != std::string::npos) {
utf8String.replace(pos, 1, "&lt;");
pos += 4;
}
inputLine->setValueText("");
auto sendString = Wt::WString(utf8String.substr(0, 250));
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->clear();
renderChatLines(conversation, outputContainer);
outputContainer->setOverflow(Wt::Overflow::Auto);
}
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;
}
}
}
void App::renderChatLine(Wt::Json::Object &line, Wt::Json::Object conversation, Wt::WContainerWidget* outputContainer) {
std::string writer = getChatLineWriter(line, conversation);
Wt::WWebWidget* item;
if ((std::string)line["type"] == "text") {
item = createTextElement(writer, (std::string)line["string"], outputContainer);
} else if ((std::string)line["type"] == "image") {
item = createImageElement(line, writer, outputContainer);
} else {
item = new Wt::WText("");
}
item->setInline(false);
}
Wt::WWebWidget* App::createTextElement(const std::string& writer, const std::string& text, Wt::WContainerWidget* outputContainer) {
std::string outputText = replaceSmileys(text);
Wt::WString output = Wt::WString("<b>{1}</b>: {2}").arg(writer).arg(outputText);
return outputContainer->addNew<Wt::WText>(output);
}
void App::createImprintContainer(Wt::WVBoxLayout *containerLayout) {
auto imprintContainer = containerLayout->addNew<Wt::WContainerWidget>();
imprintContainer->setContentAlignment(Wt::AlignmentFlag::Right);
imprintContainer->setPadding(Wt::WLength(0.5, Wt::LengthUnit::FontEm));
auto imprintButton = imprintContainer->addNew<Wt::WText>("Imprint");
Wt::WCssDecorationStyle imprintButtonDecorationStyle;
imprintButtonDecorationStyle.setForegroundColor(Wt::WColor(126, 71, 27));
imprintButtonDecorationStyle.setBackgroundColor(Wt::WColor(249, 162, 44));
imprintButtonDecorationStyle.setCursor(Wt::Cursor::PointingHand);
imprintContainer->setDecorationStyle(imprintButtonDecorationStyle);
imprintButton->clicked().connect([=, this]() {
auto imprintDialog = root()->addNew<Wt::WMessageBox>("Imprint", "", Wt::Icon::None, Wt::StandardButton::Ok);
imprintDialog->contents()->addNew<Wt::WText>("<h1>Imprint</h1>");
imprintDialog->contents()->addNew<Wt::WText>("<p>Information according to § 5 TMG</p>");
imprintDialog->contents()->addNew<Wt::WText>("<p>Torsten Schulz <br> "
"Friedrich-Stampfer-Str. 21<br> "
"60437 Frankfurt <br> "
"</p>", Wt::TextFormat::UnsafeXHTML);
imprintDialog->contents()->addNew<Wt::WText>("<p> <strong>Represented by: </strong><br> "
"Torsten Schulz<br> "
"</p>", Wt::TextFormat::UnsafeXHTML);
imprintDialog->contents()->addNew<Wt::WText>("<p><strong>Contact:</strong> <br> "
"Phone: 069-95 64 17 10<br> "
"Email: <a href='mailto:tsschulz@tsschulz.de'>tsschulz@tsschulz.de</a></br> "
"</p>", Wt::TextFormat::UnsafeXHTML);
imprintDialog->contents()->addNew<Wt::WText>("<p><strong>Disclaimer: </strong><br><br><strong>Liability for Contents</strong><br><br> "
"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.<br><br><strong>Liability for Links</strong><br><br> "
"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.<br><br><strong>Data Protection</strong><br><br> "
"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. <br> "
"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. <br> "
"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.<br> "
"</p>", Wt::TextFormat::UnsafeXHTML);
imprintDialog->contents()->addNew<Wt::WText>("<br> "
"Imprint from <a href=\"https://www.impressum-generator.de\">Imprint Generator</a> of <a href=\"https://www.kanzlei-hasselbach.de/\">Kanzlei Hasselbach, Lawyers for Labor Law and Family Law</a> ", 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) {
Wt::Json::Object imageDescription = line["image"];
auto imageLineItem = outputContainer->addNew<Wt::WContainerWidget>();
auto outputText = Wt::WString("<b>{1}:</b>&nbsp;").arg(writer);
imageLineItem->addNew<Wt::WText>(outputText)->setStyleClass("output-line");
auto image = imageLineItem->addNew<Wt::WImage>();
auto imageBlob = Wt::Utils::base64Decode((std::string)imageDescription["imageblobbase64"]);
auto imageResource = std::make_shared<Wt::WMemoryResource>(
"image/png",
std::vector<unsigned char>(imageBlob.begin(), imageBlob.end()));
image->setImageLink(Wt::WLink(imageResource));
image->resize(Wt::WLength((int)imageDescription["width"], Wt::LengthUnit::Pixel), Wt::WLength((int)imageDescription["height"], Wt::LengthUnit::Pixel));
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 buttonText = (int)data["data"] == 0 ? "Inbox" : Wt::WString("Inbox ({1})").arg((int)data["data"]);
auto currentText = inbox_->text();
inbox_->setText(buttonText);
std::string pattern{"Inbox\\(\\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 == "Inbox" && buttonText != "Inbox") {
doPlay = true;
}
std::cout << text << "/" << buttonText << ":" << doPlay << std::endl;
if (doPlay) {
messageReceived_->play();
}
triggerUpdate();
if (inboxOpen_) {
server_.sendOpenConversations(sessionId());
}
}
void App::showOpenInbox(Wt::Json::Object data) {
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<Wt::WText>("<h2>Inbox</h2>");
headline->setInline(false);
auto conversationsTable = contentContainer_->addNew<Wt::WTable>();
conversationsTable->resize(Wt::WLength(15, Wt::LengthUnit::FontEm), Wt::WLength::Auto);
for (Wt::Json::Object &user: (Wt::Json::Array)data["data"]) {
Wt::WString partnerName = (std::string)user["name"];
auto userItem = conversationsTable->elementAt(conversationsTable->rowCount(), 0)->addNew<Wt::WText>(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->setInline(false);
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<Wt::WText*>(infoWidgetLayout->itemAt(0)->widget());
auto blockButton = dynamic_cast<Wt::WPushButton*>(infoWidgetLayout->itemAt(1)->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<Wt::WContainerWidget> App::createSmileysBar(Wt::WLineEdit *inputLine, std::shared_ptr<int> cursorPosition) {
auto smileyBar = std::make_unique<Wt::WContainerWidget>();
smileyBar->setHidden(true);
for (auto &smiley: smileys_) {
auto code = smileyPlaceholder_;
code = code.arg(smiley.second.code);
auto item = smileyBar->addNew<Wt::WText>(code, Wt::TextFormat::UnsafeXHTML);
item->clicked().connect([=]() {
auto currentText = inputLine->text().toUTF8();
currentText.insert(*cursorPosition, smiley.first);
inputLine->setText(currentText);
inputLine->setFocus();
*cursorPosition += currentText.length() - 1;
});
}
smileyBar->setStyleClass("smiley-bar");
return smileyBar;
}
void App::toggleSmileysBar(Wt::WContainerWidget *smileyBar) {
smileyBar->setHidden(!smileyBar->isHidden());
}
void App::showSystemMessage(Wt::Json::Object broadcast) {
if ((std::string)broadcast["relatedUser"] != currentConversationWith_) {
return;
}
auto containerLayout = (Wt::WVBoxLayout*)contentContainer_->layout();
auto outputLayoutItem = containerLayout->itemAt(1);
auto outputContainer = (Wt::WContainerWidget*)outputLayoutItem->widget();
auto outputLine = outputContainer->addNew<Wt::WText>((std::string)broadcast["data"]);
outputLine->setStyleClass("system-message");
}
void App::createMenu() {
menuContainer_->clear();
menuContainer_->setStyleClass("menu");
addLeaveButton();
addIdentifier();
addSearchButton();
addInboxButton();
}
void App::addLeaveButton() {
auto leaveButton = menuContainer_->addNew<Wt::WPushButton>("Leave");
leaveButton->clicked().connect(this, &App::logout);
}
void App::logout() {
server_.disconnect(this);
userListContainer_->clear();
menuContainer_->clear();
showLogin();
inboxOpen_ = false;
}
void App::addIdentifier() {
auto identifier = menuContainer_->addNew<Wt::WText>(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::WPushButton>("Search");
searchButton->clicked().connect(this, &App::executeSearch);
}
void App::addInboxButton() {
inbox_ = menuContainer_->addNew<Wt::WPushButton>("Inbox");
inbox_->clicked().connect(this, &App::openInbox);
}
void App::executeSearch() {
currentConversationWith_ = "";
contentContainer_->clear();
contentContainer_->addNew<Wt::WText>("search");
inboxOpen_ = false;
}
void App::openInbox() {
currentConversationWith_ = "";
contentContainer_->clear();
contentContainer_->addNew<Wt::WText>("<h2>Inbox</h2>");
inboxOpen_ = true;
server_.sendOpenConversations(sessionId());
}
void App::incomingBroadcast() {
auto broadcasts = server_.getBroadcastsForSession(sessionId());
for (Wt::Json::Object &broadcast: broadcasts) {
if (broadcast["type"] == "userlist") {
updateUserlist(broadcast["data"]);
} 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") {
showSystemMessage(broadcast);
} else if (broadcast["type"] == "conversation-start") {
showConversation(broadcast);
}
}
}
void App::startChat() {
createMenu();
contentContainer_->clear();
contentContainer_->addNew<Wt::WText>("Have fun. But be patient");
}
void App::createUserListContainer(Wt::WHBoxLayout *layout) {
userListContainer_ = layout->addNew<Wt::WContainerWidget>();
userListContainer_->setStyleClass("userlist");
}
void App::createContentContainer(Wt::WHBoxLayout *layout) {
contentContainer_ = layout->addNew<Wt::WContainerWidget>();
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<const char *>(xmlNodeGetContent(node));
} else if (xmlStrEqual(node->name, BAD_CAST "country")) {
country = reinterpret_cast<const char *>(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;
});
}

150
src/app.h Normal file
View File

@@ -0,0 +1,150 @@
#ifndef APP_H
#define APP_H
#include <Wt/WApplication.h>
#include <Wt/WEnvironment.h>
#include "broadcast.h"
#include "curl/curl.h"
#include <libxml2/libxml/tree.h>
#include <libxml2/libxml/xpath.h>
namespace Magick {
class Image;
}
class App : public Wt::WApplication, public Client {
public:
App(const Wt::WEnvironment& env, Broadcast& server);
private:
std::unordered_map<std::string, std::string> genders_ {
{"F", "Female"},
{"M", "Male"},
{"P", "Pair"},
{"TF", "Transgender (M->F)"},
{"TM", "Transgender (F->M)"}
};
std::vector<std::string> 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<std::string, Smiley> smileys_ {
{":)", Smiley("1F642", "Smile")},
{":D", Smiley("1F600", "Laugh")},
{":(", Smiley("1F641", "Sad")},
{";)", Smiley("1F609", "Twinkle")},
{":p", Smiley("1F60B", "Tongue")},
{";p", Smiley("1F61C", "Twinkle tongue")},
{":'(", Smiley("1F622", "Cry")}
};
Wt::WString smileyPlaceholder_ = "&#x{1};";
const Wt::WEnvironment &env_;
Broadcast &server_;
Wt::WContainerWidget *menuContainer_;
Wt::WContainerWidget *userListContainer_;
Wt::WContainerWidget *contentContainer_;
Wt::JSignal<std::string, std::string> updateLocationSignal_;
std::string responseData_{""};
Wt::WPushButton *inbox_;
std::vector<std::string> searchResults_;
std::string currentConversationWith_{""};
bool inboxOpen_{false};
int messageCursorPosition_{-1};
std::unique_ptr<Wt::WSound> messageReceived_;
void initApp();
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<const char *>(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);
std::vector<Wt::Json::Object> sortUserList(Wt::Json::Array unsortedUserList);
void createMenu();
void addLeaveButton();
void logout();
void addIdentifier();
void addSearchButton();
void addInboxButton();
void executeSearch();
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<Wt::WContainerWidget> createSmileysBar(Wt::WLineEdit *inputLine, std::shared_ptr<int> cursorPosition);
void toggleSmileysBar(Wt::WContainerWidget *smileyBar);
void showSystemMessage(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<int> cursorPosition);
Wt::WContainerWidget *createSmileyButton(Wt::WHBoxLayout *inputLayout, Wt::WLineEdit *inputLine, std::shared_ptr<int> cursorPosition);
Wt::WImage *createSendImageButton(Wt::WHBoxLayout *inputLayout);
Wt::WLineEdit *createInputLine(Wt::WHBoxLayout *inputLayout);
Wt::WContainerWidget *createInputContainer(Wt::WVBoxLayout *layout);
std::unique_ptr<Wt::WText> 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<Wt::WPushButton> 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();
Magick::Image scaleImage(const Magick::Image &originalImage, int maxSize) const;
Wt::WWebWidget *createImageElement(Wt::Json::Object &line, const std::string &writer, Wt::WContainerWidget *outputContainer);
Wt::WWebWidget *createTextElement(const std::string &writer, const std::string &text, Wt::WContainerWidget *outputContainer);
void createImprintContainer(Wt::WVBoxLayout *containerLayout);
};
#endif // APP_H

559
src/broadcast.cpp Normal file
View File

@@ -0,0 +1,559 @@
#include "broadcast.h"
#include <Wt/WApplication.h>
#include <Wt/WAny.h>
#include <Wt/Json/Array.h>
#include <Wt/Json/Serializer.h>
#include <iostream>
#include <sstream>
#include <vector>
#include <curl/curl.h>
#include <boost/algorithm/string.hpp>
#include <Magick++.h>
Broadcast::Broadcast(Wt::WServer *server):
wtServer_(server) {
downloadCountries();
thread_ = std::thread(std::bind(&Broadcast::run, this));
lastTimeoutCheck_ = Wt::WDateTime::currentDateTime();
}
Broadcast::~Broadcast() {
stop_ = true;
thread_.join();
}
void Broadcast::connect(Client *client, const std::function<void ()> &fct) {
std::unique_lock<std::mutex> lock(mutex_);
connections_.push_back(std::make_unique<Connection>(Wt::WApplication::instance()->sessionId(), client, fct));
auto broadcast = createUserList();
for (auto &connection: connections_) {
connection->addBroadcast(broadcast);
}
}
void Broadcast::disconnect(Client *client) {
std::unique_lock<std::mutex> lock(mutex_);
for (unsigned int i = 0; i < connections_.size(); ++ i) {
if (connections_[i]->client() == client) {
connections_.erase(connections_.begin() + i);
}
}
auto broadcast = createUserList();
for (auto &connection: connections_) {
connection->addBroadcast(broadcast);
}
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;
}
}
}
int Broadcast::count() const {
std::unique_lock<std::mutex> 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<std::mutex> lock(mutex_);
for (auto &connection: connections_) {
if (sessionId == connection->sessionId()) {
connection->setClient(client);
}
}
}
bool Broadcast::nameIsFree(std::string userNameToCheck) {
std::unique_lock<std::mutex> lock(mutex_);
for (auto &connection: connections_) {
if (connection->userName() == userNameToCheck) {
return false;
}
}
return true;
}
void Broadcast::run() {
for (;;) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (stop_) {
return;
}
std::unique_lock<std::mutex> lock(mutex_);
for (auto &connection: connections_) {
if (connection->client()->activitiesTimedOut()) {
disconnect(connection->client());
continue;
}
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<Wt::WString, Wt::WString> 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<Wt::Json::Object> Broadcast::getBroadcastsForSession(std::string sessionId) {
std::unique_lock<std::mutex> lock(mutex_);
for (auto &connection: connections_) {
if (connection->sessionId() == sessionId) {
return connection->getBroadcasts(true);
}
}
return std::list<Wt::Json::Object>();
}
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<Magick::Image> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> lock(mutex_);
auto userData = userForUserName(userName);
if (blockings_.find(userName) == blockings_.end()) {
blockings_[userName] = std::set<std::string>();
}
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<std::mutex> lock(mutex_);
if (blockings_.find(blockedUser) == blockings_.end()) {
blockings_[blockedUser] = std::set<std::string>();
}
if (blockings_[blockedUser].find(blockingUserName) != blockings_[blockedUser].end()) {
blockings_[blockedUser].erase(blockingUserName);
sendUnblockDone(blockingUserSessionId, blockedUser);
} else {
blockings_[blockedUser].insert(blockingUserSessionId);
sendBlockDone(blockingUserSessionId, blockedUser);
}
sendUserInformation(blockingUserSessionId, blockedUser, blockingUserName);
}
void Broadcast::requestConversation(std::string sendToSessionId, std::string withUserName, std::string requestingUserName) {
auto userData = userForUserName(withUserName);
std::unique_lock<std::mutex> lock(mutex_);
if (blockings_.find(withUserName) == blockings_.end()) {
blockings_[withUserName] = std::set<std::string>();
}
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);
}
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<std::string> 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;
}
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());
userList.push_back(user);
}
Wt::Json::Object data;
data["data"] = userList;
data["type"] = "userlist";
return data;
}
void Broadcast::sendMessageQueueToSession(std::string receiverSessionId, std::string user1, std::string user2, std::vector<Message> 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) {
Wt::Json::Object broadcast{
{"type", "system"},
{"related-user", Wt::WString(toUserName)},
{"data", "User is blocked."}
};
addMessageToSessionBroadcast(sessionId, broadcast);
}
void Broadcast::sendUnblockDone(std::string sessionId, std::string toUserName) {
Wt::Json::Object broadcast{
{"type", "system"},
{"related-user", Wt::WString(toUserName)},
{"data", "User is 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<char*>(contents), total_size);
return total_size;
}
Broadcast::Connection::Connection(const std::string &id, Client *client, const std::function<void ()> &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<Wt::Json::Object> Broadcast::Connection::getBroadcasts(bool clear) {
auto broadcastCopy = broadcastFifo;
if (clear) {
broadcastFifo.clear();
}
return broadcastCopy;
}
std::function<void ()> Broadcast::Connection::fct() {
return fct_;
}
void Client::setActivity() {
lastActivity_ = Wt::WDateTime::currentDateTime();
}
bool Client::activitiesTimedOut() {
auto timeDifference = Wt::WDateTime::currentDateTime().secsTo(lastActivity_);
return timeDifference > 1800;
}
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}
};
}
Broadcast::Message::Message(std::string fromSessionId_, Wt::WString message_):
fromSessionId(fromSessionId_),
sendType("text"),
message(message_) {
sendTime = Wt::WDateTime::currentDateTime();
}
Broadcast::Message::Message(std::string fromSessionId_, std::shared_ptr<Magick::Image> image_):
fromSessionId(fromSessionId_),
sendType("image") {
sendTime = Wt::WDateTime::currentDateTime();
Magick::Blob imageBlob;
image_->write(&imageBlob);
image = {
{"width", Wt::Json::Value((int)image_->columns())},
{"height", Wt::Json::Value((int)image_->rows())},
{"imageblobbase64", Wt::Json::Value(imageBlob.base64())}
};
}
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);
return json;
}

130
src/broadcast.h Normal file
View File

@@ -0,0 +1,130 @@
#ifndef BROADCAST_H
#define BROADCAST_H
#include <thread>
#include <mutex>
#include <functional>
#include <map>
#include <list>
#include <unordered_map>
#include <Wt/WServer.h>
#include <Wt/WDate.h>
#include <Wt/Json/Object.h>
namespace Magick {
class Image;
}
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<std::string, std::vector<ConversationItem> > conversations;
void setActivity();
bool activitiesTimedOut();
virtual void incomingBroadcast() { std::cout << "incoming" << std::endl;};
Wt::Json::Object json();
private:
Wt::WDateTime lastActivity_;
};
class Broadcast {
public:
struct Message {
Message()= default;
Message(std::string fromSessionId_, Wt::WString message_);
Message(std::string fromSessionId_, std::shared_ptr<Magick::Image> image_);
std::string fromSessionId;
std::string sendType;
Wt::WDateTime sendTime;
Wt::WString message;
Wt::Json::Object image;
Wt::Json::Object json();
};
struct MessageQueue {
bool user1Read{false};
bool user2Read{false};
std::vector<Message> messages;
};
Broadcast(Wt::WServer *server);
~Broadcast();
void connect(Client *client, const std::function<void ()> &fct);
void disconnect(Client *client);
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<Wt::WString, Wt::WString> countries();
Wt::WString getCountryIsoCodeByCountry(Wt::WString country);
std::list<Wt::Json::Object> 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<Magick::Image> 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);
protected:
struct Connection {
Connection(const std::string &id, Client *client, const std::function<void ()> &fct);
public:
Client *client() const;
std::string sessionId() const;
std::function<void ()> 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<Wt::Json::Object> getBroadcasts(bool clear = false);
std::unordered_map<std::string, std::vector<std::string> > blockings_;
private:
std::string sessionId_;
Client *client_;
std::function<void()> fct_;
int age_;
std::list<Wt::Json::Object> broadcastFifo;
};
private:
Wt::WServer *wtServer_;
bool stop_{false};
mutable std::mutex mutex_;
std::thread thread_;
bool stop;
std::vector<std::unique_ptr<Connection> > connections_;
Wt::WDate lastCountriesDownload_;
Wt::WString countriesDownloadUrl_{"https://pkgstore.datahub.io/core/country-list/data_csv/data/d7c9d7cfb42cb69f4422dec222dbbaa8/data_csv.csv"};
std::map<Wt::WString, Wt::WString> countriesMap_;
std::string responseData_;
Wt::WDateTime lastTimeoutCheck_;
std::unordered_map<std::string, MessageQueue> conversations_;
std::unordered_map<std::string, std::set<std::string> > blockings_;
void run();
Wt::Json::Object logoutBroadcast();
static size_t WriteCallback(void* contents, size_t size, size_t nmemb, std::string* output);
bool parseCountriesData();
Wt::Json::Object createUserList();
void sendMessageQueueToSession(std::string receiverSessionId, std::string user1, std::string user2, std::vector<Message> 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);
};
#endif // BROADCAST_H

23
src/main.cpp Normal file
View File

@@ -0,0 +1,23 @@
#include "broadcast.h"
#include "app.h"
#include <Wt/WServer.h>
#include <Wt/WSound.h>
std::unique_ptr<Wt::WApplication> createApplication(const Wt::WEnvironment& env, Broadcast& server)
{
return std::make_unique<App>(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;
}