add data expiration date checks

pull/166/head
larteyoh 11 months ago
parent 45a3ade1cb
commit 4caf307b66

@ -1,4 +1,4 @@
# neroshop - WORK IN PROGRESS
# neroshop - WORK IN PROGRESS / ON HOLD
[![banner](assets/images/appicons/LogoLight250x250.png)](https://github.com/larteyoh/testshop "neroshop logo")
@ -55,7 +55,7 @@ The name _neroshop_ is a combination of the words _nero_, which is Italian for _
-->
## Feature Status
- [x] Distributed P2P network (bootstrap nodes needed!!!)
- [x] Distributed P2P network (need help with I2P integration!!!)
- [ ] Buy and sell products and services with Monero
- [x] No KYC
- [ ] No censorship (censorship-resistant)
@ -70,11 +70,11 @@ The name _neroshop_ is a combination of the words _nero_, which is Italian for _
- [x] Option to run a local Monero node or connect to remote Monero nodes
- [ ] Payment address QR codes containing Monero URIs
- [ ] Option to choose between sending funds directly to a seller or by using a multisignature escrow.
- [ ] Native Tor and I2P support
- tor daemon can be installed manually and i2pd will be built-in
- [ ] Native I2P support
- i2pd will be built-in (statically linked)
- [x] Seller reputation system
- [x] Product rating system
- [ ] Wishlists
- [x] Wishlists
- [x] Built-in SQLite-powered search engine that can find any products or sellers
- [ ] Full-featured and user-friendly GUI application (WIP)
@ -230,7 +230,7 @@ This project is licensed under the [GNU General Public License v3.0 (GPLv3)](LIC
## Donations
You may support the neroshop project directly by donating to any of the addresses below. Received payments will be used to reward developers for their contributions to the project (mostly by completing bounties) and will also be used to fund our official website domain name.
Donate if you like, to any of the addresses below. Received payments will be used to reward developers for their contributions to the project (mostly by completing bounties) and will also be used to keep the `neroshop.org` domain running.
**Monero (XMR):**
```

@ -4,7 +4,7 @@ import QtQuick.Layouts 1.12
import FontAwesome 1.0
//import neroshop.namespace 1.0
import neroshop.InventorySorting 1.0
import "." as NeroshopComponents
@ -142,6 +142,7 @@ Item {
property real cellHeight: 100
property real cellRadius: 0////table.titleBoxRadius
property string cellColor: (NeroshopComponents.Style.darkTheme) ? (NeroshopComponents.Style.themeName == "PurpleDust" ? "#17171c" : "#181a1b") : "#c9c9cd"//"transparent"//table.columnColor
//property int currentSorting: Inventory.SortNone
////contentHeight: childrenRect.height
////Component.onCompleted: console.log(Neroshop.InventorySorting) // C++ enum
ScrollBar.vertical: ScrollBar { } // ?
@ -157,33 +158,33 @@ Item {
}
return selectedItems
}
function getSelectedItemsProductIds() {
let selectedItemsProductIds = []
function getSelectedItemsListingKeys() {
let selectedItemsListingKeys = []
for(let index = 0; index < listView.count; ++index) {
let listItem = listView.itemAtIndex(index)
if(listItem == null || listItem == undefined) continue; // skip invalid values
if(listItem.checked) {
selectedItemsProductIds[index] = listItem.productId
console.log((listItem.children[1].children[2].text) + (" (" + listItem.productId + ")"), " selected")
selectedItemsListingKeys[index] = listItem.listingKey
console.log((listItem.children[1].children[2].text) + (" (" + listItem.listingKey + ")"), " selected")
}
}
return selectedItemsProductIds
return selectedItemsListingKeys
}
function removeSelectedItems() {
const selectedItemsProductIds = getSelectedItemsProductIds()
User.delistProducts(selectedItemsProductIds)
const selectedItemsListingKeys = getSelectedItemsListingKeys()
User.delistProducts(selectedItemsListingKeys)
// User.delistProduct fails to delete all selected items and only deletes one of the selected item (at the top of list)
// This is due to the model changing every time we emit the signal
// Hence the creation of User.delistProducts (notice the s) which solves the issue
}
model: showOutOfStockProductsBox.checked ? User.inventory : User.inventoryInStock
model: showOutOfStockProductsBox.checked ? User.inventory : User.getInventory(Inventory.SortByAvailability)
delegate: Rectangle {
width: listView.width
height: listView.cellHeight
color: listView.cellColor
border.color: "transparent"//table.columnBorderColor
radius: listView.cellRadius
property string productId: modelData.product_id
property string listingKey: modelData.key
property alias rowItem: delegateRow
property bool checked: delegateRow.children[0].checked
Rectangle {
@ -209,11 +210,28 @@ Item {
color: "transparent"
//border.color: "royalblue"
Image {
source: "file:///" + modelData.product_image_file
id: productImage
anchors.verticalCenter: parent.verticalCenter
width: parent.width; height: parent.height - 10
fillMode: Image.PreserveAspectFit
mipmap: true
asynchronous: true
onStatusChanged: {
if (productImage.status === Image.Error) {
// Handle the error by displaying a fallback or placeholder image
source = "image://listing?id=%1&image_id=%2".arg(modelData.key).arg("thumbnail.jpg")
}
}
// Wait for a short delay before attempting to load the actual image (which is created after its source is loaded :X)
Timer {
id: imageTimer
interval: 200
running: true
repeat: false
onTriggered: {
productImage.source = "image://listing?id=%1&image_id=%2".arg(modelData.key).arg(modelData.product_images[0].name)
}
}
}
}
Label {
@ -235,15 +253,52 @@ Item {
font.bold: true
elide: Label.ElideRight
}
Label {
Item {
x: productStockQtyColumn.x + (productStockQtyColumn.width - this.width) / 2//productStockQtyColumn.x
anchors.verticalCenter: parent.verticalCenter
Label {
////x: productStockQtyColumn.x + (productStockQtyColumn.width - this.width) / 2//productStockQtyColumn.x
////anchors.verticalCenter: parent.verticalCenter
text: modelData.quantity
////Layout.fillWidth: true
visible: true
//Layout.fillWidth: true
color: (NeroshopComponents.Style.darkTheme) ? "#ffffff" : "#000000"
font.bold: true
elide: Label.ElideRight
}
TextField {//
////x: productStockQtyColumn.x + (productStockQtyColumn.width - this.width) / 2//productStockQtyColumn.x
////anchors.verticalCenter: parent.verticalCenter
text: modelData.quantity
visible: false
//Layout.fillWidth: true
color: (NeroshopComponents.Style.darkTheme) ? "#ffffff" : "#000000"
font.bold: true
// TextField properties
width: contentWidth + 20
selectByMouse: true
inputMethodHints: Qt.ImhDigitsOnly
validator: RegExpValidator{ regExp: /[0-9]*/ }
background: Rectangle {
color: "transparent"
border.color: "#ffffff"
border.width: parent.activeFocus ? 2 : 1
}
function adjustQuantity() {
if(Number(this.text) >= 999999999) {
this.text = 999999999
}
if(Number(this.text) <= 0) {
this.text = 0
}
}
onEditingFinished: {
adjustQuantity()
User.setStockQuantity(modelData.key, this.text);
parent.forceActiveFocus()
}
}
} // Item
Button {
x: actionsColumn.x + (actionsColumn.width - this.width) / 2//actions.x
anchors.verticalCenter: parent.verticalCenter
@ -272,7 +327,7 @@ Item {
onPressed: mouse.accepted = false // without this, Button.onClicked won't work
cursorShape: Qt.PointingHandCursor
}
onClicked: User.delistProduct(modelData.product_id)
onClicked: User.delistProduct(modelData.key)
}
/*Label {
x: ?Column.x + (?Column.width - this.width) / 2//?.x

@ -7,7 +7,7 @@ import "." as NeroshopComponents
Item {
id: searchBar
width: childrenRect.width; height: childrenRect.height
property var model: Backend.getSearchResults(searchField.text)
property var model: Backend.getListingsBySearchTerm(searchField.text)
TextField {
id: searchField
color: (NeroshopComponents.Style.darkTheme) ? "#ffffff" : "#000000"// textColor

@ -221,7 +221,7 @@ Popup {
}
TabButton {
text: (hideTabText) ? qsTr(FontAwesome.monero) : qsTr("Node")//.arg(FontAwesome.monero)
text: (hideTabText) ? qsTr(FontAwesome.monero) : qsTr("Network")//.arg(FontAwesome.monero)
width: implicitWidth + 20
onClicked: {
settingsStack.currentIndex = 1

@ -194,7 +194,7 @@ Page {
Text {
id: stockStatusText
property bool status: (stock_available > 0)
property int stock_available: productPage.model.quantity//Backend.getStockAvailable(productPage.model.product_id)
property int stock_available: productPage.model.quantity
text: qsTr(status ? "In stock" : "Out of stock")
color: status ? "#31652c" : "#d61f1f"
font.bold: true

@ -308,12 +308,33 @@ std::vector<uint8_t> neroshop::msgpack::process(const std::vector<uint8_t>& requ
std::string key = params_object["key"];
assert(params_object["value"].is_string());
std::string value = params_object["value"];
assert(params_object["verified"].is_boolean());
bool verified = params_object["verified"].get<bool>();
// Retrieve the original data from the DHT
std::string current_value = node.send_get(key);
if(verified == false) {
code = static_cast<int>(KadResultCode::DataVerificationFailed);
response_object["error"]["code"] = code;
response_object["error"]["message"] = "Data verification failed";
response_object["tid"] = tid;
response = nlohmann::json::to_msgpack(response_object);
return response;
}
// Verify signature
// ...
// Send put messages to the closest nodes in your routing table (IPC mode)
int put_messages_sent = node.send_put(key, value);
code = (put_messages_sent <= 0)
? static_cast<int>(KadResultCode::StoreFailed)
: static_cast<int>(KadResultCode::Success);
std::cout << "Number of nodes you've sent a put message to: " << put_messages_sent << "\n";
// Store the key-value pair in your own node as well
node.store(key, value);
// Mapping data already exists in database so no need to call node.map()
// Return success response
response_object["version"] = std::string(NEROSHOP_DHT_VERSION);
response_object["response"]["id"] = node.get_id();
response_object["response"]["code"] = (code != 0) ? static_cast<int>(KadResultCode::StorePartial) : code;
response_object["response"]["message"] = (code != 0) ? "Store failed" : "Success";
}
//-----------------------------------------------------
response_object["tid"] = tid; // transaction id - MUST be the same as the request object's id

@ -3,7 +3,7 @@
#include "node.hpp"
#include "routing_table.hpp"
// TODO: DHT blacklisted nodes and probably callbacks/periodic node liveliness checks
// TODO: DHT blacklisted nodes
namespace neroshop {
@ -25,6 +25,7 @@ enum class KadResultCode {
NetworkError, // A network error occurred during the operation.
InvalidRequest,
ParseError,
DataVerificationFailed,
};
namespace kademlia {

@ -50,13 +50,11 @@ void neroshop::Mapper::add(const std::string& key, const std::string& value) {
// Store user-related data in the user_ids and display_names unordered maps
if (json.contains("monero_address") && json["monero_address"].is_string()) {
std::string user_id = json["monero_address"].get<std::string>();
user_ids[user_id].push_back(key);
std::cout << "user_id (" << user_id << ") has been mapped to account key (" << key << ")\n";
user_ids[user_id].push_back(key);//std::cout << "user_id (" << user_id << ") has been mapped to account key (" << key << ")\n";
}
if (json.contains("display_name") && json["display_name"].is_string()) {
std::string display_name = json["display_name"].get<std::string>();
display_names[display_name].push_back(key);
std::cout << "display_name (" << display_name << ") has been mapped to account key (" << key << ")\n";
display_names[display_name].push_back(key);//std::cout << "display_name (" << display_name << ") has been mapped to account key (" << key << ")\n";
}
}
//-----------------------------------------------

@ -316,7 +316,7 @@ int neroshop::Node::put(const std::string& key, const std::string& value) {
return true;
}
// If node has the key but the value has been altered, verify data integrity or ownership then update the data
// If node has the key but the value has been altered, verify data integrity and ownership then update the data
if (has_key(key) && get(key) != value) {
std::cout << "Updating value for key (" << key << ")\n";
return set(key, value);
@ -359,17 +359,21 @@ int neroshop::Node::set(const std::string& key, const std::string& value) {
nlohmann::json json = nlohmann::json::parse(value); // Already validated so we just need to parse it without checking for errors
// Detect when key is the same but the value has changed
if(has_key(key)) {
std::string preexisting_value = find_value(key);//std::cout << "preexisting_value: " << preexisting_value << std::endl;
std::string current_value = find_value(key);//std::cout << "preexisting_value: " << preexisting_value << std::endl;
// Compare the preexisting value with the new value
if (preexisting_value != value) {
std::cout << "Value modification detected. Performing signature verification ...\n";//std::cout << "Value mismatch. Skipping ...\n";//return false;}
if (current_value != value) {
////std::cout << "Value modification detected. Performing signature verification ...\n";//std::cout << "Value mismatch. Skipping ...\n";//return false;}
// Verify signature using user's public key (required for account data and listing data)
if (json.contains("signature")) {
assert(json["signature"].is_string());
std::string signature = json["signature"].get<std::string>();//neroshop::base64_decode(json["signature"].get<std::string>());
// READ THIS!!
// Unfortunately data verification can only be done on the client side since the wallet is used for verification and it only exists on the client side
// The only way to verify from the daemon backend would be to use the RSA keys instead of Monero keys for signing/verifying
// Get the public key and other account data from the user
/////std::string public_key = json["public_key"].get<std::string>();//user.get_public_key(); // Get the public key from the user object
////std::string user_id = json["monero_address"].get<std::string>();//user.get_id(); // Get the user ID from the user object
@ -831,9 +835,25 @@ void neroshop::Node::republish() {
//-----------------------------------------------------------------------------
static bool is_expired(const std::string& expiration_date) {
// Get the current UTC time
std::time_t current_time = std::time(nullptr);
std::tm* current_tm = std::gmtime(&current_time);
// Parse the expiration date string
std::tm expiration_tm{};
std::istringstream ss(expiration_date);
ss >> std::get_time(&expiration_tm, "%Y-%m-%d %H:%M:%S");
// Compare the expiration time with the current time
return (std::mktime(&expiration_tm) <= std::mktime(current_tm));
}
bool neroshop::Node::validate(const std::string& key, const std::string& value) {
assert(key.length() == 64 && "Key length is not 64 characters");
assert(!value.empty() && "Value is empty");
// Ensure that the value is a valid JSON
// Ensure that the value is valid JSON
nlohmann::json json;
try {
json = nlohmann::json::parse(value);
@ -841,12 +861,29 @@ bool neroshop::Node::validate(const std::string& key, const std::string& value)
std::cerr << "JSON parsing error: " << e.what() << std::endl;
return false; // Invalid value, return false
}
// Make sure value contains a metadata field
if(!json.contains("metadata")) {
std::cerr << "No metadata found\n";
return false;
}
// Check whether data is expired so we don't store it
if(json.contains("expiration_date")) {
assert(json["expiration_date"].is_string());
std::string expiration_date = json["expiration_date"].get<std::string>();
if(is_expired(expiration_date)) {
std::cerr << "Data has expired (exp date: " << expiration_date << " UTC)\n";
// Remove the data from hash table if it was previously stored
if(has_key(key)) {
if(remove(key) == true) {
std::cout << "Expired data with key (" << key << ") has been removed from hash table\n";
}
}
return false;
}
}
return true;
}
@ -877,6 +914,7 @@ void neroshop::Node::periodic_refresh() {
//-----------------------------------------------------------------------------
void neroshop::Node::periodic_check() {
std::vector<std::string> dead_node_ids {};
while(true) {
{
// Acquire the lock before accessing the routing table
@ -905,6 +943,7 @@ void neroshop::Node::periodic_check() {
if(node->is_dead()) {
std::cout << "\033[0;91m" << node->public_ip_address << ":" << node_port << "\033[0m marked as dead\n";
if(routing_table->has_node(node->public_ip_address, node_port)) {
dead_node_ids.push_back(node->get_id());
routing_table->remove_node(node->public_ip_address, node_port); // Already has internal write_lock
}
}
@ -913,6 +952,9 @@ void neroshop::Node::periodic_check() {
// read_lock is released here
}
on_dead_node(dead_node_ids);
// Clear the vector for the next iteration
dead_node_ids.clear();
// Sleep for a specified interval
std::this_thread::sleep_for(std::chrono::seconds(NEROSHOP_DHT_PERIODIC_CHECK_INTERVAL));
}
@ -960,7 +1002,7 @@ void neroshop::Node::periodic_check() {
//-----------------------------------------------------------------------------
void neroshop::Node::on_ping_callback(const std::vector<uint8_t>& buffer, const struct sockaddr_in& client_addr) {
void neroshop::Node::on_ping(const std::vector<uint8_t>& buffer, const struct sockaddr_in& client_addr) {
if (buffer.size() > 0) {
nlohmann::json message = nlohmann::json::from_msgpack(buffer);
if (message.contains("query") && message["query"] == "ping") {
@ -981,6 +1023,47 @@ void neroshop::Node::on_ping_callback(const std::vector<uint8_t>& buffer, const
//-----------------------------------------------------------------------------
void neroshop::Node::on_dead_node(const std::vector<std::string>& node_ids) {
for (const auto& dead_node_id : node_ids) {
//std::cout << "Processing dead node ID: " << dead_node_id << std::endl;
auto closest_nodes = find_node(dead_node_id, NEROSHOP_DHT_REPLICATION_FACTOR);
for (auto& routing_table_node : closest_nodes) {
if (routing_table_node == nullptr) continue;
std::string routing_table_node_ip = (routing_table_node->get_ip_address() == this->public_ip_address) ? "127.0.0.1" : routing_table_node->get_ip_address();
uint16_t routing_table_node_port = routing_table_node->get_port();
// Skip the bootstrap nodes (as they are not involved in the replacement of dead nodes)
if (routing_table_node->is_bootstrap_node()) continue;
// Send find_node to make up for dead nodes
auto nodes = send_find_node(dead_node_id, routing_table_node_ip, routing_table_node_port);
if(nodes.empty()) {
std::cerr << "find_node: No nodes found\n"; continue;
}
// Then add nodes to the routing table
for (auto node : nodes) {
if (node->get_id() == dead_node_id) continue; // ignore the dead node
// Ping the received nodes before adding them
std::string node_ip = (node->get_ip_address() == this->public_ip_address) ? "127.0.0.1" : node->get_ip_address();
uint16_t node_port = node->get_port();
if(!ping(node_ip, node_port)) {
node->check_counter++;
continue; // Skip the node and continue with the next iteration
}
// Update the routing table with the received nodes
if(!routing_table->has_node(node->public_ip_address, node_port)) {
routing_table->add_node(std::unique_ptr<neroshop::Node>(node));
}
}
}
}
}
//-----------------------------------------------------------------------------
void neroshop::Node::run() {
run_optimized();
@ -1024,7 +1107,7 @@ void neroshop::Node::run() {
}
// Add the node that pinged this node to the routing table
on_ping_callback(buffer, client_addr);
on_ping(buffer, client_addr);
};
// Create a detached thread to handle the request
@ -1097,7 +1180,7 @@ void neroshop::Node::run_optimized() {
}
// Add the node that pinged this node to the routing table
on_ping_callback(buffer, client_addr);
on_ping(buffer, client_addr);
};
// Create a detached thread to handle the request

@ -90,7 +90,8 @@ public:
void republish();
bool validate(const std::string& key, const std::string& value); // Validates data before storing it
//---------------------------------------------------
void on_ping_callback(const std::vector<uint8_t>& buffer, const struct sockaddr_in& client_addr);
void on_ping(const std::vector<uint8_t>& buffer, const struct sockaddr_in& client_addr);
void on_dead_node(const std::vector<std::string>& node_ids);
////bool on_keyword_blocked(const std::string& keyword);
////bool on_node_blacklisted(const std::string& address);
////bool on_data_expired();

@ -238,6 +238,27 @@ void neroshop::Client::get(const std::string& key, std::string& reply) {
std::cerr << "An error occurred: " << "Node was disconnected" << std::endl;
}
}
void neroshop::Client::set(const std::string& key, const std::string& value, bool verified, std::string& reply) {
// Send set - no id or tid required for IPC client requests. The DHT server will deal with that
nlohmann::json args_obj = { {"key", key}, {"value", value}, {"verified", verified} };
nlohmann::json query_object = { {"version", std::string(NEROSHOP_DHT_VERSION)}, {"query", "set"}, {"args", args_obj}, {"tid", nullptr} };
std::vector<uint8_t> packed_data = nlohmann::json::to_msgpack(query_object);
send(packed_data);
// Receive response
try {
std::vector<uint8_t> response;
receive(response);
try {
nlohmann::json response_object = nlohmann::json::from_msgpack(response);
reply = response_object.dump();//return response_object.dump();
} catch (const nlohmann::detail::parse_error& e) {
std::cerr << "Failed to parse server response: " << e.what() << std::endl;
}
} catch (const nlohmann::detail::parse_error& e) {
std::cerr << "An error occurred: " << "Node was disconnected" << std::endl;
}
}
////////////////////
void neroshop::Client::close() {
::close(sockfd);

@ -56,6 +56,7 @@ public:
// Interactions with the DHT node, which only exists on the client side via IPC server
void put(const std::string& key, const std::string& value, std::string& response);
void get(const std::string& key, std::string& response);
void set(const std::string& key, const std::string& value, bool verified, std::string& response);
void close(); // kills socket
void shutdown(); // shuts down connection (disconnects from server)
void disconnect(); // breaks connection to server then closes the client socket // combination of shutdown() and close()

@ -96,9 +96,9 @@ std::string neroshop::Seller::list_item(
std::string value = data.second;//std::cout << "key: " << data.first << "\nvalue: " << data.second << "\n";
// Send put request to neighboring nodes (and your node too JIC)
std::string put_response;
client->put(key, value, put_response);
std::cout << "Received response: " << put_response << "\n";
std::string response;
client->put(key, value, response);
std::cout << "Received response (put): " << response << "\n";
// Return listing key
return key;
@ -109,15 +109,54 @@ std::string neroshop::Seller::list_item(
}*/
// static_cast<Seller *>(user)->list_item(ball, 50, 8.50, "usd", 0.00, 0, 0, "", "new"); // $0.50 cents off every 2 balls
////////////////////
void neroshop::Seller::delist_item(const std::string& product_id) {
neroshop::db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
database->execute_params("DELETE FROM listings WHERE product_id = $1 AND seller_id = $2", { product_id, get_id() }); // update item stock to 0 beforehand or nah?
void neroshop::Seller::delist_item(const std::string& listing_key) {
// Transition from Sqlite to DHT:
Client * client = Client::get_main_client();
// TODO: remove product from table cart_item, table images, table products, and table product_ratings as well
}
////////////////////
void neroshop::Seller::delist_item(const neroshop::Product& item) {
delist_item(item.get_id());
// Get the value of the corresponding key from the DHT
std::string response;
client->get(listing_key, response); // TODO: error handling
std::cout << "Received response (get): " << response << "\n";
// Parse the response
nlohmann::json json = nlohmann::json::parse(response);
if(json.contains("error")) {
neroshop::print("set_stock_quantity: key is lost or missing from DHT", 1);
return; // Key is lost or missing from DHT, return
}
const auto& response_obj = json["response"];
assert(response_obj.is_object());
if (response_obj.contains("value") && response_obj["value"].is_string()) {
const auto& value = response_obj["value"].get<std::string>();
nlohmann::json value_obj = nlohmann::json::parse(value);
assert(value_obj.is_object());//std::cout << value_obj.dump(4) << "\n";
std::string metadata = value_obj["metadata"].get<std::string>();
if (metadata != "listing") { std::cerr << "Invalid metadata. \"listing\" expected, got \"" << metadata << "\" instead\n"; return; }
// Verify ownership
std::string seller_id = value_obj["seller_id"].get<std::string>();
if(seller_id != wallet->get_primary_address()) {
neroshop::print("delist_item: you cannot delist this since you are not the listing's creator", 1);
return;
}
// Verify the signature
std::string listing_id = value_obj["id"].get<std::string>();
std::string signature = value_obj["signature"].get<std::string>();
bool verified = wallet->verify_message(listing_id, signature);
// Might be a good idea to set the stock quantity to zero beforehand or nah?
////value_obj["quantity"] = 0;
// Finally, set the expiration date
auto now = std::chrono::system_clock::now();
auto in_time_t = std::chrono::system_clock::to_time_t(now); // current time
std::stringstream datetime;
datetime << std::put_time(std::gmtime(&in_time_t), "%Y-%m-%d %H:%M:%S");
std::string utc_time = datetime.str();
value_obj["expiration_date"] = utc_time;//value_obj["valid_until"] = utc_time;
// Send set request containing the updated value with the same key as before
std::string modified_value = value_obj.dump();
std::string response;
client->set(listing_key, modified_value, verified, response);
std::cout << "Received response (set): " << response << "\n";
}
}
////////////////////
////////////////////
@ -284,34 +323,47 @@ void neroshop::Seller::update_customer_orders() { // this function is faster (I
////////////////////
// setters - item and inventory-related stuff
////////////////////
void neroshop::Seller::set_stock_quantity(const std::string& product_id, unsigned int stock_qty) {
// seller must be logged in
if(!is_logged()) {NEROSHOP_TAG_OUT std::cout << "\033[0;91m" << "You must be logged in to set stock" << "\033[0m" << std::endl; return;}
// user must be an actual seller, not a buyer
if(!is_seller()) {neroshop::print("Must be a seller to set stock (id: " + product_id + ")", 2); return;}
// a seller can create an item and then register it to the database
////if(product_id <= 0) {NEROSHOP_TAG_OUT std::cout << "\033[0;91m" << "Could not set stock_qty (invalid Product id)" << "\033[0m" << std::endl; return;}
/*// update stock_qty in database
db::Sqlite3 db("neroshop.db");
//db.execute("PRAGMA journal_mode = WAL;"); // this may reduce the incidence of SQLITE_BUSY errors (such as database being locked)
if(db.table_exists("inventory"))
db.update("inventory", "stock_qty", std::to_string(stock_qty), "product_id = " + product_id + " AND seller_id = " + get_id());
db.close();*/
////////////////////////////////
// postgresql
////////////////////////////////
#if defined(NEROSHOP_USE_POSTGRESQL)
//database->connect("host=127.0.0.1 port=5432 user=postgres password=postgres dbname=neroshoptest");
database->execute_params("UPDATE inventory SET stock_qty = $1 WHERE product_id = $2 AND seller_id = $3", { std::to_string(stock_qty), product_id, get_id() });
std::string item_name = database->get_text_params("SELECT name FROM item WHERE id = $1", { product_id });
neroshop::print("\"" + item_name + "\"'s stock has been updated", 3);
void neroshop::Seller::set_stock_quantity(const std::string& listing_key, int quantity) {
// Transition from Sqlite to DHT:
Client * client = Client::get_main_client();
// Get the value of the corresponding key from the DHT
std::string response;
client->get(listing_key, response); // TODO: error handling
std::cout << "Received response (get): " << response << "\n";
// Parse the response
nlohmann::json json = nlohmann::json::parse(response);
if(json.contains("error")) {
neroshop::print("set_stock_quantity: key is lost or missing from DHT", 1);
return; // Key is lost or missing from DHT, return
}
////////////////////////////////
#endif
}
////////////////////
void neroshop::Seller::set_stock_quantity(const neroshop::Product& item, unsigned int stock_qty) {
set_stock_quantity(item.get_id(), stock_qty);
const auto& response_obj = json["response"];
assert(response_obj.is_object());
if (response_obj.contains("value") && response_obj["value"].is_string()) {
const auto& value = response_obj["value"].get<std::string>();
nlohmann::json value_obj = nlohmann::json::parse(value);
assert(value_obj.is_object());//std::cout << value_obj.dump(4) << "\n";
std::string metadata = value_obj["metadata"].get<std::string>();
if (metadata != "listing") { std::cerr << "Invalid metadata. \"listing\" expected, got \"" << metadata << "\" instead\n"; return; }
// Verify ownership of the data (listing)
std::string seller_id = value_obj["seller_id"].get<std::string>();
if(seller_id != wallet->get_primary_address()) {
neroshop::print("set_stock_quantity: you cannot modify this listing since you are not the listing's creator", 1);
return;
}
// Verify the signature
std::string listing_id = value_obj["id"].get<std::string>();
std::string signature = value_obj["signature"].get<std::string>();
bool verified = wallet->verify_message(listing_id, signature);
// Finally, modify the quantity
value_obj["quantity"] = quantity;
// Send set request containing the updated value with the same key as before
std::string modified_value = value_obj.dump();
std::string response;
client->set(listing_key, modified_value, verified, response);
std::cout << "Received response (set): " << response << "\n";
}
}
////////////////////
////////////////////
@ -530,7 +582,7 @@ unsigned int neroshop::Seller::get_products_count() const {
neroshop::db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
int products_listed = database->get_integer_params("SELECT COUNT(product_id) FROM listings WHERE seller_id = $1;", { get_id() });
int products_listed = database->get_integer_params("SELECT COUNT(key) FROM mappings WHERE search_term = $1 AND content = 'listing';", { get_id() });
return products_listed;
}
////////////////////

@ -41,11 +41,9 @@ public:
const std::string& location
) const; // adds an item to the inventory
////void list_item(const neroshop::Product& item, unsigned int stock_qty, double sales_price = 0.00, std::string currency = "usd", double discount = 0.00, unsigned int discounted_items = 1, unsigned int discount_times = 1, std::string discount_expiry = ""/*"0000-00-00 00:00:00"*/, std::string condition = "new");
void delist_item(const std::string& product_id); // deletes an item from the inventory
void delist_item(const neroshop::Product& item);
void delist_item(const std::string& listing_key); // deletes an item from the inventory
// setters - item and inventory-related stuff
void set_stock_quantity(const std::string& product_id, unsigned int stock_qty);
void set_stock_quantity(const neroshop::Product& item, unsigned int stock_qty);
void set_stock_quantity(const std::string& listing_key, int quantity);
// setters - wallet-related stuff
void set_wallet(const neroshop::Wallet& wallet);// temporary - delete ASAP
// getters - seller rating system

@ -374,7 +374,11 @@ void neroshop::User::clear_favorites() {
int favorites_count = database->get_integer_params("SELECT COUNT(*) FROM favorites WHERE user_id = ?1", { this->id });
if(favorites_count <= 0) return; // table is empty so that means there is nothing to delete, exit function
// clear all items from favorites
database->execute_params("DELETE FROM favorites WHERE user_id = ?1", { this->id });
int rescode = database->execute_params("DELETE FROM favorites WHERE user_id = ?1", { this->id });
if (rescode != SQLITE_OK) {
neroshop::print("failed to clear favorites", 1);
return;
}
// clear favorites from vector as well
favorites.clear();
if(favorites.empty()) neroshop::print("your favorites have been cleared"); // confirm that favorites has been cleared

@ -730,18 +730,48 @@ QVariantList neroshop::Backend::getSellerRatings(const QString& user_id) {
//----------------------------------------------------------------
//----------------------------------------------------------------
QString neroshop::Backend::getDisplayNameByUserId(const QString& user_id) {
Client * client = Client::get_main_client();
db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
std::string display_name = database->get_text_params("SELECT name FROM users WHERE monero_address = $1", { user_id.toStdString() });
return QString::fromStdString(display_name);
std::string display_name = "";
std::string key = database->get_text_params("SELECT key FROM mappings WHERE search_term = $1 AND content = 'account' LIMIT 1;", { user_id.toStdString() });
if(key.empty()) return user_id;
// Get the value of the corresponding key from the DHT
std::string response;
client->get(key, response); // TODO: error handling
std::cout << "Received response (get): " << response << "\n";
// Parse the response
nlohmann::json json = nlohmann::json::parse(response);
if(json.contains("error")) {
int rescode = database->execute_params("DELETE FROM mappings WHERE key = ?1", { key });
if(rescode != SQLITE_OK) neroshop::print("sqlite error: DELETE failed", 1);
return user_id; // Key is lost or missing from DHT, skip to next iteration
}
const auto& response_obj = json["response"];
assert(response_obj.is_object());
if (response_obj.contains("value") && response_obj["value"].is_string()) {
const auto& value = response_obj["value"].get<std::string>();
nlohmann::json value_obj = nlohmann::json::parse(value);
assert(value_obj.is_object());//std::cout << value_obj.dump(4) << "\n";
std::string metadata = value_obj["metadata"].get<std::string>();
if (metadata != "user") { std::cerr << "Invalid metadata. \"user\" expected, got \"" << metadata << "\" instead\n"; return user_id; }
if(value_obj.contains("display_name") && value_obj["display_name"].is_string()) {
display_name = value_obj["display_name"].get<std::string>();
}
return (display_name.empty()) ? user_id : QString::fromStdString(display_name);
}
return user_id;
}
// un-tested
QString neroshop::Backend::getKeyByUserId(const QString& user_id) {
db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
std::string display_name = database->get_text_params("SELECT key FROM mappings WHERE search_term = $1 AND content = 'account' LIMIT 1;", { user_id.toStdString() });
return QString::fromStdString(display_name);
std::string key = database->get_text_params("SELECT key FROM mappings WHERE search_term = $1 AND content = 'account' LIMIT 1;", { user_id.toStdString() });
return QString::fromStdString(key);
}
//----------------------------------------------------------------
//----------------------------------------------------------------
@ -753,11 +783,39 @@ int neroshop::Backend::getCartMaximumQuantity() {
return neroshop::Cart::get_max_quantity();
}
//----------------------------------------------------------------
// not really used at the moment
int neroshop::Backend::getStockAvailable(const QString& product_id) {
Client * client = Client::get_main_client();
db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
int quantity = database->get_integer_params("SELECT quantity FROM listings WHERE product_id = $1 AND quantity > 0", { product_id.toStdString() });
return quantity;
std::string key = database->get_text_params("SELECT key FROM mappings WHERE search_term = $1 AND content = 'listing'", { product_id.toStdString() });
if(key.empty()) return 0;
// Get the value of the corresponding key from the DHT
std::string response;
client->get(key, response); // TODO: error handling
std::cout << "Received response (get): " << response << "\n";
// Parse the response
nlohmann::json json = nlohmann::json::parse(response);
if(json.contains("error")) {
int rescode = database->execute_params("DELETE FROM mappings WHERE key = ?1", { key });
if(rescode != SQLITE_OK) neroshop::print("sqlite error: DELETE failed", 1);
return 0; // Key is lost or missing from DHT, return
}
const auto& response_obj = json["response"];
assert(response_obj.is_object());
if (response_obj.contains("value") && response_obj["value"].is_string()) {
const auto& value = response_obj["value"].get<std::string>();
nlohmann::json value_obj = nlohmann::json::parse(value);
assert(value_obj.is_object());//std::cout << value_obj.dump(4) << "\n";
std::string metadata = value_obj["metadata"].get<std::string>();
if (metadata != "listing") { std::cerr << "Invalid metadata. \"listing\" expected, got \"" << metadata << "\" instead\n"; return 0; }
int quantity = value_obj["quantity"].get<int>();
return quantity;
}
return 0;
}
//----------------------------------------------------------------
//----------------------------------------------------------------
@ -825,7 +883,7 @@ QVariantList neroshop::Backend::getInventory(const QString& user_id) {
}
//----------------------------------------------------------------
//----------------------------------------------------------------
QVariantList neroshop::Backend::getSearchResults(const QString& searchTerm, int count) {
QVariantList neroshop::Backend::getListingsBySearchTerm(const QString& searchTerm, int count) {
// Transition from Sqlite to DHT:
Client * client = Client::get_main_client();
db::Sqlite3 * database = neroshop::get_database();
@ -1027,6 +1085,8 @@ QVariantList neroshop::Backend::getListings(ListingSorting sorting) {
}
}
sqlite3_finalize(stmt);
switch(sorting) {
case SortNone:
// Code for sorting by none - do nothing
@ -1095,8 +1155,6 @@ QVariantList neroshop::Backend::getListings(ListingSorting sorting) {
// Code for handling unknown sorting value - do nothing
break;
}
sqlite3_finalize(stmt);
return catalog;
}
@ -1226,16 +1284,6 @@ void neroshop::Backend::createOrder(UserController * user_controller, const QStr
user_controller->createOrder(shipping_address);
}
//----------------------------------------------------------------
// TODO: run this function periodically
int neroshop::Backend::deleteExpiredOrders() {
db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
// If order is at least 2 years old or older, then it is considered expired. Therefore it must be deleted
std::string modifier = "+" + std::to_string(2) + " years";//std::cout << modifier << std::endl;
std::string command = "DELETE FROM orders WHERE datetime(created_at, $1) <= datetime('now');";
return database->execute_params(command, { modifier }); // 0 = success
}
//----------------------------------------------------------------
//----------------------------------------------------------------
QVariantList neroshop::Backend::getNodeListDefault(const QString& coin) const {
QVariantList node_list;
@ -1349,40 +1397,15 @@ QVariantList neroshop::Backend::validateDisplayName(const QString& display_name)
}
return { false, QString::fromStdString(default_message) };
}
// Check database to see if display name is available - might not be necessary as user ids are unique
auto name_check_result = checkDisplayName(display_name);
if(!name_check_result[0].toBool()) {////if(!Validator::validate_display_name(display_name.toStdString())) return false;
bool boolean_result = name_check_result[0].toBool();
QString message_result = name_check_result[1].toString();
return { boolean_result, message_result };
}
return { true, "" };
}
//----------------------------------------------------------------
// TODO: replace function return type with enum
QVariantList neroshop::Backend::checkDisplayName(const QString& display_name) const {
db::Sqlite3 * database = neroshop::get_database();
if(!database->table_exists("users")) { return {true, ""}; }
std::string name = database->get_text_params("SELECT name FROM users WHERE name = $1 COLLATE NOCASE;", { display_name.toStdString() });
if(name.empty()) return { true, "Display name is available for use" };// Name is not taken which means that the user is good to go!
// Empty display names are acceptable
bool is_name_empty = display_name.isEmpty();
if(is_name_empty) return { true, "No display name set" };
// Note: both database and input display names are converted to lowercase strings and then compared within the app
std::string name_lowercase = QString::fromStdString(name).toLower().toStdString();//QString::fromUtf8(name.data(), name.size()).toLower();
std::string display_name_lowercase = display_name.toLower().toStdString();
if(name_lowercase == display_name_lowercase) {
return { false, "This username is already taken" };////result_list << false << QString("This username is already taken");return result_list;
}
return { true, "" };
}
//----------------------------------------------------------------
// TODO: replace function return type with enum
QVariantList neroshop::Backend::registerUser(WalletController* wallet_controller, const QString& display_name, UserController * user_controller) {
// TODO: Make sure daemon is connected first
if(!DaemonManager::isDaemonServerBound()) {
return { false, "Please wait for the daemon IPC server to connect first" };
return { false, "Please wait for the daemon LIPC server to connect first" };
}
//---------------------------------------------
db::Sqlite3 * database = neroshop::get_database();
@ -1441,16 +1464,16 @@ QVariantList neroshop::Backend::registerUser(WalletController* wallet_controller
// Store login credentials in DHT
Client * client = Client::get_main_client();
// If client is not connect, return error
if (!client->is_connected()) return { false, "Not connected to daemon server" };
if (!client->is_connected()) return { false, "Not connected to daemon LIPC server" };
// Serialize user object
auto data = Serializer::serialize(*user_controller->_user);
std::string key = data.first;
std::string value = data.second;
// Send put and receive response
std::string put_response;
client->put(key, value, put_response);
std::cout << "Received response: " << put_response << "\n";
std::string response;
client->put(key, value, response);
std::cout << "Received response (put): " << response << "\n";
//---------------------------------------------
emit user_controller->userChanged();

@ -33,7 +33,7 @@ public:
SortByMostSales,
};
//Q_PROPERTY(int categoryProductCount READ getCategoryProductCount NOTIFY categoryProductCountChanged)
//Q_PROPERTY(QVariantList searchResults READ getSearchResults NOTIFY searchResultsChanged)
//Q_PROPERTY(QVariantList searchResults READ getListingsBySearchTerm NOTIFY searchResultsChanged)
Q_INVOKABLE QString urlToLocalFile(const QUrl& url) const;
Q_INVOKABLE void copyTextToClipboard(const QString& text);
@ -62,7 +62,6 @@ public:
Q_INVOKABLE bool isWalletDaemonRunning() const;
QVariantList validateDisplayName(const QString& display_name) const; // Validates display name based on regex requirements
QVariantList checkDisplayName(const QString& display_name) const; // Checks database for display name availability
Q_INVOKABLE QVariantList registerUser(WalletController* wallet_controller, const QString& display_name, UserController * user_controller);
Q_INVOKABLE bool loginWithWalletFile(WalletController* wallet_controller, const QString& path, const QString& password, UserController * user_controller);
@ -74,7 +73,7 @@ public:
Q_INVOKABLE QVariantList getListingsByCategory(int category_id);
Q_INVOKABLE QVariantList getListingsByMostRecentLimit(int limit);
Q_INVOKABLE QVariantList getSearchResults(const QString& search_term, int count = 1000); // count is the maximum number of search results (total). The search results (per page) can be between 10-100 or 50-100
Q_INVOKABLE QVariantList getListingsBySearchTerm(const QString& search_term, int count = 1000); // count is the maximum number of search results (total). The search results (per page) can be between 10-100 or 50-100
// Products should be registered so that sellers can list pre-existing products without the need to duplicate a product which is unnecessary and can make the database bloated
Q_INVOKABLE bool createFolders();
@ -106,8 +105,6 @@ public:
Q_INVOKABLE QVariantList getInventory(const QString& user_id);
Q_INVOKABLE void createOrder(UserController * user_controller, const QString& shipping_address);
static int deleteExpiredOrders();
signals:
//void categoryProductCountChanged();//(int category_id);

@ -137,8 +137,10 @@ int main(int argc, char *argv[])
////qmlRegisterUncreatableMetaObject(neroshop::staticMetaObject, "neroshop.namespace", 1, 0, "Neroshop", "Error: only enums");
// TableModel
////qmlRegisterType<TableModel>("neroshop.TableModel", 1, 0, "TableModel"); // Usage: import neroshop.TableModel ... TableModel { id: tableModel }
// Register the ListingSorting type
// Register the ListingSorting enum type
qmlRegisterUncreatableType<Backend>("neroshop.ListingSorting", 1, 0, "Listing", "Enums cannot be created."); // Usage: import neroshop.ListingSorting 1.0 ... console.log("SortByCategory",Listing.SortByCategory);
// Register the InventorySorting enum type
qmlRegisterUncreatableType<UserController>("neroshop.InventorySorting", 1, 0, "Inventory", "Enums cannot be created.");
engine.addImageProvider(WALLET_QR_PROVIDER, new WalletQrProvider(WALLET_QR_PROVIDER));
engine.addImageProvider(AVATAR_IMAGE_PROVIDER, new ImageProvider(AVATAR_IMAGE_PROVIDER));

@ -1,7 +1,11 @@
#include "user_controller.hpp"
#include <QDateTime>
#include "../core/cart.hpp"
#include "../core/category.hpp"
#include "../core/database/database.hpp"
#include "../core/protocol/transport/client.hpp"
#include "../core/tools/logger.hpp"
neroshop::UserController::UserController(QObject *parent) : QObject(parent)
@ -106,24 +110,27 @@ int quantity, double price, const QString& currency, const QString& condition, c
location.toStdString()
);
emit productsCountChanged();
emit inventoryChanged();
return QString::fromStdString(listing_key);
}
//----------------------------------------------------------------
void neroshop::UserController::delistProduct(const QString& product_id) {
void neroshop::UserController::delistProduct(const QString& listing_key) {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
auto seller = dynamic_cast<neroshop::Seller *>(_user.get());
seller->delist_item(product_id.toStdString());
seller->delist_item(listing_key.toStdString());
emit productsCountChanged();
emit inventoryChanged();
}
//----------------------------------------------------------------
void neroshop::UserController::delistProducts(const QStringList& product_ids) {
void neroshop::UserController::delistProducts(const QStringList& listing_keys) {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
auto seller = dynamic_cast<neroshop::Seller *>(_user.get());
for(const auto& product_id : product_ids) {
seller->delist_item(product_id.toStdString());//std::cout << product_id.toStdString() << " has been delisted\n";// <- may be "undefined"
for(const auto& listing_key : listing_keys) {
seller->delist_item(listing_key.toStdString());//std::cout << listing_key.toStdString() << " has been delisted\n";// <- may be "undefined"
}
emit productsCountChanged(); // Only emit the signal once we're done delisting all the products
emit inventoryChanged();
}
//----------------------------------------------------------------
//----------------------------------------------------------------
@ -198,6 +205,15 @@ bool neroshop::UserController::exportAvatar() {
return _user->export_avatar();
}
//----------------------------------------------------------------
//----------------------------------------------------------------
void neroshop::UserController::setStockQuantity(const QString& listing_id, int quantity) {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
auto seller = dynamic_cast<neroshop::Seller *>(_user.get());
seller->set_stock_quantity(listing_id.toStdString(), quantity);
emit productsCountChanged();
emit inventoryChanged();
}
//----------------------------------------------------------------
//----------------------------------------------------------------
neroshop::User * neroshop::UserController::getUser() const {
return _user.get();
@ -233,122 +249,15 @@ int neroshop::UserController::getCartQuantity() const {
}
//----------------------------------------------------------------
//----------------------------------------------------------------
QVariantList neroshop::UserController::getInventory() const {
// This code causes the images to not load for some reason unless the app is restarted. Edit: fixed
QVariantList neroshop::UserController::getInventory(InventorySorting sorting) const {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
neroshop::db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
std::string command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
/*switch(sortType) {
case InventorySorting::SortNone:
std::cout << "SortNone (" << SortNone << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;
case InventorySorting::SortByDateOldest:
std::cout << "SortByDateOldest (" << SortByDateOldest << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id ORDER BY date ASC;";
break;
case InventorySorting::SortByDateRecent:
std::cout << "SortByDateRecent (" << SortByDateRecent << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id ORDER BY date DESC;";
break;*/
/*case InventorySorting::SortByName:
std::cout << "SortByName (" << SortByName << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*case InventorySorting::SortByQuantity:
std::cout << "SortByQuantity (" << SortByQuantity << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*case InventorySorting::SortByPrice:
std::cout << "SortByPrice (" << SortByPrice << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*case InventorySorting::SortByProductCode:
std::cout << "SortByProductCode (" << SortByProductCode << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*case InventorySorting::SortByCategory:
std::cout << "SortByCategory (" << SortByCategory << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*case InventorySorting::SortByCondition:
std::cout << "SortByCondition (" << SortByCondition << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*case InventorySorting:: :
std::cout << "Sort? (" << ? << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*default:
std::cout << "default: SortNone (" << SortNone << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;
}*/
sqlite3_stmt * stmt = nullptr;
// Prepare (compile) statement
if(sqlite3_prepare_v2(database->get_handle(), command.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
neroshop::print("sqlite3_prepare_v2: " + std::string(sqlite3_errmsg(database->get_handle())), 1);
return {};
}
// Bind user_id to TEXT
std::string user_id = _user->get_id();
if(sqlite3_bind_text(stmt, 1, user_id.c_str(), user_id.length(), SQLITE_STATIC) != SQLITE_OK) {
neroshop::print("sqlite3_bind_text (arg: 1): " + std::string(sqlite3_errmsg(database->get_handle())), 1);
sqlite3_finalize(stmt);
return {};
}
// Check whether the prepared statement returns no data (for example an UPDATE)
if(sqlite3_column_count(stmt) == 0) {
neroshop::print("No data found. Be sure to use an appropriate SELECT statement", 1);
return {};
}
QVariantList inventory_array;
// Get all table values row by row
while(sqlite3_step(stmt) == SQLITE_ROW) {
QVariantMap inventory_object; // Create an object for each row
for(int i = 0; i < sqlite3_column_count(stmt); i++) {
std::string column_value = (sqlite3_column_text(stmt, i) == nullptr) ? "NULL" : reinterpret_cast<const char *>(sqlite3_column_text(stmt, i));////if(sqlite3_column_text(stmt, i) == nullptr) {throw std::runtime_error("column is NULL");}
////std::cout << column_value << " (" << i << ")" << std::endl;//std::cout << sqlite3_column_name(stmt, i) << std::endl;
// listings table
if(i == 0) inventory_object.insert("listing_uuid", QString::fromStdString(column_value));
if(i == 1) inventory_object.insert("product_id", QString::fromStdString(column_value));
if(i == 2) inventory_object.insert("seller_id", QString::fromStdString(column_value));
if(i == 3) inventory_object.insert("quantity", QString::fromStdString(column_value).toInt());
if(i == 4) inventory_object.insert("price", QString::fromStdString(column_value).toDouble());
if(i == 5) inventory_object.insert("currency", QString::fromStdString(column_value));
if(i == 6) inventory_object.insert("condition", QString::fromStdString(column_value));
if(i == 7) inventory_object.insert("location", QString::fromStdString(column_value));
if(i == 8) inventory_object.insert("date", QString::fromStdString(column_value));
// products table
if(i == 9) inventory_object.insert("product_uuid", QString::fromStdString(column_value));
if(i == 10) inventory_object.insert("product_name", QString::fromStdString(column_value));
if(i == 11) inventory_object.insert("product_description", QString::fromStdString(column_value));
if(i == 12) inventory_object.insert("weight", QString::fromStdString(column_value).toDouble());
if(i == 13) inventory_object.insert("product_attributes", QString::fromStdString(column_value));
if(i == 14) inventory_object.insert("product_code", QString::fromStdString(column_value));
if(i == 15) inventory_object.insert("product_category_id", QString::fromStdString(column_value).toInt());
// images table
//if(i == 16) inventory_object.insert("image_id", QString::fromStdString(column_value));
//if(i == 17) inventory_object.insert("product_uuid", QString::fromStdString(column_value));
if(i == 18) inventory_object.insert("product_image_file", QString::fromStdString(column_value));
//if(i == 19) inventory_object.insert("product_image_data", QString::fromStdString(column_value));
}
inventory_array.append(inventory_object);
}
Client * client = Client::get_main_client();
sqlite3_finalize(stmt);
return inventory_array;
}
//----------------------------------------------------------------
QVariantList neroshop::UserController::getInventoryInStock() const {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
neroshop::db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
std::string command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 AND quantity > 0 GROUP BY images.product_id;";
std::string command = "SELECT DISTINCT key FROM mappings WHERE search_term = ?1 AND content = 'listing'";
sqlite3_stmt * stmt = nullptr;
// Prepare (compile) statement
if(sqlite3_prepare_v2(database->get_handle(), command.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
@ -372,107 +281,158 @@ QVariantList neroshop::UserController::getInventoryInStock() const {
// Get all table values row by row
while(sqlite3_step(stmt) == SQLITE_ROW) {
QVariantMap inventory_object; // Create an object for each row
QVariantList product_images;
for(int i = 0; i < sqlite3_column_count(stmt); i++) {
std::string column_value = (sqlite3_column_text(stmt, i) == nullptr) ? "NULL" : reinterpret_cast<const char *>(sqlite3_column_text(stmt, i));////if(sqlite3_column_text(stmt, i) == nullptr) {throw std::runtime_error("column is NULL");}
////std::cout << column_value << " (" << i << ")" << std::endl;//std::cout << sqlite3_column_name(stmt, i) << std::endl;
// listings table
if(i == 0) inventory_object.insert("listing_uuid", QString::fromStdString(column_value));
if(i == 1) inventory_object.insert("product_id", QString::fromStdString(column_value));
if(i == 2) inventory_object.insert("seller_id", QString::fromStdString(column_value));
if(i == 3) inventory_object.insert("quantity", QString::fromStdString(column_value).toInt());
if(i == 4) inventory_object.insert("price", QString::fromStdString(column_value).toDouble());
if(i == 5) inventory_object.insert("currency", QString::fromStdString(column_value));
if(i == 6) inventory_object.insert("condition", QString::fromStdString(column_value));
if(i == 7) inventory_object.insert("location", QString::fromStdString(column_value));
if(i == 8) inventory_object.insert("date", QString::fromStdString(column_value));
// products table
if(i == 9) inventory_object.insert("product_uuid", QString::fromStdString(column_value));
if(i == 10) inventory_object.insert("product_name", QString::fromStdString(column_value));
if(i == 11) inventory_object.insert("product_description", QString::fromStdString(column_value));
if(i == 12) inventory_object.insert("weight", QString::fromStdString(column_value).toDouble());
if(i == 13) inventory_object.insert("product_attributes", QString::fromStdString(column_value));
if(i == 14) inventory_object.insert("product_code", QString::fromStdString(column_value));
if(i == 15) inventory_object.insert("product_category_id", QString::fromStdString(column_value).toInt());
// images table
//if(i == 16) inventory_object.insert("image_id", QString::fromStdString(column_value));
//if(i == 17) inventory_object.insert("product_uuid", QString::fromStdString(column_value));
if(i == 18) inventory_object.insert("product_image_file", QString::fromStdString(column_value));
//if(i == 19) inventory_object.insert("product_image_data", QString::fromStdString(column_value));
std::string column_value = (sqlite3_column_text(stmt, i) == nullptr) ? "NULL" : reinterpret_cast<const char *>(sqlite3_column_text(stmt, i));//std::cout << column_value << " (" << i << ")" << std::endl;
if(column_value == "NULL") continue; // Skip invalid columns
QString key = QString::fromStdString(column_value);
// Get the value of the corresponding key from the DHT
std::string response;
client->get(key.toStdString(), response); // TODO: error handling
std::cout << "Received response (get): " << response << "\n";
// Parse the response
nlohmann::json json = nlohmann::json::parse(response);
if(json.contains("error")) {
int rescode = database->execute_params("DELETE FROM mappings WHERE key = ?1", { key.toStdString() });
if(rescode != SQLITE_OK) neroshop::print("sqlite error: DELETE failed", 1);
//emit productsCountChanged();
//emit inventoryChanged();
continue; // Key is lost or missing from DHT, skip to next iteration
}
const auto& response_obj = json["response"];
assert(response_obj.is_object());
if (response_obj.contains("value") && response_obj["value"].is_string()) {
const auto& value = response_obj["value"].get<std::string>();
nlohmann::json value_obj = nlohmann::json::parse(value);
assert(value_obj.is_object());//std::cout << value_obj.dump(4) << "\n";
std::string metadata = value_obj["metadata"].get<std::string>();
if (metadata != "listing") { std::cerr << "Invalid metadata. \"listing\" expected, got \"" << metadata << "\" instead\n"; continue; }
inventory_object.insert("key", key);
inventory_object.insert("listing_uuid", QString::fromStdString(value_obj["id"].get<std::string>()));
inventory_object.insert("seller_id", QString::fromStdString(value_obj["seller_id"].get<std::string>()));
inventory_object.insert("quantity", value_obj["quantity"].get<int>());
inventory_object.insert("price", value_obj["price"].get<double>());
inventory_object.insert("currency", QString::fromStdString(value_obj["currency"].get<std::string>()));
inventory_object.insert("condition", QString::fromStdString(value_obj["condition"].get<std::string>()));
if(value_obj.contains("location") && value_obj["location"].is_string()) {
inventory_object.insert("location", QString::fromStdString(value_obj["location"].get<std::string>()));
}
inventory_object.insert("date", QString::fromStdString(value_obj["date"].get<std::string>()));
assert(value_obj["product"].is_object());
const auto& product_obj = value_obj["product"];
inventory_object.insert("product_uuid", QString::fromStdString(product_obj["id"].get<std::string>()));
inventory_object.insert("product_name", QString::fromStdString(product_obj["name"].get<std::string>()));
inventory_object.insert("product_description", QString::fromStdString(product_obj["description"].get<std::string>()));
inventory_object.insert("product_category_id", get_category_id_by_name(product_obj["category"].get<std::string>()));
//inventory_object.insert("", QString::fromStdString(product_obj[""].get<std::string>()));
if (product_obj.contains("images") && product_obj["images"].is_array()) {
const auto& images_array = product_obj["images"];
for (const auto& image : images_array) {
if (image.contains("name") && image.contains("id")) {
const auto& image_name = image["name"].get<std::string>();
const auto& image_id = image["id"].get<int>();
QVariantMap image_map;
image_map.insert("name", QString::fromStdString(image_name));
image_map.insert("id", image_id);
product_images.append(image_map);
}
}
inventory_object.insert("product_images", product_images);
}
if (product_obj.contains("thumbnail") && product_obj["thumbnail"].is_string()) {
inventory_object.insert("product_thumbnail", QString::fromStdString(product_obj["thumbnail"].get<std::string>()));
}
}
inventory_array.append(inventory_object);
}
inventory_array.append(inventory_object);
}
sqlite3_finalize(stmt);
switch(sorting) {
case InventorySorting::SortNone:
std::cout << "SortNone (" << SortNone << ") selected\n";
break;
case InventorySorting::SortByAvailability: // Filter items with quantity less than 1
std::cout << "SortByAvailability (" << SortByAvailability << ") selected\n";
inventory_array.erase(std::remove_if(inventory_array.begin(), inventory_array.end(), [](const QVariant& variant) {
const QVariantMap& inventory_object = variant.toMap();
return inventory_object.value("quantity").toInt() < 1;
}), inventory_array.end());
break;
case InventorySorting::SortByQuantitySmallest:
std::sort(inventory_array.begin(), inventory_array.end(), [](const QVariant& a, const QVariant& b) {
const QVariantMap& itemA = a.toMap();
const QVariantMap& itemB = b.toMap();
return itemA.value("quantity").toInt() < itemB.value("quantity").toInt();
});
break;
case InventorySorting::SortByQuantityBiggest:
std::sort(inventory_array.begin(), inventory_array.end(), [](const QVariant& a, const QVariant& b) {
const QVariantMap& itemA = a.toMap();
const QVariantMap& itemB = b.toMap();
return itemA.value("quantity").toInt() > itemB.value("quantity").toInt();
});
break;
case InventorySorting::SortByDateOldest:
std::sort(inventory_array.begin(), inventory_array.end(), [](const QVariant& a, const QVariant& b) {
QVariantMap listingA = a.toMap();
QVariantMap listingB = b.toMap();
QString dateA = listingA["date"].toString();
QString dateB = listingB["date"].toString();
QDateTime dateTimeA = QDateTime::fromString(dateA, "yyyy-MM-dd HH:mm:ss");
QDateTime dateTimeB = QDateTime::fromString(dateB, "yyyy-MM-dd HH:mm:ss");
return inventory_array;
}
//----------------------------------------------------------------
QVariantList neroshop::UserController::getInventoryByDate() const {
if (!_user) throw std::runtime_error("neroshop::User is not initialized");
neroshop::db::Sqlite3 * database = neroshop::get_database();
if(!database) throw std::runtime_error("database is NULL");
std::string command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id ORDER BY date ASC;";
sqlite3_stmt * stmt = nullptr;
// Prepare (compile) statement
if(sqlite3_prepare_v2(database->get_handle(), command.c_str(), -1, &stmt, nullptr) != SQLITE_OK) {
neroshop::print("sqlite3_prepare_v2: " + std::string(sqlite3_errmsg(database->get_handle())), 1);
return {};
}
// Bind user_id to TEXT
std::string user_id = _user->get_id();
if(sqlite3_bind_text(stmt, 1, user_id.c_str(), user_id.length(), SQLITE_STATIC) != SQLITE_OK) {
neroshop::print("sqlite3_bind_text (arg: 1): " + std::string(sqlite3_errmsg(database->get_handle())), 1);
sqlite3_finalize(stmt);
return {};
}
// Check whether the prepared statement returns no data (for example an UPDATE)
if(sqlite3_column_count(stmt) == 0) {
neroshop::print("No data found. Be sure to use an appropriate SELECT statement", 1);
return {};
}
QVariantList inventory_array;
// Get all table values row by row
while(sqlite3_step(stmt) == SQLITE_ROW) {
QVariantMap inventory_object; // Create an object for each row
for(int i = 0; i < sqlite3_column_count(stmt); i++) {
std::string column_value = (sqlite3_column_text(stmt, i) == nullptr) ? "NULL" : reinterpret_cast<const char *>(sqlite3_column_text(stmt, i));////if(sqlite3_column_text(stmt, i) == nullptr) {throw std::runtime_error("column is NULL");}
////std::cout << column_value << " (" << i << ")" << std::endl;//std::cout << sqlite3_column_name(stmt, i) << std::endl;
// listings table
if(i == 0) inventory_object.insert("listing_uuid", QString::fromStdString(column_value));
if(i == 1) inventory_object.insert("product_id", QString::fromStdString(column_value));
if(i == 2) inventory_object.insert("seller_id", QString::fromStdString(column_value));
if(i == 3) inventory_object.insert("quantity", QString::fromStdString(column_value).toInt());
if(i == 4) inventory_object.insert("price", QString::fromStdString(column_value).toDouble());
if(i == 5) inventory_object.insert("currency", QString::fromStdString(column_value));
if(i == 6) inventory_object.insert("condition", QString::fromStdString(column_value));
if(i == 7) inventory_object.insert("location", QString::fromStdString(column_value));
if(i == 8) inventory_object.insert("date", QString::fromStdString(column_value));
// products table
if(i == 9) inventory_object.insert("product_uuid", QString::fromStdString(column_value));
if(i == 10) inventory_object.insert("product_name", QString::fromStdString(column_value));
if(i == 11) inventory_object.insert("product_description", QString::fromStdString(column_value));
if(i == 12) inventory_object.insert("weight", QString::fromStdString(column_value).toDouble());
if(i == 13) inventory_object.insert("product_attributes", QString::fromStdString(column_value));
if(i == 14) inventory_object.insert("product_code", QString::fromStdString(column_value));
if(i == 15) inventory_object.insert("product_category_id", QString::fromStdString(column_value).toInt());
// images table
//if(i == 16) inventory_object.insert("image_id", QString::fromStdString(column_value));
//if(i == 17) inventory_object.insert("product_uuid", QString::fromStdString(column_value));
if(i == 18) inventory_object.insert("product_image_file", QString::fromStdString(column_value));
//if(i == 19) inventory_object.insert("product_image_data", QString::fromStdString(column_value));
}
inventory_array.append(inventory_object);
return dateTimeA < dateTimeB;
});
break;
case InventorySorting::SortByDateNewest:
std::sort(inventory_array.begin(), inventory_array.end(), [](const QVariant& a, const QVariant& b) {
QVariantMap listingA = a.toMap();
QVariantMap listingB = b.toMap();
QString dateA = listingA["date"].toString();
QString dateB = listingB["date"].toString();
QDateTime dateTimeA = QDateTime::fromString(dateA, "yyyy-MM-dd HH:mm:ss");
QDateTime dateTimeB = QDateTime::fromString(dateB, "yyyy-MM-dd HH:mm:ss");
return dateTimeA > dateTimeB;
});
break;
case InventorySorting::SortByPriceLowest:
std::sort(inventory_array.begin(), inventory_array.end(), [](const QVariant& a, const QVariant& b) {
QVariantMap listingA = a.toMap();
QVariantMap listingB = b.toMap();
return listingA["price"].toDouble() < listingB["price"].toDouble();
});
break;
case InventorySorting::SortByPriceHighest:
std::sort(inventory_array.begin(), inventory_array.end(), [](const QVariant& a, const QVariant& b) {
QVariantMap listingA = a.toMap();
QVariantMap listingB = b.toMap();
return listingA["price"].toDouble() > listingB["price"].toDouble();
});
break;
/*case InventorySorting::SortByName:
std::cout << "SortByName (" << SortByName << ") selected\n";
command = "SELECT DISTINCT * FROM listings JOIN products ON products.uuid = listings.product_id JOIN images ON images.product_id = listings.product_id WHERE seller_id = $1 GROUP BY images.product_id;";
break;*/
/*case InventorySorting:: :
std::cout << "Sort? (" << ? << ") selected\n";
break;*/
default:
std::cout << "default: SortNone (" << SortNone << ") selected\n";
break;
}
sqlite3_finalize(stmt);
return inventory_array;
return inventory_array;
}
//----------------------------------------------------------------
//----------------------------------------------------------------
//----------------------------------------------------------------
bool neroshop::UserController::isUserLogged() const {
return (_user.get() != nullptr);
}

@ -15,34 +15,36 @@
#include "../core/seller.hpp"////"../core/user.hpp"
namespace neroshop {
/* Q_NAMESPACE // required for meta object creation
class UserController : public QObject, public neroshop::Seller {
Q_OBJECT
Q_ENUMS(InventorySorting)
public:
UserController(QObject *parent = nullptr);
~UserController();
enum InventorySorting {
SortNone = 0,
SortByDate,
SortByAvailability, // If item is in stock
SortByDateOldest,
SortByDateNewest,
SortByName,
SortByQuantity,
SortByPrice,
SortByQuantitySmallest,
SortByQuantityBiggest,
SortByPriceLowest,
SortByPriceHighest,
SortByProductCode,
SortByCategory,
SortByCondition,
//TODO: productid, currency, location, color, size, weight, imagefilesize, desc
};
Q_ENUMS(InventorySorting) // register the enum in meta object data
*/
class UserController : public QObject, public neroshop::Seller {
Q_OBJECT
public:
UserController(QObject *parent = nullptr);
~UserController();
Q_PROPERTY(neroshop::User* user READ getUser NOTIFY userChanged);
Q_PROPERTY(bool logged READ isUserLogged NOTIFY userLogged);
Q_PROPERTY(int productsCount READ getProductsCount NOTIFY productsCountChanged);
Q_PROPERTY(int cartQuantity READ getCartQuantity NOTIFY cartQuantityChanged);
Q_PROPERTY(QVariantList inventory READ getInventory NOTIFY productsCountChanged);
Q_PROPERTY(QVariantList inventoryInStock READ getInventoryInStock NOTIFY productsCountChanged);
//Q_PROPERTY(QVariantList inventoryDate READ getInventoryByDate NOTIFY productsCountChanged);
Q_PROPERTY(QVariantList inventory READ getInventory NOTIFY inventoryChanged);
//Q_PROPERTY(QVariantList cart READ getCart NOTIFY cartQuantityChanged);
Q_INVOKABLE QString listProduct(
@ -62,8 +64,8 @@ public:
const QString& condition,
const QString& location
);
Q_INVOKABLE void delistProduct(const QString& product_id);
Q_INVOKABLE void delistProducts(const QStringList& product_ids);
Q_INVOKABLE void delistProduct(const QString& listing_key);
Q_INVOKABLE void delistProducts(const QStringList& listing_keys);
Q_INVOKABLE void addToCart(const QString& product_id, int quantity);
//Q_INVOKABLE void removeFromCart(const QString& product_id, int quantity);
Q_INVOKABLE void createOrder(const QString& shipping_address);
@ -74,8 +76,12 @@ public:
Q_INVOKABLE void removeFromFavorites(const QString& listing_key);
Q_INVOKABLE bool hasFavorited(const QString& listing_key);
//Q_INVOKABLE void exportCartData();
//Q_INVOKABLE void exportFavoritesData();
//Q_INVOKABLE void setID(const QString& id);
//Q_INVOKABLE void setWallet(neroshop::WalletController * wallet); // get the actual wallet from the controller then set it as the wallet
Q_INVOKABLE void setStockQuantity(const QString& listing_id, int quantity);
Q_INVOKABLE void uploadAvatar(const QString& filename);
Q_INVOKABLE bool exportAvatar();
@ -86,9 +92,7 @@ public:
//Q_INVOKABLE <type> <function_name>() const;
Q_INVOKABLE int getCartQuantity() const;
Q_INVOKABLE QVariantList getInventory() const;
Q_INVOKABLE QVariantList getInventoryInStock() const;
Q_INVOKABLE QVariantList getInventoryByDate() const;
Q_INVOKABLE QVariantList getInventory(InventorySorting sorting = SortNone) const;
Q_INVOKABLE neroshop::User * getUser() const;
neroshop::Seller * getSeller() const;
@ -99,6 +103,7 @@ public:
signals:
void userChanged();
void userLogged();
void inventoryChanged(); // for inventory sorting
void productsCountChanged();
void cartQuantityChanged();
private:

Loading…
Cancel
Save