| /*
|
| * message.cpp -- respresent an IRC message.
|
| *
|
| * Written on Sunday, 05 May 2024 by oldfashionedcow
|
| *
|
| * Copyright 2024 Cow
|
| *
|
| * This file is part of ircplusplus.
|
| *
|
| * ircplusplus is free software: you can redistribute it and/or modify
|
| * it under the terms of the GNU Affero General Public License as published by
|
| * the Free Software Foundation, either version 3 of the License, or
|
| * (at your option) any later version.
|
| *
|
| * ircplusplus is distributed in the hope that it will be useful,
|
| * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| * GNU Affero General Public License for more details.
|
| *
|
| * You should have received a copy of the GNU Affero General Public License
|
| * along with ircplusplus. If not, see <http://www.gnu.org/licenses/>.
|
| *
|
| * SPDX-License-Identifier: AGPL-3.0-or-later
|
| */
|
|
|
| #include "ircplusplus/parser/message.hpp"
|
|
|
| #include "ircplusplus/parser/error.hpp"
|
|
|
| #include <ranges>
|
| #include <sstream>
|
|
|
| namespace ircplusplus::parser {
|
| using std::string;
|
|
|
| using enum ParserError;
|
|
|
| [[nodiscard]] std::string Message::_unescape_tag(std::string value) {
|
| std::string result;
|
|
|
| for (size_t i = 0; i < value.size(); ++i) {
|
| if (value[i] == '\\') {
|
| if (i + 1 < value.size()) {
|
| ++i;
|
| switch (value[i]) {
|
| case '\\':
|
| result += '\\';
|
| break;
|
| case ':':
|
| result += ';';
|
| break;
|
| case 's':
|
| result += ' ';
|
| break;
|
| case 'r':
|
| result += '\r';
|
| break;
|
| case 'n':
|
| result += '\n';
|
| break;
|
| default:
|
| result += '\\';
|
| result += value[i];
|
| break;
|
| }
|
| } else {
|
| result += '\\';
|
| }
|
| } else {
|
| result += value[i];
|
| }
|
| }
|
|
|
| return result;
|
| }
|
|
|
| [[nodiscard]] std::expected<std::pair<std::unordered_map<std::string, std::string>, std::string>, ParserError>
|
| Message::_parse_tags(const std::string &response) {
|
| auto line = response;
|
| std::unordered_map<std::string, std::string> tags = {};
|
|
|
| if (!line.starts_with('@')) {
|
| return std::make_pair(tags, line);
|
| }
|
|
|
| const auto space_pos = line.find(' ');
|
|
|
| if (space_pos == std::string_view::npos) {
|
| return std::unexpected(EmptyCommand);
|
| }
|
|
|
| const auto tags_split = std::string{line.substr(0, space_pos)};
|
| line = std::string{line.substr(space_pos + 1)};
|
|
|
| auto parts = std::ranges::transform_view(std::views::split(tags_split.substr(1), ';'), [](auto &&part) {
|
| return std::string(part.begin(), part.end());
|
| });
|
|
|
| for (const auto &part : parts) {
|
| if (part.empty()) {
|
| return std::unexpected(TrailingSemicolon);
|
| }
|
|
|
| auto kv_result = [](auto part) -> std::expected<std::pair<std::string, std::string>, ParserError> {
|
| auto equal_pos = part.find('=');
|
| if (equal_pos == std::string::npos) {
|
| return std::make_pair(part, "");
|
| }
|
|
|
| auto key = part.substr(0, equal_pos);
|
| auto value = _unescape_tag(part.substr(equal_pos + 1));
|
|
|
| if (key.empty()) {
|
| return std::unexpected(MissingKey);
|
| }
|
|
|
| return std::make_pair(key, value);
|
| }(part);
|
|
|
| if (!kv_result.has_value()) {
|
| return std::unexpected(kv_result.error());
|
| }
|
|
|
| auto [key, value] = kv_result.value();
|
| if (tags.contains(key)) {
|
| tags[key] = value;
|
| } else {
|
| tags.insert({key, value});
|
| }
|
| }
|
|
|
| return std::make_pair(tags, line);
|
| }
|
|
|
| [[nodiscard]] std::pair<std::string, std::optional<std::string>>
|
| Message::_split_line(const std::string &response) {
|
| auto pos = response.find(" :");
|
| if (pos == std::string::npos) {
|
| return std::make_pair(std::string(response), std::optional<std::string>());
|
| }
|
|
|
| auto line = std::string(response.substr(0, pos));
|
| if (pos + 2 >= response.size()) {
|
| return std::make_pair(line, std::optional<std::string>());
|
| }
|
|
|
| auto trailing = std::string(response.substr(pos + 2));
|
| return std::make_pair(line, std::optional<std::string>(std::move(trailing)));
|
| }
|
|
|
| [[nodiscard]] std::vector<std::string> Message::_split_params(const std::string &line, char delim) {
|
| std::vector<std::string> params;
|
| std::istringstream iss(line);
|
| std::string token;
|
|
|
| while (std::getline(iss, token, delim)) {
|
| if (!token.empty()) {
|
| params.emplace_back(token);
|
| }
|
| }
|
|
|
| return params;
|
| }
|
|
|
| [[nodiscard]] std::expected<std::pair<std::optional<std::string>, std::string>, ParserError>
|
| Message::_parse_command(std::vector<std::string> ¶ms, const std::optional<std::string> &trailing) {
|
| std::optional<std::string> source;
|
|
|
| if (params.at(0).starts_with(":")) {
|
| source = params.at(0).substr(1);
|
| params.erase(params.begin());
|
| }
|
|
|
| if (params.empty()) {
|
| return std::unexpected(EmptyCommand); // Error if command-less line
|
| }
|
|
|
| auto command = params.at(0);
|
| params.erase(params.begin());
|
|
|
| if (trailing.has_value()) {
|
| params.emplace_back(*trailing);
|
| }
|
|
|
| return std::make_tuple(std::move(source), std::move(command));
|
| }
|
|
|
| [[nodiscard]] Source Message::_parse_source(const std::string &source) {
|
| std::string nickname;
|
| std::string username;
|
| string hostmask;
|
|
|
| std::vector<std::string> tokens;
|
| size_t pos = source.find('@');
|
| if (pos != std::string::npos) {
|
| tokens.emplace_back(source.substr(0, pos));
|
| tokens.emplace_back(source.substr(pos + 1));
|
| } else {
|
| tokens.emplace_back(source);
|
| }
|
|
|
| if (tokens.size() == 2) {
|
| username = tokens.at(0);
|
| hostmask = tokens.at(1);
|
| } else {
|
| username = source;
|
| }
|
|
|
| pos = username.find('!');
|
| if (pos != std::string::npos) {
|
| nickname = username.substr(0, pos);
|
| username = username.substr(pos + 1);
|
| } else {
|
| nickname = username;
|
| username.clear();
|
| }
|
|
|
| return {nickname, username, hostmask};
|
| }
|
|
|
| [[nodiscard]] std::expected<Message, ParserError> Message::parse(const std::string &response) {
|
| auto tags_result = _parse_tags(response);
|
|
|
| if (!tags_result.has_value()) {
|
| return std::unexpected(tags_result.error());
|
| }
|
|
|
| auto [tags, response_split] = tags_result.value();
|
| auto [line, trailing] = _split_line(response_split);
|
| auto params = _split_params(line, ' ');
|
|
|
| if (params.empty()) {
|
| return std::unexpected(EmptyCommand); // Error if command-less line
|
| }
|
|
|
| auto command_result = _parse_command(params, trailing);
|
| if (!command_result.has_value()) {
|
| return std::unexpected(command_result.error());
|
| }
|
| auto [source_str, command] = command_result.value();
|
|
|
| std::optional<Source> source = std::nullopt;
|
| if (source_str.has_value()) {
|
| source = _parse_source(source_str.value());
|
| }
|
|
|
| return Message(tags, source, command, params);
|
| }
|
|
|
| } // namespace ircplusplus::parser
|