commit fb0fcd2669f7271d9b9960d3ba601654f80f16d1 Author: sha512sum Date: Tue Oct 22 18:23:46 2024 +0000 First version diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6a358db --- /dev/null +++ b/.clang-format @@ -0,0 +1,13 @@ +BasedOnStyle: Google +IndentWidth: 2 +ColumnLimit: 140 +AllowShortIfStatementsOnASingleLine: false +AllowShortLoopsOnASingleLine: false +BreakConstructorInitializers: AfterColon +AlwaysBreakAfterReturnType: None +SpaceBeforeParens: Never +AllowShortFunctionsOnASingleLine: None +AllowShortLambdasOnASingleLine: Empty +BinPackArguments: false +BinPackParameters: false +AlwaysBreakTemplateDeclarations: true diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..1d4ba70 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1 @@ +Checks: '-*,google-*,cppcoreguidelines-*,-cppcoreguidelines-c-copy-assignment-signature,-cppcoreguidelines-special-member-functions,-cppcoreguidelines-avoid-const-or-ref-data-members,modernize-*' diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..45de36b --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,35 @@ +project(yail LANGUAGES CXX) +cmake_minimum_required(VERSION 3.28) + + +set(UTEMPL_URL + "https://helicopter.myftp.org/git/sha512sum/utempl" + CACHE STRING "utempl repository URL") + +file( + DOWNLOAD + https://github.com/cpm-cmake/CPM.cmake/releases/download/v0.40.0/CPM.cmake + ${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake + EXPECTED_HASH + SHA256=7b354f3a5976c4626c876850c93944e52c83ec59a159ae5de5be7983f0e17a2a +) +include(${CMAKE_CURRENT_BINARY_DIR}/cmake/CPM.cmake) + +CPMAddPackage( + NAME utempl + URL "${UTEMPL_URL}/archive/refs/heads/main.zip" + EXCLUDE_FROM_ALL ON + OPTIONS "ENABLE_TESTS OFF" "ENABLE_EXAMPLES OFF" +) + +add_library(yail INTERFACE) + +target_link_libraries(yail INTERFACE utempl::utempl) + +target_include_directories(yail INTERFACE + $ + $) + +add_executable(main main.cpp) + +target_link_libraries(main yail) diff --git a/include/yail/core.hpp b/include/yail/core.hpp new file mode 100644 index 0000000..ceadd98 --- /dev/null +++ b/include/yail/core.hpp @@ -0,0 +1,163 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace yail { + +struct ParsedData { + int id; + std::string_view other; +}; + +namespace event { + +struct Welcome { + std::string_view message; + static constexpr auto TryParse(auto& self, std::string_view message) -> std::optional { + return self + .ParseServerMessage(message) // + .and_then([](ParsedData data) { // + return data.id == 1 ? std::optional{Welcome{data.other.substr(1)}} : std::nullopt; + }); + } +}; + +struct Ping { + std::string_view data; + static constexpr auto TryParse(auto& self, std::string_view message) -> std::optional { + return message.size() > 4 && message.substr(0, 4) == "PING" ? std::optional{Ping{.data = message.substr(sizeof("PING :") - 1)}} + : std::nullopt; + } +}; + +} // namespace event + +template +struct FuncTag {}; + +template +struct Caster { + template {}, utempl::kType>{}> + consteval operator T(); // NOLINT +}; + +template +constexpr Caster kCaster{}; + +template +using GetEventType = decltype(std::declval()(kCaster, Caster{}), Magic(utempl::loopholes::Getter{}>{}))::Type; + +template +struct Function { + F&& f; +}; + +template , auto KeysTuple = utempl::Tuple{}, typename StateTuple = std::tuple<>> +struct IrcClient { + boost::asio::ip::tcp::socket socket; + std::string nick; + std::string server; + Fs callbacks; + StateTuple state; + + template + auto AddState(Args&&... args) -> IrcClientstate), std::make_tuple(std::declval())))> { + return {.socket = std::move(this->socket), + .nick = std::move(this->nick), + .server = std::move(this->server), + .callbacks = std::move(this->callbacks), + .state = std::tuple_cat(std::move(this->state), std::make_tuple(T{std::forward(args)...}))}; + }; + + template + auto GetState() -> auto& { + constexpr auto strs = utempl::Map( + KeysTuple, + [](auto& str) { + return std::string_view{str}; + }, + utempl::kType>>); + constexpr auto i = std::ranges::find(strs, std::string_view{Key}) - strs.begin(); + return std::get(this->state); + }; + + auto ParseServerMessage(std::string_view input) -> std::optional { // NOLINTNEXTLINE + return ((input.size() > this->server.size() + 6 + this->nick.size() + 1) ? std::optional{input.substr(this->server.size() + 2)} + : std::nullopt) + .and_then([&](std::string_view str) { + return ToInt(str.substr(0, 3)).transform([&](int i) { + return ParsedData{.id = i, .other = str.substr(4 + 1 + this->nick.size())}; + }); + }); + } + + template + auto With(F&& func) -> IrcClientcallbacks), + std::make_tuple(Function>{std::forward(func)}))), + KeysTuple, + StateTuple> { + return {.socket = std::move(this->socket), + .nick = std::move(this->nick), + .server = std::move(this->server), + .callbacks = std::tuple_cat(std::move(this->callbacks), std::make_tuple(Function>{std::forward(func)})), + .state = std::move(this->state)}; + }; + + template + auto With(F&& f) -> decltype(With, F>(std::forward(f))) { + return With, F>(std::forward(f)); + }; + auto SendMessage(std::string_view to, std::string_view content) -> boost::asio::awaitable { + const auto request = std::format("PRIVMSG {} :{}", to, content); + + co_await boost::asio::async_write(this->socket, boost::asio::buffer(request), boost::asio::transfer_all(), boost::asio::use_awaitable); + }; + auto Pong(std::string_view buff) -> boost::asio::awaitable { + const auto pong = std::format("PONG {}", buff); + + co_await boost::asio::async_write(this->socket, boost::asio::buffer(pong), boost::asio::transfer_all(), boost::asio::use_awaitable); + }; + auto Connect(std::string_view realName) -> boost::asio::awaitable { + const auto request = std::format("NICK {0}\r\nUSER {0} 0 * :{1}\r\n", this->nick, realName); + co_await boost::asio::async_write(socket, boost::asio::buffer(request), boost::asio::transfer_all(), boost::asio::use_awaitable); + }; + auto Loop() -> boost::asio::awaitable { + std::string buff; + for(;;) { + auto messageStr = co_await yail::ReadLine(this->socket, buff); + + std::println("Readed message: {}", messageStr); + co_await std::apply( // NOLINTNEXTLINE + [&](Function&... fs) -> boost::asio::awaitable { + auto self = this; + auto& str = messageStr; + // clang-format off + ( // NOLINTNEXTLINE + co_await [](auto self, auto& messageStr, auto& fs) -> boost::asio::awaitable { + if(auto value = Ids::TryParse(*self, messageStr)) { + co_await fs.f(*self, *value); + } + }(self, str, fs), + ...); + // clang-format on + }, + this->callbacks); + }; + } + auto Join(std::string_view channel) -> boost::asio::awaitable { + const auto request = std::format("JOIN {}\r\n", channel); + co_await boost::asio::async_write(socket, boost::asio::buffer(request), boost::asio::transfer_all(), boost::asio::use_awaitable); + std::string response; + co_await boost::asio::async_read_until(socket, boost::asio::dynamic_buffer(response), "\r\n", boost::asio::use_awaitable); + + co_return response; + } +}; + +} // namespace yail diff --git a/include/yail/event/users.hpp b/include/yail/event/users.hpp new file mode 100644 index 0000000..6f23597 --- /dev/null +++ b/include/yail/event/users.hpp @@ -0,0 +1,27 @@ +#pragma once +#include +#include + +namespace yail::event { + +struct ChannelUsers { + static constexpr auto kId = 353; + std::string_view channel; + decltype(ToUsers({})) users; + static constexpr auto TryParse(auto& self, std::string_view message) -> std::optional { + return self + .ParseServerMessage(message) // + .and_then([&](ParsedData data) { // + return (data.id == kId ? std::optional{data.other.substr(2)} : std::nullopt) + .and_then([&](std::string_view data) -> std::optional { + auto n = data.find(" "); + if(data.size() < n + self.nick.size() + 3) { + return std::nullopt; + } + return ChannelUsers{.channel = data.substr(0, n), .users = ToUsers(data.substr(n + self.nick.size() + 3))}; + }); + }); + } +}; + +} // namespace yail::event diff --git a/include/yail/socks5.hpp b/include/yail/socks5.hpp new file mode 100644 index 0000000..adba24d --- /dev/null +++ b/include/yail/socks5.hpp @@ -0,0 +1,54 @@ +#pragma once +#include +#include +#include + +namespace yail { + +constexpr std::array kSocks5RequestStart = {std::byte{0x05}, std::byte{0x01}, std::byte{0x00}, std::byte{0x03}}; + +constexpr std::size_t kSocks5RequestMaxSize = 257; + +constexpr std::size_t kSocks5ReplyTypeSize = 10; + +constexpr std::array kHandshakeRequest{std::byte{0x05}, std::byte{0x01}, std::byte{0x00}}; + +inline auto Socks5ProxyConnect(std::string proxy, std::uint16_t proxyPort, std::string_view address, std::uint16_t port) + -> boost::asio::awaitable { + std::println("Connecting to proxy {}", proxyPort); + auto executor = co_await boost::asio::this_coro::executor; + boost::asio::ip::tcp::resolver resolver{executor}; + auto resolved = co_await resolver.async_resolve({std::move(proxy), std::to_string(proxyPort)}, boost::asio::use_awaitable); + boost::asio::ip::tcp::socket socket{executor}; + co_await socket.async_connect(*resolved, boost::asio::use_awaitable); + + std::array handshakeResponse; // NOLINT + + co_await boost::asio::async_write( + socket, boost::asio::buffer(kHandshakeRequest), boost::asio::transfer_all(), boost::asio::use_awaitable); + co_await boost::asio::async_read(socket, boost::asio::buffer(handshakeResponse), boost::asio::transfer_all(), boost::asio::use_awaitable); + if(handshakeResponse[0] != std::byte{0x05} || handshakeResponse[1] != std::byte{0x00}) { // NOLINT + throw std::exception{}; + }; + const auto [connectRequest, size] = [&] { + const auto size = static_cast(address.size()); + const auto htonsPort = std::bit_cast>(htons(port)); + auto range = std::array{std::span{kSocks5RequestStart.begin(), kSocks5RequestStart.size()}, + std::span{&size, 1}, + std::span{StartLifetimeAsArray(address.data(), address.size()), address.size()}, + std::span{htonsPort.data(), 2}} | + std::views::join; + std::array response; // NOLINT + return std::pair{std::move(response), std::ranges::copy(range, response.begin()).out - response.begin()}; + }(); + co_await boost::asio::async_write( + socket, boost::asio::buffer(connectRequest.begin(), size), boost::asio::transfer_all(), boost::asio::use_awaitable); + std::array connectReplyType; // NOLINT + co_await boost::asio::async_read(socket, boost::asio::buffer(connectReplyType), boost::asio::transfer_all(), boost::asio::use_awaitable); + if(connectReplyType[1] != std::byte{0x00}) { + throw std::exception{}; + }; + co_return socket; +}; + +} // namespace yail diff --git a/include/yail/user.hpp b/include/yail/user.hpp new file mode 100644 index 0000000..30c63d3 --- /dev/null +++ b/include/yail/user.hpp @@ -0,0 +1,114 @@ +#pragma once +#include +#include +#include +#include +#include + +namespace yail { + +namespace prefix { + +struct None {}; + +struct Operator {}; + +struct Priveleged {}; + +struct Admin {}; + +struct HalfOp {}; + +struct Owner {}; + +} // namespace prefix + +using Prefix = std::variant; + +struct UserView { + Prefix prefix; + std::string_view nick; + static constexpr auto Parse(std::string_view str) -> UserView { + if(str.at(0) == '+') { + return {.prefix = prefix::Priveleged{}, .nick = str.substr(1)}; + } + if(str.at(0) == '@') { + return {.prefix = prefix::Operator{}, .nick = str.substr(1)}; + } + if(str.at(0) == '~') { + return {.prefix = prefix::Owner{}, .nick = str.substr(1)}; + } + if(str.at(0) == '%') { + return {.prefix = prefix::HalfOp{}, .nick = str.substr(1)}; + } + if(str.at(0) == '&') { + return {.prefix = prefix::Admin{}, .nick = str.substr(1)}; + } + return {.prefix = prefix::None{}, .nick = str}; + }; +}; + +struct User { + Prefix prefix; + std::string nick; + constexpr explicit User(UserView view) : prefix(view.prefix), nick(static_cast(view.nick)) {}; + // NOLINTNEXTLINE + constexpr operator UserView() const { + return {.prefix = this->prefix, .nick = this->nick}; + } + constexpr auto operator==(const UserView& other) const -> bool { + return this->prefix.index() == other.prefix.index() && this->nick == other.nick; + } + struct Hash { + static constexpr auto operator()(const UserView& user) -> std::size_t { + std::size_t hash_prefix = std::hash{}(user.prefix.index()); + std::size_t hash_nick = std::hash{}(user.nick); + return hash_prefix ^ (hash_nick << 1); + }; + }; +}; + +constexpr auto ToUsers(std::string_view message) { + return message | std::views::split(' ') | std::views::transform([](auto range) { + return std::string_view{range}; + }) | + std::views::filter([](std::string_view user) { + return !user.empty(); + }) | + std::views::transform([](std::string_view user) { + return UserView::Parse(user); + }); +} +} // namespace yail + +template <> +struct std::formatter : std::formatter { + template + constexpr auto format(yail::UserView s, FmtContext& ctx) const -> FmtContext::iterator { + auto it = ctx.out(); + std::visit(utempl::Overloaded( + [&](yail::prefix::Operator) { + *it = '@'; + ++it; + }, + [&](yail::prefix::Priveleged) { + *it = '+'; + ++it; + }, + [&](yail::prefix::Owner) { + *it = '~'; + ++it; + }, + [&](yail::prefix::HalfOp) { + *it = '%'; + ++it; + }, + [&](yail::prefix::Admin) { + *it = '&'; + ++it; + }, + [&](yail::prefix::None) {}), + s.prefix); + return std::ranges::copy(s.nick, it).out; + }; +}; diff --git a/include/yail/utils.hpp b/include/yail/utils.hpp new file mode 100644 index 0000000..691259e --- /dev/null +++ b/include/yail/utils.hpp @@ -0,0 +1,84 @@ +#pragma once +#include +#include + +namespace yail { + +#if __has_cpp_attribute(__cpp_lib_start_lifetime_as) + +template +inline auto StartLifetimeAsArray(void* ptr, std::size_t n) -> T* { + return std::start_lifetime_as_array(ptr, n); +} + +template +inline auto StartLifetimeAs(void* ptr) -> T* { + return std::start_lifetime_as(ptr); +} + +template +inline auto StartLifetimeAsArray(const void* ptr, std::size_t n) -> const T* { + return std::start_lifetime_as_array(ptr, n); +} + +template +inline auto StartLifetimeAs(const void* ptr) -> const T* { + return std::start_lifetime_as(ptr); +} + +#else +template +inline auto StartLifetimeAsArray(void* ptr, std::size_t n) -> T* { + return std::launder(reinterpret_cast(new(ptr) std::byte[n * sizeof(T)])); // NOLINT +} + +template +inline auto StartLifetimeAs(void* ptr) -> T* { + return StartLifetimeAsArray(ptr, 1); +} + +template +inline auto StartLifetimeAsArray(const void* ptr, std::size_t n) -> const T* { + return std::launder(reinterpret_cast(new(const_cast(ptr)) std::byte[n * sizeof(T)])); // NOLINT +} + +template +inline auto StartLifetimeAs(const void* ptr) -> const T* { + return StartLifetimeAsArray(ptr, 1); +} + +#endif + +template +inline auto ToInt(std::string_view input) -> std::optional { + T out{}; + const std::from_chars_result result = std::from_chars(input.data(), input.data() + input.size(), out); + return result.ec == std::errc::invalid_argument || result.ec == std::errc::result_out_of_range ? std::nullopt : std::optional{out}; +} + +auto ReadLine(auto& socket, std::string& buff) -> boost::asio::awaitable { + auto it = buff.find("\r\n"); + return it == std::string::npos ? [](auto& socket, auto& buff) -> boost::asio::awaitable { // NOLINT + co_await boost::asio::async_read( + socket, boost::asio::dynamic_buffer(buff), boost::asio::transfer_at_least(1), boost::asio::use_awaitable); + co_return co_await ReadLine(socket, buff); + }(socket, buff) + : [&](auto it, auto& buff) -> boost::asio::awaitable { // NOLINT + std::string response; + response.reserve(sizeof(std::string) + 1); + response = buff.substr(0, it); + buff = buff.substr(it + 2); + co_return response; + }(it, buff); +} + +template +struct BufferedSocket { + Socket socket; + std::string buff; + auto ReadLine() -> boost::asio::awaitable { + return ::yail::ReadLine(this->socket, this->buff); + }; +}; + +} // namespace yail diff --git a/include/yail/yail.hpp b/include/yail/yail.hpp new file mode 100644 index 0000000..fa15209 --- /dev/null +++ b/include/yail/yail.hpp @@ -0,0 +1,3 @@ +#pragma once +#include +#include diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..d9f4d00 --- /dev/null +++ b/main.cpp @@ -0,0 +1,46 @@ +#include +#include +#include + +auto Test() -> boost::asio::awaitable { + try { + auto executor = co_await boost::asio::this_coro::executor; // NOLINTNEXTLINE + // auto sock = co_await yail::Socks5ProxyConnect("127.0.0.1", 4447, "irc.ilita.i2p", 6667); + // NOLINTNEXTLINE + auto sock = co_await yail::Socks5ProxyConnect("127.0.0.1", 4447, "irc.ilita.i2p", 6667); + auto client = + yail::IrcClient, utempl::Tuple{}, std::tuple<>>{// clangd bug :( + .socket = std::move(sock), + .nick = "sha512sum_bot", + .server = "irc.ilita.i2p"} + .AddState, "ruUsers">() + // NOLINTNEXTLINE + .With([](auto& self, yail::event::Ping ping) -> boost::asio::awaitable { + co_await self.Pong(ping.data); + }) + // NOLINTNEXTLINE + .With([](auto& self, yail::event::Welcome message) -> boost::asio::awaitable { + std::println("Welcome: {}", message.message); + co_await self.Join("#ru"); + co_await self.SendMessage("#ru", "Hello from yet another irc library"); + }) + // NOLINTNEXTLINE + .With([](auto& self, yail::event::ChannelUsers event) -> boost::asio::awaitable { + std::unordered_set& ruUsers = self.template GetState<"ruUsers">(); + for(yail::UserView view : event.users) { + ruUsers.emplace(view); + } + co_return; + }); + co_await client.Connect("sha512sum_bot"); + co_await client.Loop(); + } catch(const std::exception& err) { + std::println("Err: {}", err.what()); + } +}; + +auto main() -> int { + boost::asio::io_context context; + boost::asio::co_spawn(context, Test(), boost::asio::detached); + context.run(); +};