mirror of https://github.com/qt/qtgrpc.git
Backport the simple chat example
Backport the simple chat example. The application implements a simple messenger using Qt GRPC. It shows how to utilize the server-side streaming and use the call-based user/password credentials. Task-number: QTBUG-109598 Pick-to: 6.5 Change-Id: I4ca3695780a9cc9991c92c4423e3af9d8e0eaf35 Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io> Reviewed-by: Mårten Nordheim <marten.nordheim@qt.io>
This commit is contained in:
parent
2e3fdd5c05
commit
fdd1d0339f
|
|
@ -6,4 +6,5 @@ if(TARGET Qt6::Quick
|
||||||
AND TARGET Qt6::qtprotobufgen
|
AND TARGET Qt6::qtprotobufgen
|
||||||
AND TARGET Qt6::qtgrpcgen)
|
AND TARGET Qt6::qtgrpcgen)
|
||||||
qt_internal_add_example(magic8ball)
|
qt_internal_add_example(magic8ball)
|
||||||
|
qt_internal_add_example(chat)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
add_subdirectory(client)
|
||||||
|
add_subdirectory(server)
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
# Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(GrpcChatClient LANGUAGES CXX)
|
||||||
|
|
||||||
|
if(NOT DEFINED INSTALL_EXAMPLESDIR)
|
||||||
|
set(INSTALL_EXAMPLESDIR "examples")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/grpc/chat")
|
||||||
|
|
||||||
|
find_package(Qt6 REQUIRED COMPONENTS Core Protobuf Grpc Quick)
|
||||||
|
|
||||||
|
qt_standard_project_setup()
|
||||||
|
|
||||||
|
qt_add_executable(grpcchatclient
|
||||||
|
main.cpp
|
||||||
|
simplechatengine.cpp simplechatengine.h
|
||||||
|
chatmessagemodel.cpp chatmessagemodel.h
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_qml_module(grpcchatclient
|
||||||
|
URI qtgrpc.examples.chat
|
||||||
|
VERSION 1.0
|
||||||
|
AUTO_RESOURCE_PREFIX
|
||||||
|
QML_FILES
|
||||||
|
Main.qml
|
||||||
|
ChatInputField.qml
|
||||||
|
ChatView.qml
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_resources(grpcchatclient
|
||||||
|
"assets"
|
||||||
|
PREFIX "/"
|
||||||
|
FILES
|
||||||
|
"qt_logo_green_64x64px.png"
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_protobuf(grpcchatclient
|
||||||
|
PROTO_FILES
|
||||||
|
../proto/simplechat.proto
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_grpc(grpcchatclient CLIENT
|
||||||
|
PROTO_FILES
|
||||||
|
../proto/simplechat.proto
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(grpcchatclient
|
||||||
|
PRIVATE
|
||||||
|
Qt::Gui
|
||||||
|
Qt::Quick
|
||||||
|
Qt::Grpc
|
||||||
|
)
|
||||||
|
|
||||||
|
install(TARGETS grpcchatclient
|
||||||
|
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||||
|
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||||
|
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
TextField {
|
||||||
|
id: _inputField
|
||||||
|
width: 200
|
||||||
|
color: "#cecfd5"
|
||||||
|
placeholderTextColor: "#9d9faa"
|
||||||
|
font.pointSize: 14
|
||||||
|
padding: 10
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 5
|
||||||
|
border {
|
||||||
|
width: 1
|
||||||
|
color: _inputField.focus ? "#41cd52" : "#f3f3f4"
|
||||||
|
}
|
||||||
|
color: "#222840"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,153 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
import qtgrpc.examples.chat
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#09102b"
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
_inputField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
id: messageListView
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: _inputField.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
model: SimpleChatEngine.messages
|
||||||
|
clip: true
|
||||||
|
delegate: Item {
|
||||||
|
height: _imageWrapper.height + 10
|
||||||
|
width: root.width
|
||||||
|
Item {
|
||||||
|
id: _imageWrapper
|
||||||
|
height: _messageColumn.height + 20
|
||||||
|
width: parent.width/2 - 20
|
||||||
|
property bool ownMessage: SimpleChatEngine.userName === model.from
|
||||||
|
anchors{
|
||||||
|
right: _imageWrapper.ownMessage ? parent.right : undefined
|
||||||
|
left: _imageWrapper.ownMessage ? undefined : parent.left
|
||||||
|
rightMargin: _imageWrapper.ownMessage ? 10 : 0
|
||||||
|
leftMargin: _imageWrapper.ownMessage ? 0 : 10
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: 5
|
||||||
|
color: _imageWrapper.ownMessage ? "#9d9faa" : "#53586b"
|
||||||
|
border.color: "#41cd52"
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: _messageColumn
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
leftMargin: 10
|
||||||
|
rightMargin: 10
|
||||||
|
verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
id: _userName
|
||||||
|
property string from: _imageWrapper.ownMessage ? qsTr("You") : model.from
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
font.pointSize: 12
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: "#f3f3f4"
|
||||||
|
text: _userName.from + ": "
|
||||||
|
}
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: delegateLoader
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: item ? item.height : 0
|
||||||
|
sourceComponent: model.type === SimpleChatEngine.Image ? imageDelegate : textDelegate
|
||||||
|
onItemChanged: {
|
||||||
|
if (item) {
|
||||||
|
item.content = model.content
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onCountChanged: {
|
||||||
|
Qt.callLater( messageListView.positionViewAtEnd )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component {
|
||||||
|
id: textDelegate
|
||||||
|
Item {
|
||||||
|
property alias content: innerText.text
|
||||||
|
height: childrenRect.height
|
||||||
|
Text {
|
||||||
|
id: innerText
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
height: implicitHeight
|
||||||
|
font.pointSize: 12
|
||||||
|
color: "#f3f3f4"
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component {
|
||||||
|
id: imageDelegate
|
||||||
|
Item {
|
||||||
|
clip: true
|
||||||
|
property alias content: innerImage.source
|
||||||
|
height: childrenRect.height
|
||||||
|
Image {
|
||||||
|
id: innerImage
|
||||||
|
width: implicitWidth
|
||||||
|
height: implicitHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatInputField {
|
||||||
|
id: _inputField
|
||||||
|
focus: true
|
||||||
|
anchors {
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
bottom: parent.bottom
|
||||||
|
margins: 20
|
||||||
|
}
|
||||||
|
|
||||||
|
placeholderText: qsTr("Start typing here or paste an image")
|
||||||
|
onAccepted: {
|
||||||
|
SimpleChatEngine.sendMessage(_inputField.text)
|
||||||
|
_inputField.text = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onPressed: (event)=> {
|
||||||
|
if (event.key === Qt.Key_V && event.modifiers & Qt.ControlModifier) {
|
||||||
|
console.log("Ctrl + V")
|
||||||
|
switch (SimpleChatEngine.clipBoardContentType) {
|
||||||
|
case SimpleChatEngine.Text:
|
||||||
|
paste()
|
||||||
|
break
|
||||||
|
case SimpleChatEngine.Image:
|
||||||
|
SimpleChatEngine.sendImageFromClipboard()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,151 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
|
||||||
|
import qtgrpc.examples.chat
|
||||||
|
|
||||||
|
ApplicationWindow {
|
||||||
|
id: mainWindow
|
||||||
|
visible: true
|
||||||
|
width: 640
|
||||||
|
height: 480
|
||||||
|
minimumWidth: 640
|
||||||
|
minimumHeight: 480
|
||||||
|
title: qsTr("Simple Chat")
|
||||||
|
Rectangle {
|
||||||
|
id: background
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "#09102b"
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: 10
|
||||||
|
anchors {
|
||||||
|
top: parent.top
|
||||||
|
topMargin: 10
|
||||||
|
left: parent.left
|
||||||
|
leftMargin: 20
|
||||||
|
}
|
||||||
|
Image {
|
||||||
|
source: "qrc:/qt_logo_green_64x64px.png"
|
||||||
|
width: implicitWidth
|
||||||
|
height: implicitHeight
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: "#f3f3f4"
|
||||||
|
font.pointSize: 20
|
||||||
|
text: qsTr("Chat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: loginControl
|
||||||
|
spacing: 5
|
||||||
|
visible: SimpleChatEngine.state !== SimpleChatEngine.Connected
|
||||||
|
anchors.centerIn: parent
|
||||||
|
enabled: SimpleChatEngine.state === SimpleChatEngine.Disconnected
|
||||||
|
ChatInputField {
|
||||||
|
id: loginField
|
||||||
|
width: 200
|
||||||
|
placeholderText: qsTr("Login")
|
||||||
|
onAccepted: {
|
||||||
|
SimpleChatEngine.login(loginField.text, passwordField.text)
|
||||||
|
}
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (visible) {
|
||||||
|
loginField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (visible) {
|
||||||
|
loginField.forceActiveFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ChatInputField {
|
||||||
|
id: passwordField
|
||||||
|
echoMode: TextInput.Password
|
||||||
|
placeholderText: qsTr("Password")
|
||||||
|
onAccepted: {
|
||||||
|
SimpleChatEngine.login(loginField.text, passwordField.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
id: loginButton
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: enterText.implicitWidth + 20
|
||||||
|
height: 40
|
||||||
|
background: Rectangle {
|
||||||
|
radius: 5
|
||||||
|
border {
|
||||||
|
width: 1
|
||||||
|
color: loginButton.pressed ? "#41cd52" : "#f3f3f4"
|
||||||
|
}
|
||||||
|
color:"#53586b"
|
||||||
|
Text {
|
||||||
|
id: enterText
|
||||||
|
text : qsTr("Enter")
|
||||||
|
color: "#f3f3f4"
|
||||||
|
anchors.centerIn: parent
|
||||||
|
font.pointSize: 14
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
SimpleChatEngine.login(loginField.text, passwordField.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatView {
|
||||||
|
id: chatView
|
||||||
|
visible: SimpleChatEngine.state === SimpleChatEngine.Connected
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: connectingText
|
||||||
|
visible: SimpleChatEngine.state === SimpleChatEngine.Connecting
|
||||||
|
anchors.top: loginControl.bottom
|
||||||
|
anchors.topMargin: 10
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("Connecting...")
|
||||||
|
font.pointSize: 14
|
||||||
|
color: "#f3f3f4"
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: authFailedText
|
||||||
|
visible: false
|
||||||
|
anchors.top: loginControl.bottom
|
||||||
|
anchors.topMargin: 10
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: qsTr("This username with this password doesn't exists.")
|
||||||
|
font.pointSize: 14
|
||||||
|
color: "#f3f3f4"
|
||||||
|
onVisibleChanged: {
|
||||||
|
if (authFailedText.visible) {
|
||||||
|
fadeOutTimer.restart()
|
||||||
|
} else {
|
||||||
|
fadeOutTimer.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: fadeOutTimer
|
||||||
|
onTriggered: {
|
||||||
|
authFailedText.visible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: SimpleChatEngine
|
||||||
|
function onAuthFailed() {
|
||||||
|
authFailedText.visible = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
|
#include <QImage>
|
||||||
|
#include <QBuffer>
|
||||||
|
|
||||||
|
#include "chatmessagemodel.h"
|
||||||
|
#include "simplechat.qpb.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
enum Role {
|
||||||
|
Content = Qt::UserRole + 1,
|
||||||
|
Type,
|
||||||
|
From,
|
||||||
|
};
|
||||||
|
|
||||||
|
QString getImage(const QByteArray &data)
|
||||||
|
{
|
||||||
|
return QString("data:image/png;base64,") + data.toBase64();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getImageScaled(const QByteArray &data)
|
||||||
|
{
|
||||||
|
QImage img = QImage::fromData(data, "PNG");
|
||||||
|
img = img.scaled(300, 300, Qt::KeepAspectRatio);
|
||||||
|
QByteArray scaledData;
|
||||||
|
QBuffer buffer(&scaledData);
|
||||||
|
img.save(&buffer, "PNG");
|
||||||
|
return getImage(scaledData);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString getText(const QByteArray &data)
|
||||||
|
{
|
||||||
|
return QString::fromUtf8(data);
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ChatMessageModel::ChatMessageModel(QObject *parent) : QAbstractListModel(parent) { }
|
||||||
|
|
||||||
|
QHash<int, QByteArray> ChatMessageModel::roleNames() const
|
||||||
|
{
|
||||||
|
static QHash<int, QByteArray> s_roleNames;
|
||||||
|
if (s_roleNames.isEmpty()) {
|
||||||
|
s_roleNames.insert(Content, "content");
|
||||||
|
s_roleNames.insert(Type, "type");
|
||||||
|
s_roleNames.insert(From, "from");
|
||||||
|
}
|
||||||
|
return s_roleNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ChatMessageModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
int row = index.row();
|
||||||
|
|
||||||
|
if (row < 0 || row >= m_container.count() || !m_container.at(row))
|
||||||
|
return QVariant();
|
||||||
|
|
||||||
|
auto dataPtr = m_container.at(row).get();
|
||||||
|
|
||||||
|
switch (role) {
|
||||||
|
case Content:
|
||||||
|
if (dataPtr->type() == qtgrpc::examples::chat::ChatMessage::Image)
|
||||||
|
return QVariant::fromValue(getImageScaled(dataPtr->content()));
|
||||||
|
else
|
||||||
|
return QVariant::fromValue(getText(dataPtr->content()));
|
||||||
|
case Type:
|
||||||
|
return QVariant::fromValue(dataPtr->type());
|
||||||
|
case From:
|
||||||
|
return QVariant::fromValue(dataPtr->from());
|
||||||
|
}
|
||||||
|
|
||||||
|
return QVariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
int ChatMessageModel::rowCount(const QModelIndex &) const
|
||||||
|
{
|
||||||
|
return m_container.count();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ChatMessageModel::append(
|
||||||
|
const QList<std::shared_ptr<qtgrpc::examples::chat::ChatMessage>> &messages)
|
||||||
|
{
|
||||||
|
if (messages.size() > 0) {
|
||||||
|
beginInsertRows(QModelIndex(), m_container.size(),
|
||||||
|
m_container.size() + messages.size() - 1);
|
||||||
|
m_container.append(messages);
|
||||||
|
endInsertRows();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
|
#ifndef CHATMESSAGEMODEL_H
|
||||||
|
#define CHATMESSAGEMODEL_H
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
namespace qtgrpc::examples::chat {
|
||||||
|
class ChatMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChatMessageModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
explicit ChatMessageModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent) const override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
QVariant data(const QModelIndex &index, int role) const override;
|
||||||
|
|
||||||
|
void append(const QList<std::shared_ptr<qtgrpc::examples::chat::ChatMessage>> &messages);
|
||||||
|
|
||||||
|
private:
|
||||||
|
QList<std::shared_ptr<qtgrpc::examples::chat::ChatMessage>> m_container;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // CHATMESSAGEMODEL_H
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QFontDatabase>
|
||||||
|
#include <QIcon>
|
||||||
|
#include <QFont>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
QGuiApplication app(argc, argv);
|
||||||
|
|
||||||
|
QGuiApplication::setWindowIcon(QIcon(":/qt_logo_green_64x64px.png"));
|
||||||
|
|
||||||
|
QQmlApplicationEngine engine;
|
||||||
|
|
||||||
|
QObject::connect(&engine, &QQmlApplicationEngine::objectCreationFailed, &app, [](){
|
||||||
|
QCoreApplication::exit(-1);
|
||||||
|
}, Qt::QueuedConnection);
|
||||||
|
|
||||||
|
engine.loadFromModule("qtgrpc.examples.chat", "Main");
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,172 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
|
#include "simplechatengine.h"
|
||||||
|
|
||||||
|
#include <QGrpcHttp2Channel>
|
||||||
|
#include <QGrpcUserPasswordCredentials>
|
||||||
|
#include <QGrpcInsecureChannelCredentials>
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QCryptographicHash>
|
||||||
|
#include <QDateTime>
|
||||||
|
#include <QClipboard>
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QMimeData>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QByteArray>
|
||||||
|
#include <QBuffer>
|
||||||
|
|
||||||
|
SimpleChatEngine::SimpleChatEngine(QObject *parent)
|
||||||
|
: QObject(parent),
|
||||||
|
m_state(Disconnected),
|
||||||
|
m_client(new qtgrpc::examples::chat::SimpleChat::Client),
|
||||||
|
m_clipBoard(QGuiApplication::clipboard())
|
||||||
|
{
|
||||||
|
qRegisterProtobufTypes();
|
||||||
|
if (m_clipBoard)
|
||||||
|
QObject::connect(m_clipBoard, &QClipboard::dataChanged, this,
|
||||||
|
&SimpleChatEngine::clipBoardContentTypeChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleChatEngine::~SimpleChatEngine()
|
||||||
|
{
|
||||||
|
delete m_client;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleChatEngine::login(const QString &name, const QString &password)
|
||||||
|
{
|
||||||
|
if (m_state != Disconnected)
|
||||||
|
return;
|
||||||
|
|
||||||
|
setState(Connecting);
|
||||||
|
QUrl url("http://localhost:65002");
|
||||||
|
|
||||||
|
// ![0]
|
||||||
|
std::shared_ptr<QAbstractGrpcChannel> channel(
|
||||||
|
new QGrpcHttp2Channel(url,
|
||||||
|
QGrpcUserPasswordCredentials(name, password.toUtf8())
|
||||||
|
| QGrpcInsecureChannelCredentials()));
|
||||||
|
// ![0]
|
||||||
|
|
||||||
|
m_client->attachChannel(channel);
|
||||||
|
|
||||||
|
// ![1]
|
||||||
|
auto stream = m_client->streamMessageList(qtgrpc::examples::chat::None());
|
||||||
|
QObject::connect(stream.get(), &QGrpcStream::errorOccurred, this,
|
||||||
|
[this, stream](const QGrpcStatus &status) {
|
||||||
|
qCritical()
|
||||||
|
<< "Stream error(" << status.code() << "):" << status.message();
|
||||||
|
if (status.code() == QGrpcStatus::Unauthenticated)
|
||||||
|
emit authFailed();
|
||||||
|
});
|
||||||
|
|
||||||
|
QObject::connect(stream.get(), &QGrpcStream::finished, this,
|
||||||
|
[this, stream]() { setState(Disconnected); });
|
||||||
|
|
||||||
|
QObject::connect(stream.get(), &QGrpcStream::messageReceived, this, [this, name, stream]() {
|
||||||
|
if (m_userName != name) {
|
||||||
|
m_userName = name;
|
||||||
|
emit userNameChanged();
|
||||||
|
}
|
||||||
|
setState(Connected);
|
||||||
|
m_messages.append(stream->read<qtgrpc::examples::chat::ChatMessages>().messages());
|
||||||
|
});
|
||||||
|
// ![1]
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleChatEngine::sendMessage(const QString &content)
|
||||||
|
{
|
||||||
|
// ![2]
|
||||||
|
qtgrpc::examples::chat::ChatMessage msg;
|
||||||
|
msg.setContent(content.toUtf8());
|
||||||
|
msg.setType(qtgrpc::examples::chat::ChatMessage::Text);
|
||||||
|
msg.setTimestamp(QDateTime::currentMSecsSinceEpoch());
|
||||||
|
msg.setFrom(m_userName);
|
||||||
|
m_client->sendMessage(msg);
|
||||||
|
// ![2]
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleChatEngine::ContentType SimpleChatEngine::clipBoardContentType() const
|
||||||
|
{
|
||||||
|
if (m_clipBoard != nullptr) {
|
||||||
|
const QMimeData *mime = m_clipBoard->mimeData();
|
||||||
|
if (mime != nullptr) {
|
||||||
|
if (mime->hasImage() || mime->hasUrls())
|
||||||
|
return SimpleChatEngine::ContentType::Image;
|
||||||
|
else if (mime->hasText())
|
||||||
|
return SimpleChatEngine::ContentType::Text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return SimpleChatEngine::ContentType::Unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleChatEngine::sendImageFromClipboard()
|
||||||
|
{
|
||||||
|
if (m_clipBoard == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QByteArray imgData;
|
||||||
|
const QMimeData *mime = m_clipBoard->mimeData();
|
||||||
|
if (mime != nullptr) {
|
||||||
|
if (mime->hasImage()) {
|
||||||
|
QImage img = mime->imageData().value<QImage>();
|
||||||
|
img = img.scaled(300, 300, Qt::KeepAspectRatio);
|
||||||
|
QBuffer buffer(&imgData);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
img.save(&buffer, "PNG");
|
||||||
|
buffer.close();
|
||||||
|
} else if (mime->hasUrls()) {
|
||||||
|
QUrl imgUrl = mime->urls().at(0);
|
||||||
|
if (!imgUrl.isLocalFile()) {
|
||||||
|
qWarning() << "Only supports transfer of local images";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QImage img(imgUrl.toLocalFile());
|
||||||
|
if (img.isNull()) {
|
||||||
|
qWarning() << "Invalid image format";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QBuffer buffer(&imgData);
|
||||||
|
buffer.open(QIODevice::WriteOnly);
|
||||||
|
img.save(&buffer, "PNG");
|
||||||
|
buffer.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imgData.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
qtgrpc::examples::chat::ChatMessage msg;
|
||||||
|
msg.setContent(imgData);
|
||||||
|
msg.setType(qtgrpc::examples::chat::ChatMessage::Image);
|
||||||
|
msg.setTimestamp(QDateTime::currentMSecsSinceEpoch());
|
||||||
|
msg.setFrom(m_userName);
|
||||||
|
m_client->sendMessage(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
ChatMessageModel *SimpleChatEngine::messages()
|
||||||
|
{
|
||||||
|
return &m_messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
QString SimpleChatEngine::userName() const
|
||||||
|
{
|
||||||
|
return m_userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleChatEngine::State SimpleChatEngine::state() const
|
||||||
|
{
|
||||||
|
return m_state;
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleChatEngine::setState(SimpleChatEngine::State state)
|
||||||
|
{
|
||||||
|
if (m_state != state) {
|
||||||
|
m_state = state;
|
||||||
|
emit stateChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
|
#ifndef SIMPLECHATENGINE_H
|
||||||
|
#define SIMPLECHATENGINE_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include "simplechat.qpb.h"
|
||||||
|
#include "simplechat_client.grpc.qpb.h"
|
||||||
|
#include "chatmessagemodel.h"
|
||||||
|
|
||||||
|
QT_BEGIN_NAMESPACE
|
||||||
|
class QClipboard;
|
||||||
|
QT_END_NAMESPACE
|
||||||
|
|
||||||
|
class SimpleChatEngine : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
Q_PROPERTY(QString userName READ userName NOTIFY userNameChanged)
|
||||||
|
Q_PROPERTY(ChatMessageModel *messages READ messages CONSTANT)
|
||||||
|
Q_PROPERTY(SimpleChatEngine::ContentType clipBoardContentType READ clipBoardContentType NOTIFY
|
||||||
|
clipBoardContentTypeChanged)
|
||||||
|
Q_PROPERTY(SimpleChatEngine::State state READ state NOTIFY stateChanged)
|
||||||
|
public:
|
||||||
|
enum ContentType {
|
||||||
|
Unknown = qtgrpc::examples::chat::ChatMessage::ContentType::Unknown,
|
||||||
|
Text = qtgrpc::examples::chat::ChatMessage::ContentType::Text,
|
||||||
|
Image = qtgrpc::examples::chat::ChatMessage::ContentType::Image,
|
||||||
|
};
|
||||||
|
Q_ENUM(ContentType)
|
||||||
|
|
||||||
|
enum State {
|
||||||
|
Disconnected = 0,
|
||||||
|
Connecting = 1,
|
||||||
|
Connected = 2,
|
||||||
|
};
|
||||||
|
Q_ENUM(State)
|
||||||
|
|
||||||
|
explicit SimpleChatEngine(QObject *parent = nullptr);
|
||||||
|
~SimpleChatEngine() override;
|
||||||
|
Q_INVOKABLE void login(const QString &name, const QString &password);
|
||||||
|
Q_INVOKABLE void sendMessage(const QString &message);
|
||||||
|
|
||||||
|
Q_INVOKABLE void sendImageFromClipboard();
|
||||||
|
|
||||||
|
QString userName() const;
|
||||||
|
ChatMessageModel *messages();
|
||||||
|
SimpleChatEngine::ContentType clipBoardContentType() const;
|
||||||
|
|
||||||
|
SimpleChatEngine::State state() const;
|
||||||
|
|
||||||
|
Q_SIGNALS:
|
||||||
|
void authFailed();
|
||||||
|
void clipBoardContentTypeChanged();
|
||||||
|
void userNameChanged();
|
||||||
|
void stateChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void setState(SimpleChatEngine::State state);
|
||||||
|
|
||||||
|
SimpleChatEngine::State m_state;
|
||||||
|
ChatMessageModel m_messages;
|
||||||
|
qtgrpc::examples::chat::SimpleChat::Client *m_client;
|
||||||
|
QClipboard *m_clipBoard;
|
||||||
|
QString m_userName;
|
||||||
|
};
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(ChatMessageModel *)
|
||||||
|
|
||||||
|
#endif // SIMPLECHATENGINE_H
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
//! [0]
|
||||||
|
package qtgrpc.examples.chat;
|
||||||
|
|
||||||
|
message ChatMessage
|
||||||
|
{
|
||||||
|
enum ContentType {
|
||||||
|
Unknown = 0;
|
||||||
|
Text = 1;
|
||||||
|
Image = 2;
|
||||||
|
};
|
||||||
|
uint64 timestamp = 1;
|
||||||
|
bytes content = 2;
|
||||||
|
ContentType type = 3;
|
||||||
|
string from = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message ChatMessages
|
||||||
|
{
|
||||||
|
repeated ChatMessage messages = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message User
|
||||||
|
{
|
||||||
|
string name = 1;
|
||||||
|
string password = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Users {
|
||||||
|
repeated User users = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message None { }
|
||||||
|
|
||||||
|
service SimpleChat
|
||||||
|
{
|
||||||
|
rpc messageList(None) returns (stream ChatMessages) { }
|
||||||
|
rpc sendMessage(ChatMessage) returns (None) { }
|
||||||
|
}
|
||||||
|
//! [0]
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
# Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
# SPDX-License-Identifier: BSD-3-Clause
|
||||||
|
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
|
||||||
|
project(GrpcChatServer LANGUAGES CXX)
|
||||||
|
|
||||||
|
# Qt6::Grpc module is not used directly in this project. But this allows to find Qt6::Grpc's
|
||||||
|
# dependencies without setting extra cmake module paths.
|
||||||
|
find_package(Qt6 COMPONENTS Grpc)
|
||||||
|
find_package(WrapgRPCPlugin)
|
||||||
|
find_package(WrapgRPC)
|
||||||
|
|
||||||
|
if(NOT TARGET WrapgRPC::WrapgRPCPlugin OR NOT TARGET WrapProtoc::WrapProtoc
|
||||||
|
OR NOT TARGET WrapgRPC::WrapLibgRPC)
|
||||||
|
message(WARNING "Dependencies of QtGrpc test server not found. Skipping.")
|
||||||
|
return()
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set(proto_files "${CMAKE_CURRENT_LIST_DIR}/../proto/simplechat.proto")
|
||||||
|
set(out_dir "${CMAKE_CURRENT_BINARY_DIR}")
|
||||||
|
|
||||||
|
set(generated_files
|
||||||
|
"${out_dir}/simplechat.pb.h" "${out_dir}/simplechat.pb.cc"
|
||||||
|
"${out_dir}/simplechat.grpc.pb.h" "${out_dir}/simplechat.grpc.pb.cc")
|
||||||
|
|
||||||
|
add_custom_command(
|
||||||
|
OUTPUT ${generated_files}
|
||||||
|
COMMAND
|
||||||
|
$<TARGET_FILE:WrapProtoc::WrapProtoc>
|
||||||
|
ARGS
|
||||||
|
--grpc_out "${out_dir}"
|
||||||
|
--cpp_out "${out_dir}"
|
||||||
|
"-I${CMAKE_CURRENT_LIST_DIR}/../proto/"
|
||||||
|
--plugin=protoc-gen-grpc=$<TARGET_FILE:WrapgRPC::WrapgRPCPlugin>
|
||||||
|
"${proto_files}"
|
||||||
|
WORKING_DIRECTORY ${out_dir}
|
||||||
|
DEPENDS "${proto_files}"
|
||||||
|
COMMENT "Generating gRPC ${target} sources..."
|
||||||
|
COMMAND_EXPAND_LISTS
|
||||||
|
VERBATIM
|
||||||
|
)
|
||||||
|
|
||||||
|
set_source_files_properties(${generated_files} PROPERTIES GENERATED TRUE)
|
||||||
|
|
||||||
|
add_executable(grpcchatserver
|
||||||
|
${generated_files}
|
||||||
|
main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(grpcchatserver PRIVATE ${out_dir})
|
||||||
|
target_link_libraries(grpcchatserver PRIVATE WrapgRPC::WrapLibgRPC)
|
||||||
|
|
||||||
|
install(TARGETS grpcchatserver
|
||||||
|
RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||||
|
BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||||
|
LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}"
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,213 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// Copyright (C) 2019 Alexey Edelev <semlanik@gmail.com>
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <string_view>
|
||||||
|
#include <list>
|
||||||
|
#include <cassert>
|
||||||
|
#include <optional>
|
||||||
|
|
||||||
|
#include <grpc++/grpc++.h>
|
||||||
|
#include <simplechat.pb.h>
|
||||||
|
#include <simplechat.grpc.pb.h>
|
||||||
|
using namespace qtgrpc::examples::chat;
|
||||||
|
|
||||||
|
class MessageListHandler;
|
||||||
|
class UserListHandler;
|
||||||
|
|
||||||
|
constexpr std::string_view nameHeader("user-name");
|
||||||
|
constexpr std::string_view passwordHeader("user-password");
|
||||||
|
class SimpleChatService final : public SimpleChat::WithAsyncMethod_messageList<SimpleChat::Service>
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
SimpleChatService();
|
||||||
|
void run(std::string_view address);
|
||||||
|
|
||||||
|
grpc::Status sendMessage(grpc::ServerContext *context, const ChatMessage *request,
|
||||||
|
None *) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::optional<std::string> checkUserCredentials(grpc::ServerContext *context);
|
||||||
|
void addMessageHandler(MessageListHandler *userHandler);
|
||||||
|
void sendMessageToClients(const ChatMessage *message);
|
||||||
|
|
||||||
|
Users m_usersDatabase;
|
||||||
|
ChatMessages m_messages;
|
||||||
|
std::list<MessageListHandler *> m_activeClients;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct HandlerTag
|
||||||
|
{
|
||||||
|
enum Type { Request = 1, Reply, Disconnect, Reject };
|
||||||
|
|
||||||
|
HandlerTag(HandlerTag::Type t, MessageListHandler *h) : tag(t), handler(h) { }
|
||||||
|
|
||||||
|
HandlerTag::Type tag;
|
||||||
|
MessageListHandler *handler;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MessageListHandler
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
MessageListHandler(SimpleChatService *service, grpc::ServerCompletionQueue *queue)
|
||||||
|
: writer(&ctx), cq(queue)
|
||||||
|
{
|
||||||
|
ctx.AsyncNotifyWhenDone(new HandlerTag(HandlerTag::Disconnect, this));
|
||||||
|
service->RequestmessageList(&ctx, &request, &writer, cq, cq,
|
||||||
|
new HandlerTag(HandlerTag::Request, this));
|
||||||
|
}
|
||||||
|
|
||||||
|
None request;
|
||||||
|
grpc::ServerAsyncWriter<qtgrpc::examples::chat::ChatMessages> writer;
|
||||||
|
grpc::ServerContext ctx;
|
||||||
|
grpc::ServerCompletionQueue *cq;
|
||||||
|
};
|
||||||
|
|
||||||
|
void SimpleChatService::run(std::string_view address)
|
||||||
|
{
|
||||||
|
grpc::ServerBuilder builder;
|
||||||
|
builder.AddListeningPort(std::string(address), grpc::InsecureServerCredentials());
|
||||||
|
builder.RegisterService(this);
|
||||||
|
std::unique_ptr<grpc::ServerCompletionQueue> cq = builder.AddCompletionQueue();
|
||||||
|
std::unique_ptr<grpc::Server> server(builder.BuildAndStart());
|
||||||
|
std::cout << "Server listening on " << address << '\n';
|
||||||
|
|
||||||
|
MessageListHandler *pending = new MessageListHandler(this, cq.get());
|
||||||
|
while (true) {
|
||||||
|
HandlerTag *tag = nullptr;
|
||||||
|
bool ok = false;
|
||||||
|
cq->Next(reinterpret_cast<void **>(&tag), &ok);
|
||||||
|
|
||||||
|
if (tag == nullptr)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
std::cout << "Unable to proccess tag from the completion queue\n";
|
||||||
|
delete tag;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (tag->tag) {
|
||||||
|
case HandlerTag::Request: {
|
||||||
|
std::cout << "New connection request received\n";
|
||||||
|
std::string name;
|
||||||
|
if (!checkUserCredentials(&(pending->ctx))) {
|
||||||
|
std::cout << "Authentication failed\n";
|
||||||
|
pending->writer.Finish(grpc::Status(grpc::StatusCode::UNAUTHENTICATED,
|
||||||
|
"User or login are invalid"),
|
||||||
|
new HandlerTag(HandlerTag::Reject, pending));
|
||||||
|
} else {
|
||||||
|
std::cout << "User " << name << " connected to chat\n";
|
||||||
|
addMessageHandler(pending);
|
||||||
|
pending->writer.Write(m_messages, nullptr);
|
||||||
|
}
|
||||||
|
pending = new MessageListHandler(this, cq.get());
|
||||||
|
} break;
|
||||||
|
case HandlerTag::Disconnect: {
|
||||||
|
auto it = std::find(m_activeClients.begin(), m_activeClients.end(), tag->handler);
|
||||||
|
if (it != m_activeClients.end()) {
|
||||||
|
std::cout << "Client disconnected\n";
|
||||||
|
m_activeClients.erase(it);
|
||||||
|
delete tag->handler;
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case HandlerTag::Reject:
|
||||||
|
std::cout << "Connection rejected\n";
|
||||||
|
m_activeClients.remove(tag->handler);
|
||||||
|
delete tag->handler;
|
||||||
|
break;
|
||||||
|
case HandlerTag::Reply:
|
||||||
|
std::cout << "Sending data to users\n";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
delete tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::string> SimpleChatService::checkUserCredentials(grpc::ServerContext *context)
|
||||||
|
{
|
||||||
|
assert(context != nullptr);
|
||||||
|
|
||||||
|
std::string name;
|
||||||
|
std::string password;
|
||||||
|
for (const auto &[key, value] : std::as_const(context->client_metadata())) {
|
||||||
|
if (std::string(key.data(), key.size()) == nameHeader) {
|
||||||
|
name = std::string(value.data(), value.size());
|
||||||
|
}
|
||||||
|
if (std::string(key.data(), key.size()) == passwordHeader) {
|
||||||
|
password = std::string(value.data(), value.size());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return std::find_if(m_usersDatabase.users().begin(), m_usersDatabase.users().end(),
|
||||||
|
[&name, &password](const auto &it) {
|
||||||
|
return it.name() == name && it.password() == password;
|
||||||
|
})
|
||||||
|
!= m_usersDatabase.users().end()
|
||||||
|
? std::optional{ name }
|
||||||
|
: std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
SimpleChatService::SimpleChatService()
|
||||||
|
{
|
||||||
|
// All passwords are 'qwerty' by default
|
||||||
|
User *newUser = m_usersDatabase.add_users();
|
||||||
|
newUser->set_name("user1");
|
||||||
|
newUser->set_password("qwerty");
|
||||||
|
newUser = m_usersDatabase.add_users();
|
||||||
|
newUser->set_name("user2");
|
||||||
|
newUser->set_password("qwerty");
|
||||||
|
newUser = m_usersDatabase.add_users();
|
||||||
|
newUser->set_name("user3");
|
||||||
|
newUser->set_password("qwerty");
|
||||||
|
newUser = m_usersDatabase.add_users();
|
||||||
|
newUser->set_name("user4");
|
||||||
|
newUser->set_password("qwerty");
|
||||||
|
newUser = m_usersDatabase.add_users();
|
||||||
|
newUser->set_name("user5");
|
||||||
|
newUser->set_password("qwerty");
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleChatService::sendMessageToClients(const ChatMessage *message)
|
||||||
|
{
|
||||||
|
assert(message != nullptr);
|
||||||
|
|
||||||
|
// Send only new messages after users received all the messages for this session after stream
|
||||||
|
// call.
|
||||||
|
ChatMessages messages;
|
||||||
|
ChatMessage *msg = messages.add_messages();
|
||||||
|
*msg = *message;
|
||||||
|
|
||||||
|
for (auto client : m_activeClients)
|
||||||
|
client->writer.Write(messages, new HandlerTag(HandlerTag::Reply, client));
|
||||||
|
}
|
||||||
|
|
||||||
|
void SimpleChatService::addMessageHandler(MessageListHandler *handler)
|
||||||
|
{
|
||||||
|
assert(handler != nullptr);
|
||||||
|
m_activeClients.push_back(handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
grpc::Status SimpleChatService::sendMessage(grpc::ServerContext *context,
|
||||||
|
const ChatMessage *request, None *)
|
||||||
|
{
|
||||||
|
assert(context != nullptr);
|
||||||
|
assert(request != nullptr);
|
||||||
|
|
||||||
|
auto name = checkUserCredentials(context);
|
||||||
|
if (!name)
|
||||||
|
return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Login or password are invalid");
|
||||||
|
|
||||||
|
auto msg = m_messages.add_messages();
|
||||||
|
*msg = *request;
|
||||||
|
msg->set_from(*name);
|
||||||
|
sendMessageToClients(msg);
|
||||||
|
return grpc::Status();
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int, char *[])
|
||||||
|
{
|
||||||
|
SimpleChatService srv;
|
||||||
|
srv.run("localhost:65002");
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 8.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.7 KiB |
|
|
@ -0,0 +1,78 @@
|
||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
|
||||||
|
|
||||||
|
/*!
|
||||||
|
\example chat
|
||||||
|
\ingroup qtgrpc-examples
|
||||||
|
\meta tag {network,protobuf,grpc,serialization,tcp}
|
||||||
|
\meta category {Networking}
|
||||||
|
\title Chat
|
||||||
|
|
||||||
|
\brief Using the Qt GRPC client API in the user applications.
|
||||||
|
|
||||||
|
\e Chat explains how to authenticate chat users and send and receive short
|
||||||
|
messages between chat clients. The application supports the following
|
||||||
|
message formats:
|
||||||
|
\list
|
||||||
|
\li Text - use message input field to send the message.
|
||||||
|
\li Image - copy the image buffer to the clipboard to send the message
|
||||||
|
using the \c{'Ctrl + V'} shortcut.
|
||||||
|
\endlist
|
||||||
|
|
||||||
|
The chat client uses a simple RPC protocol described in the protobuf scheme:
|
||||||
|
\snippet chat/proto/simplechat.proto 0
|
||||||
|
|
||||||
|
On the login screen, enter user credentials:
|
||||||
|
\image chatlogin.webp
|
||||||
|
|
||||||
|
\note The list of users is predefined on the server side and is constant.
|
||||||
|
The password for all users is \c qwerty.
|
||||||
|
|
||||||
|
The chat client uses an authentication type called \c {call credentials}.
|
||||||
|
Each gRPC message includes the user credentials in the message header.
|
||||||
|
Credentials are passed to the \l QGrpcHttp2Channel once and reused
|
||||||
|
implicitily:
|
||||||
|
|
||||||
|
\snippet chat/client/simplechatengine.cpp 0
|
||||||
|
|
||||||
|
The chat client starts the communication with the server using a
|
||||||
|
subscription to gRPC server streaming:
|
||||||
|
|
||||||
|
\snippet chat/client/simplechatengine.cpp 1
|
||||||
|
|
||||||
|
The \l QGrpcStream handler provides the signals that the client application
|
||||||
|
should connect to.
|
||||||
|
|
||||||
|
The \l QGrpcStream::errorOccurred signal indicates the error that occurred
|
||||||
|
either on the server side or in the communication channel. Typically, an
|
||||||
|
error results in the connection to the server being closed and
|
||||||
|
the \l QGrpcStream::finished signal being emitted.
|
||||||
|
|
||||||
|
When the server sends new messages to the stream, \l QGrpcStream emits the
|
||||||
|
\l QGrpcStream::messageReceived signal. The slot connected to this signal
|
||||||
|
processes the chat message. Messages that are received from the
|
||||||
|
\c {SimpleChat/messageList} server stream are collected in the custom
|
||||||
|
\l QAbstractListModel model and displayed to the user.
|
||||||
|
|
||||||
|
When the \l QGrpcStream::finished signal is emitted, there is nothing more
|
||||||
|
you can do with this stream instance, so you need to initiate a new
|
||||||
|
subscription.
|
||||||
|
|
||||||
|
After a successful subscription, the chat client switches to the
|
||||||
|
conversation screen and allows you to see and send short messages:
|
||||||
|
|
||||||
|
\image chatconversation.webp
|
||||||
|
|
||||||
|
To send the message, use a unary RPC call \c {SimpleChat/sendMessage}.
|
||||||
|
The client application first sets fields of the \c ChatMessage protobuf
|
||||||
|
message and then calls the client method:
|
||||||
|
|
||||||
|
\snippet chat/client/simplechatengine.cpp 2
|
||||||
|
|
||||||
|
Then, the gRPC server processes the client messages and broadcasts them to
|
||||||
|
all the connected clients through the \c {SimpleChat/messageList} stream.
|
||||||
|
|
||||||
|
\note This example uses the reference gRPC C++ API in the server
|
||||||
|
implementation.
|
||||||
|
*/
|
||||||
|
|
||||||
Loading…
Reference in New Issue