From 00dc057ea5d9244f1df9457a316fd193c54dbfb0 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Tue, 21 Oct 2025 23:14:25 +0200 Subject: main: Split out main api/server part from main Add fake-api, sharing api/server implementation with main but fakes bluetooth events, to simplify working on the UI. --- src/server.cc | 397 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 src/server.cc (limited to 'src/server.cc') diff --git a/src/server.cc b/src/server.cc new file mode 100644 index 0000000..9b556b0 --- /dev/null +++ b/src/server.cc @@ -0,0 +1,397 @@ +#include "server.hh" + +#include "bt.hh" +#include "cfg.hh" +#include "config.h" +#include "http.hh" +#include "json.hh" +#include "logger.hh" +#include "looper.hh" +#include "signals.hh" +#include "uri.hh" +#include "websocket.hh" + +#include +#include +#include +#include +#include +#include +#include + +namespace server { + +namespace { + +const std::string_view kSignalUpdateAdapter("controller/update"); +const std::string kSignalUpdateDevicePrefix("device/update/"); + +std::optional match_bool(std::string_view str) { + if (str == "true") + return true; + if (str == "false") + return false; + return std::nullopt; +} + +class HttpServerDelegate : public http::Server::Delegate { + public: + HttpServerDelegate(Api& api, Signaler& signaler) + : api_(api), + signaler_(signaler), + json_writer_(json::writer(json_tmp_)), + json_mimetype_(http::MimeType::create("application", "json")) {} + + std::unique_ptr handle( + http::Request const& request) override { + if (request.path().starts_with("/api/v1/")) { + auto path = request.path().substr(8); + if (path == "status") { + if (request.method() != "GET") { + return http::Response::status(http::StatusCode::kMethodNotAllowed); + } + + return status_ok(); + } + + if (path == "controller") { + if (request.method() != "GET") { + return http::Response::status(http::StatusCode::kMethodNotAllowed); + } + + auto const* adapter = api_.adapter(); + json_writer_->clear(); + write_adapter(adapter); + + return http::Response::content(json_tmp_, *json_mimetype_); + } + + if (path == "controller/discoverable") { + if (request.method() != "POST") { + return http::Response::status(http::StatusCode::kMethodNotAllowed); + } + std::optional value = match_bool(request.body()); + auto* adapter = api_.adapter(); + if (adapter && value.has_value()) { + adapter->set_discoverable(value.value()); + return status_ok(); + } + return status_error("Bad state"); + } + + if (path.starts_with("device/")) { + if (request.method() != "GET") { + return http::Response::status(http::StatusCode::kMethodNotAllowed); + } + + std::string tmp; + auto address = uri::decode(path.substr(7), tmp); + auto const* adapter = api_.adapter(); + if (adapter) { + for (auto* device : adapter->devices()) { + if (device->address() == address) { + json_writer_->clear(); + write_device(*device); + return http::Response::content(json_tmp_, *json_mimetype_); + } + } + } + return http::Response::status(http::StatusCode::kNotFound); + } + + if (path == "events") { + if (request.method() != "GET") { + return http::Response::status(http::StatusCode::kMethodNotAllowed); + } + + auto resp = signaler_.handle(request); + if (resp) + return resp; + return http::Response::status(http::StatusCode::kBadRequest); + } + } + + return http::Response::status(http::StatusCode::kNotFound); + } + + private: + std::unique_ptr status_ok() { + json_writer_->clear(); + json_writer_->start_object(); + json_writer_->key("status"); + json_writer_->value("OK"); + json_writer_->end_object(); + return http::Response::content(json_tmp_, *json_mimetype_); + } + + std::unique_ptr status_error(std::string_view message) { + json_writer_->clear(); + json_writer_->start_object(); + json_writer_->key("status"); + json_writer_->value("error"); + json_writer_->key("message"); + json_writer_->value(message); + json_writer_->end_object(); + return http::Response::content(json_tmp_, *json_mimetype_); + } + + void write_adapter(bt::Adapter const* adapter) { + json_writer_->start_object(); + json_writer_->key("name"); + json_writer_->value(adapter ? adapter->name() : "unknown"); + json_writer_->key("pairable"); + json_writer_->value(adapter ? adapter->pairable() : false); + json_writer_->key("discoverable"); + json_writer_->value(adapter ? adapter->discoverable() : false); + json_writer_->key("discoverable_timeout_seconds"); + json_writer_->value(adapter ? adapter->discoverable_timeout_seconds() : 0); + json_writer_->key("pairing"); + json_writer_->value(adapter ? adapter->pairing() : false); + + json_writer_->key("devices"); + json_writer_->start_array(); + + if (adapter) { + for (auto* device : adapter->devices()) { + write_device(*device); + } + } + + json_writer_->end_array(); + json_writer_->end_object(); + } + + void write_device(bt::Device const& device) { + json_writer_->start_object(); + json_writer_->key("name"); + json_writer_->value(device.name()); + json_writer_->key("address"); + json_writer_->value(device.address()); + json_writer_->key("paired"); + json_writer_->value(device.paired()); + json_writer_->key("connected"); + json_writer_->value(device.connected()); + + json_writer_->key("playing"); + if (auto* player = device.player()) { + json_writer_->start_object(); + json_writer_->key("status"); + switch (player->status()) { + case bt::Player::Status::kPlaying: + case bt::Player::Status::kForwardSeek: + case bt::Player::Status::kReverseSeek: + json_writer_->value("playing"); + break; + case bt::Player::Status::kStopped: + json_writer_->value("stopped"); + break; + case bt::Player::Status::kPaused: + json_writer_->value("paused"); + break; + case bt::Player::Status::kError: + json_writer_->value("error"); + break; + } + json_writer_->key("title"); + json_writer_->value(player->track_title()); + json_writer_->key("album"); + json_writer_->value(player->track_album()); + json_writer_->key("artist"); + json_writer_->value(player->track_artist()); + json_writer_->end_object(); + } else { + json_writer_->value(nullptr); + } + + json_writer_->end_object(); + } + + Api& api_; + Signaler& signaler_; + std::unique_ptr json_writer_; + std::string json_tmp_; + std::unique_ptr json_mimetype_; +}; + +class BluetoothManagerDelegate : public Api { + public: + BluetoothManagerDelegate(logger::Logger& logger, Signaler& signaler) + : logger_(logger), signaler_(signaler) {} + + [[nodiscard]] + bt::Adapter* adapter() const override { + return adapter_; + } + + void new_adapter(bt::Adapter* adapter) override { + adapter_ = adapter; + + signaler_.send(kSignalUpdateAdapter); + + if (adapter) { + logger_.info(std::format("New adapter: {} [{}]", adapter->name(), + adapter->address())); + + // Assuming pairable doesn't have a timeout - make it so. + if (adapter_->pairable_timeout_seconds() > 0) { + adapter_->set_pairable_timeout_seconds(0); + } + + // Assuming discoverable has one + if (adapter_->discoverable_timeout_seconds() == 0) { + adapter_->set_discoverable_timeout_seconds(180); + } + } else { + logger_.info("No adapter"); + } + } + + void updated_adapter(bt::Adapter& adapter) override { + if (adapter_ == &adapter) + signaler_.send(kSignalUpdateAdapter); + } + + void added_device(bt::Device& device) override { + logger_.info( + std::format("New device: {} [{}]", device.name(), device.address())); + + if (adapter_) + signaler_.send(kSignalUpdateAdapter); + } + + void removed_device(std::string const& address) override { + logger_.info(std::format("Remove device: [{}]", address)); + + if (adapter_) + signaler_.send(kSignalUpdateAdapter); + } + + void added_player(bt::Device& device, bt::Player& /* player */) override { + logger_.info(std::format("New player for {}", device.name())); + + if (adapter_) + signaler_.send(kSignalUpdateDevicePrefix + device.address()); + } + + void removed_player(bt::Device& device) override { + logger_.info(std::format("Remove player for {}", device.name())); + + if (adapter_) + signaler_.send(kSignalUpdateDevicePrefix + device.address()); + } + + void updated_player(bt::Device& device, bt::Player& /* player */) override { + if (adapter_) + signaler_.send(kSignalUpdateDevicePrefix + device.address()); + } + + void agent_request_pincode( + bt::Device& device, + std::function)> callback) override { + logger_.dbg(std::format("Device request pincode: {}", device.name())); + callback(std::nullopt); + } + + void agent_display_pincode(bt::Device& device, + std::string const& pincode) override { + logger_.dbg(std::format("Device pincode: {} {}", device.name(), pincode)); + } + + void agent_request_passkey( + bt::Device& device, + std::function)> callback) override { + logger_.dbg(std::format("Device request passkey: {}", device.name())); + callback(std::nullopt); + } + + void agent_display_passkey(bt::Device& device, uint32_t passkey, + uint16_t /* entered */) override { + logger_.dbg(std::format("Device passkey: {} {}", device.name(), passkey)); + } + + void agent_request_confirmation(bt::Device& device, uint32_t passkey, + std::function callback) override { + logger_.dbg(std::format("Device request confirmation: {} {}", device.name(), + passkey)); + // Confirm all + callback(true); + } + + void agent_request_authorization( + bt::Device& device, std::function callback) override { + logger_.dbg(std::format("Device request authorization: {}", device.name())); + callback(false); + } + + void agent_authorize_service(bt::Device& device, std::string const& uuid, + std::function callback) override { + if (uuid == "0000110d-0000-1000-8000-00805f9b34fb") { + // Advanced Audio Distribution Profile + callback(true); + return; + } + if (uuid == "0000111e-0000-1000-8000-00805f9b34fb") { + // Hands-Free Service Class and Profile + // Not interrested. + callback(false); + return; + } + + logger_.dbg(std::format("Device request authorize unknown service: {} {}", + device.name(), uuid)); + + // Do not authorize unknown services. + callback(false); + } + + void agent_cancel() override {} + + private: + logger::Logger& logger_; + Signaler& signaler_; + bt::Adapter* adapter_{nullptr}; +}; + +class SignalerImpl : public Signaler, ws::Server::Delegate { + public: + SignalerImpl(logger::Logger& logger, cfg::Config const& cfg, + looper::Looper& looper) + : server_(ws::create_server(logger, cfg, looper, *this)) {} + + void send(std::string_view signal) override { + server_->send_text_to_all(signal); + } + + std::unique_ptr handle( + http::Request const& request) override { + return server_->handle(request); + } + + std::unique_ptr handle(ws::Message const& /* msg */) override { + // Ignore anything sent by clients + return nullptr; + } + + private: + std::unique_ptr server_; +}; + +} // namespace + +std::unique_ptr create_signaler(logger::Logger& logger, + cfg::Config const& cfg, + looper::Looper& looper) { + return std::make_unique(logger, cfg, looper); +} + +std::unique_ptr create_bt_delegate(logger::Logger& logger, + Signaler& signaler) { + return std::make_unique(logger, signaler); +} + +std::unique_ptr create_http_delegate( + Api& api, Signaler& signaler) { + return std::make_unique(api, signaler); +} + +} // namespace server -- cgit v1.2.3-70-g09d2