First version

This commit is contained in:
sha512sum 2024-10-22 18:23:46 +00:00
commit fb0fcd2669
10 changed files with 540 additions and 0 deletions

13
.clang-format Normal file
View file

@ -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

1
.clang-tidy Normal file
View file

@ -0,0 +1 @@
Checks: '-*,google-*,cppcoreguidelines-*,-cppcoreguidelines-c-copy-assignment-signature,-cppcoreguidelines-special-member-functions,-cppcoreguidelines-avoid-const-or-ref-data-members,modernize-*'

35
CMakeLists.txt Normal file
View file

@ -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
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>)
add_executable(main main.cpp)
target_link_libraries(main yail)

163
include/yail/core.hpp Normal file
View file

@ -0,0 +1,163 @@
#pragma once
#include <format>
#include <utempl/constexpr_string.hpp>
#include <utempl/loopholes/core.hpp>
#include <utempl/overloaded.hpp>
#include <utempl/utils.hpp>
#include <yail/utils.hpp>
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<Welcome> {
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<Ping> {
return message.size() > 4 && message.substr(0, 4) == "PING" ? std::optional{Ping{.data = message.substr(sizeof("PING :") - 1)}}
: std::nullopt;
}
};
} // namespace event
template <typename F>
struct FuncTag {};
template <typename F>
struct Caster {
template <typename T, auto = utempl::loopholes::Injector<FuncTag<F>{}, utempl::kType<T>>{}>
consteval operator T(); // NOLINT
};
template <typename F>
constexpr Caster<F> kCaster{};
template <typename F>
using GetEventType = decltype(std::declval<F>()(kCaster<F>, Caster<F>{}), Magic(utempl::loopholes::Getter<FuncTag<F>{}>{}))::Type;
template <typename Id, typename F>
struct Function {
F&& f;
};
template <typename Fs = std::tuple<>, 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 <typename T, utempl::ConstexprString Key, typename... Args>
auto AddState(Args&&... args) -> IrcClient<Fs,
KeysTuple + utempl::Tuple{Key},
decltype(std::tuple_cat(std::move(this->state), std::make_tuple(std::declval<T>())))> {
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>(args)...}))};
};
template <utempl::ConstexprString Key>
auto GetState() -> auto& {
constexpr auto strs = utempl::Map(
KeysTuple,
[](auto& str) {
return std::string_view{str};
},
utempl::kType<std::array<std::string_view, utempl::kTupleSize<decltype(KeysTuple)>>>);
constexpr auto i = std::ranges::find(strs, std::string_view{Key}) - strs.begin();
return std::get<i>(this->state);
};
auto ParseServerMessage(std::string_view input) -> std::optional<ParsedData> { // 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 <typename Id, typename F>
auto With(F&& func) -> IrcClient<decltype(std::tuple_cat(std::move(this->callbacks),
std::make_tuple(Function<Id, std::decay_t<F>>{std::forward<F>(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<Id, std::decay_t<F>>{std::forward<F>(func)})),
.state = std::move(this->state)};
};
template <typename F>
auto With(F&& f) -> decltype(With<GetEventType<F>, F>(std::forward<F>(f))) {
return With<GetEventType<F>, F>(std::forward<F>(f));
};
auto SendMessage(std::string_view to, std::string_view content) -> boost::asio::awaitable<void> {
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<void> {
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<void> {
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<void> {
std::string buff;
for(;;) {
auto messageStr = co_await yail::ReadLine(this->socket, buff);
std::println("Readed message: {}", messageStr);
co_await std::apply( // NOLINTNEXTLINE
[&]<typename... Ids, typename... FFs>(Function<Ids, FFs>&... fs) -> boost::asio::awaitable<void> {
auto self = this;
auto& str = messageStr;
// clang-format off
( // NOLINTNEXTLINE
co_await [](auto self, auto& messageStr, auto& fs) -> boost::asio::awaitable<void> {
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<std::string> {
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

View file

@ -0,0 +1,27 @@
#pragma once
#include <yail/core.hpp>
#include <yail/user.hpp>
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<ChannelUsers> {
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<ChannelUsers> {
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

54
include/yail/socks5.hpp Normal file
View file

@ -0,0 +1,54 @@
#pragma once
#include <ranges>
#include <span>
#include <yail/utils.hpp>
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<boost::asio::ip::tcp::socket> {
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<std::byte, 2> 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<std::byte>(address.size());
const auto htonsPort = std::bit_cast<std::array<std::byte, 2>>(htons(port));
auto range = std::array{std::span{kSocks5RequestStart.begin(), kSocks5RequestStart.size()},
std::span{&size, 1},
std::span{StartLifetimeAsArray<const std::byte>(address.data(), address.size()), address.size()},
std::span{htonsPort.data(), 2}} |
std::views::join;
std::array<std::byte, kSocks5RequestMaxSize> 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<std::byte, kSocks5ReplyTypeSize> 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

114
include/yail/user.hpp Normal file
View file

@ -0,0 +1,114 @@
#pragma once
#include <format>
#include <ranges>
#include <string>
#include <utempl/overloaded.hpp>
#include <variant>
namespace yail {
namespace prefix {
struct None {};
struct Operator {};
struct Priveleged {};
struct Admin {};
struct HalfOp {};
struct Owner {};
} // namespace prefix
using Prefix = std::variant<prefix::None, prefix::Operator, prefix::Priveleged, prefix::Admin, prefix::HalfOp, prefix::Owner>;
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<std::string>(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<std::size_t>{}(user.prefix.index());
std::size_t hash_nick = std::hash<std::string_view>{}(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<yail::UserView> : std::formatter<std::string_view> {
template <class FmtContext>
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;
};
};

84
include/yail/utils.hpp Normal file
View file

@ -0,0 +1,84 @@
#pragma once
#include <boost/asio.hpp>
#include <charconv>
namespace yail {
#if __has_cpp_attribute(__cpp_lib_start_lifetime_as)
template <typename T>
inline auto StartLifetimeAsArray(void* ptr, std::size_t n) -> T* {
return std::start_lifetime_as_array<T>(ptr, n);
}
template <typename T>
inline auto StartLifetimeAs(void* ptr) -> T* {
return std::start_lifetime_as<T>(ptr);
}
template <typename T>
inline auto StartLifetimeAsArray(const void* ptr, std::size_t n) -> const T* {
return std::start_lifetime_as_array<T>(ptr, n);
}
template <typename T>
inline auto StartLifetimeAs(const void* ptr) -> const T* {
return std::start_lifetime_as<T>(ptr);
}
#else
template <typename T>
inline auto StartLifetimeAsArray(void* ptr, std::size_t n) -> T* {
return std::launder(reinterpret_cast<T*>(new(ptr) std::byte[n * sizeof(T)])); // NOLINT
}
template <typename T>
inline auto StartLifetimeAs(void* ptr) -> T* {
return StartLifetimeAsArray<T>(ptr, 1);
}
template <typename T>
inline auto StartLifetimeAsArray(const void* ptr, std::size_t n) -> const T* {
return std::launder(reinterpret_cast<const T*>(new(const_cast<void*>(ptr)) std::byte[n * sizeof(T)])); // NOLINT
}
template <typename T>
inline auto StartLifetimeAs(const void* ptr) -> const T* {
return StartLifetimeAsArray<T>(ptr, 1);
}
#endif
template <typename T = int>
inline auto ToInt(std::string_view input) -> std::optional<T> {
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<std::string> {
auto it = buff.find("\r\n");
return it == std::string::npos ? [](auto& socket, auto& buff) -> boost::asio::awaitable<std::string> { // 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<std::string> { // 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 <typename Socket>
struct BufferedSocket {
Socket socket;
std::string buff;
auto ReadLine() -> boost::asio::awaitable<std::string> {
return ::yail::ReadLine(this->socket, this->buff);
};
};
} // namespace yail

3
include/yail/yail.hpp Normal file
View file

@ -0,0 +1,3 @@
#pragma once
#include <yail/event/users.hpp>
#include <yail/socks5.hpp>

46
main.cpp Normal file
View file

@ -0,0 +1,46 @@
#include <unordered_set>
#include <utempl/tuple.hpp>
#include <yail/yail.hpp>
auto Test() -> boost::asio::awaitable<void> {
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<std::tuple<>, utempl::Tuple{}, std::tuple<>>{// clangd bug :(
.socket = std::move(sock),
.nick = "sha512sum_bot",
.server = "irc.ilita.i2p"}
.AddState<std::unordered_set<yail::User, yail::User::Hash>, "ruUsers">()
// NOLINTNEXTLINE
.With([](auto& self, yail::event::Ping ping) -> boost::asio::awaitable<void> {
co_await self.Pong(ping.data);
})
// NOLINTNEXTLINE
.With([](auto& self, yail::event::Welcome message) -> boost::asio::awaitable<void> {
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<void> {
std::unordered_set<yail::User, yail::User::Hash>& 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();
};