diff options
| -rw-r--r-- | meson.build | 41 | ||||
| -rw-r--r-- | src/fake_api.cc | 409 | ||||
| -rw-r--r-- | src/main.cc | 392 | ||||
| -rw-r--r-- | src/server.cc | 397 | ||||
| -rw-r--r-- | src/server.hh | 50 |
5 files changed, 899 insertions, 390 deletions
diff --git a/meson.build b/meson.build index 58f38a0..fd24f0a 100644 --- a/meson.build +++ b/meson.build @@ -256,6 +256,41 @@ websocket_dep = declare_dependency( dependencies: [base64_dep, looper_dep, logger_dep, str_dep, sha1_dep], ) +server_lib = library( + 'server', + sources: [ + 'src/server.cc', + 'src/server.hh', + ], + include_directories: inc, + dependencies : [ + http_dep, + json_dep, + looper_dep, + uri_dep, + websocket_dep, + ], +) +server_dep = declare_dependency( + link_with: server_lib, + dependencies: [http_dep, json_dep, looper_dep, uri_dep, websocket_dep], +) + +fake_api = executable( + 'fake-api', + sources: [ + 'src/fake_api.cc', + ], + include_directories: inc, + install : true, + dependencies : [ + args_dep, + cfg_dep, + signals_dep, + server_dep, + ], +) + bluetooth_jukebox = executable( 'bluetooth-jukebox', sources: [ @@ -267,12 +302,8 @@ bluetooth_jukebox = executable( args_dep, bt_dep, cfg_dep, - http_dep, - json_dep, - looper_dep, signals_dep, - uri_dep, - websocket_dep, + server_dep, ], ) diff --git a/src/fake_api.cc b/src/fake_api.cc new file mode 100644 index 0000000..f61ac8f --- /dev/null +++ b/src/fake_api.cc @@ -0,0 +1,409 @@ +#include "args.hh" +#include "bt.hh" +#include "cfg.hh" +#include "config.h" +#include "http.hh" +#include "logger.hh" +#include "looper.hh" +#include "server.hh" +#include "signals.hh" +#include "websocket.hh" + +#include <algorithm> +#include <cstdint> +#include <functional> +#include <iostream> +#include <iterator> +#include <memory> +#include <optional> +#include <ranges> +#include <string> +#include <string_view> +#include <utility> +#include <vector> + +#ifndef VERSION +# define VERSION "unknown" +#endif + +namespace { + +class FakeConfig : public cfg::Config { + public: + [[nodiscard]] + std::optional<std::string_view> get( + std::string_view /* name */) const override { + return std::nullopt; + } +}; + +class FakeApi { + public: + virtual ~FakeApi() = default; + + [[nodiscard]] + virtual std::vector<bt::Device*> devices() = 0; + + virtual void update_adapter() = 0; + virtual void update_device(bt::Device& device) = 0; + virtual void update_player(bt::Player& player) = 0; + + protected: + FakeApi() = default; +}; + +class FakeAdapter : public bt::Adapter { + public: + explicit FakeAdapter(FakeApi& api) : api_(api) {} + + [[nodiscard]] + std::string const& address() const override { + return address_; + } + + [[nodiscard]] + std::string const& name() const override { + return name_; + } + + [[nodiscard]] + bool discoverable() const override { + return discoverable_; + } + + [[nodiscard]] + uint32_t discoverable_timeout_seconds() const override { + return discoverable_timeout_seconds_; + } + + [[nodiscard]] + bool pairable() const override { + return true; + } + + [[nodiscard]] + uint32_t pairable_timeout_seconds() const override { + return pairable_timeout_seconds_; + } + + [[nodiscard]] + bool pairing() const override { + return false; + } + + [[nodiscard]] + bool powered() const override { + return true; + } + + [[nodiscard]] + bool connectable() const override { + return true; + } + + [[nodiscard]] + std::vector<bt::Device*> devices() const override { + return api_.devices(); + } + + void set_discoverable(bool discoverable) override { + if (discoverable == discoverable_) + return; + + discoverable_ = discoverable; + api_.update_adapter(); + } + + void set_discoverable_timeout_seconds(uint32_t timeout) override { + if (discoverable_timeout_seconds_ == timeout) + return; + + discoverable_timeout_seconds_ = timeout; + api_.update_adapter(); + } + + void set_pairable_timeout_seconds(uint32_t timeout) override { + if (pairable_timeout_seconds_ == timeout) + return; + + pairable_timeout_seconds_ = timeout; + api_.update_adapter(); + } + + private: + FakeApi& api_; + std::string const address_{"00:11:22:33:44:66"}; + std::string const name_{"Fake adapter"}; + bool discoverable_{false}; + uint32_t discoverable_timeout_seconds_{180}; + uint32_t pairable_timeout_seconds_{0}; +}; + +class FakeDevice : public bt::Device { + public: + FakeDevice(FakeApi& api, std::string address, std::string name) + : api_(api), address_(std::move(address)), name_(std::move(name)) {} + + [[nodiscard]] + std::string const& address() const override { + return address_; + } + + [[nodiscard]] + std::string const& name() const override { + return name_; + } + + [[nodiscard]] + bool paired() const override { + return paired_; + } + + [[nodiscard]] + bool connected() const override { + return player_ != nullptr; + } + + // Returns null if there are no players. + // Otherwise, returns the "primary" player. + [[nodiscard]] + bt::Player* player() const override { + return player_; + } + + void set_paired(bool paired) { + if (paired_ == paired) + return; + + paired_ = paired; + api_.update_device(*this); + } + + void set_player(bt::Player* player) { + if (player_ == player) + return; + + player_ = player; + api_.update_device(*this); + } + + private: + FakeApi& api_; + std::string const address_; + std::string const name_; + bool paired_{false}; + bt::Player* player_{nullptr}; +}; + +class FakePlayer : public bt::Player { + public: + explicit FakePlayer(FakeApi& api) : api_(api) {} + + [[nodiscard]] + Status status() const override { + return status_; + } + + [[nodiscard]] + std::string const& track_title() const override { + return title_; + } + + [[nodiscard]] + std::string const& track_artist() const override { + return artist_; + } + + [[nodiscard]] + std::string const& track_album() const override { + return album_; + } + + void play() override { + if (status_ == Status::kPlaying) + return; + status_ = Status::kPlaying; + api_.update_player(*this); + } + + void pause() override { + if (status_ == Status::kPaused) + return; + status_ = Status::kPaused; + api_.update_player(*this); + } + + void set(std::string const& title, std::string const& artist, + std::string const& album) { + if (title_ == title && artist_ == artist && album_ == album) + return; + title_ = title; + artist_ = artist; + album_ = album; + api_.update_player(*this); + } + + private: + FakeApi& api_; + Status status_{Status::kStopped}; + std::string title_; + std::string artist_; + std::string album_; +}; + +class FakeBluetoothManager : public FakeApi { + public: + FakeBluetoothManager(looper::Looper& looper, bt::Manager::Delegate& delegate) + : looper_(looper), delegate_(delegate) { + devices_.emplace_back(std::make_unique<FakeDevice>( + *this, "11:22:33:44:55:66", "Fake device 1")); + devices_.emplace_back(std::make_unique<FakeDevice>( + *this, "66:55:44:33:22:11", "Fake device 2")); + + players_.emplace_back(std::make_unique<FakePlayer>(*this)); + players_.emplace_back(std::make_unique<FakePlayer>(*this)); + + delegate_.new_adapter(&adapter_); + + schedule_next(); + } + + ~FakeBluetoothManager() override { + if (timer_) + looper_.cancel(timer_); + } + + std::vector<bt::Device*> devices() override { + std::vector<bt::Device*> ret; + std::ranges::copy(std::views::transform( + devices_, [](auto& device) { return device.get(); }), + std::back_inserter(ret)); + return ret; + } + + void update_adapter() override { delegate_.updated_adapter(adapter_); } + + void update_device(bt::Device& device) override { + delegate_.updated_device(device); + } + + void update_player(bt::Player& player) override { + auto it = std::ranges::find_if(devices_, [&player](auto& device) { + return device->player() == &player; + }); + if (it != devices_.end()) { + delegate_.updated_player(**it, player); + } + } + + private: + void schedule_next() { + timer_ = looper_.schedule(5, [this](uint32_t /* timer */) { + step(); + schedule_next(); + }); + } + + void step() { + switch (step_++) { + case 0: + devices_[0]->set_paired(true); + break; + case 1: + devices_[1]->set_paired(true); + break; + case 2: + devices_[1]->set_player(players_[1].get()); + delegate_.added_player(*devices_[1], *players_[1]); + break; + case 3: + players_[1]->play(); + break; + case 4: + players_[1]->pause(); + break; + case 5: + devices_[1]->set_player(nullptr); + delegate_.removed_player(*devices_[1]); + break; + case 6: + devices_[1]->set_paired(false); + break; + case 7: + devices_[0]->set_paired(false); + break; + default: + step_ = 0; + break; + } + } + + FakeAdapter adapter_{*this}; + std::vector<std::unique_ptr<FakeDevice>> devices_; + std::vector<std::unique_ptr<FakePlayer>> players_; + looper::Looper& looper_; + bt::Manager::Delegate& delegate_; + uint32_t timer_{0}; + uint32_t step_{0}; +}; + +bool run(logger::Logger& logger, std::unique_ptr<http::OpenPort> port) { + FakeConfig cfg; + auto looper = looper::create(); + auto signaler = server::create_signaler(logger, cfg, *looper); + auto bt_delegate = server::create_bt_delegate(logger, *signaler); + auto http_delegate = server::create_http_delegate(*bt_delegate, *signaler); + auto server = http::create_server(logger, cfg, *looper, std::move(port), + *http_delegate); + FakeBluetoothManager fake_bt(*looper, *bt_delegate); + auto sigint_handler = signals::Handler::create( + *looper, signals::Signal::INT, [&looper, &logger]() { + logger.info("Received SIGINT, quitting..."); + looper->quit(); + }); + auto sigterm_handler = signals::Handler::create( + *looper, signals::Signal::TERM, [&looper, &logger]() { + logger.info("Received SIGTERM, quitting..."); + looper->quit(); + }); + return looper->run(logger); +} + +} // namespace + +int main(int argc, char** argv) { + auto args = Args::create(); + auto opt_help = args->option('h', "help", "display this text and exit."); + auto opt_version = args->option('V', "version", "display version and exit."); + auto opt_bind = + args->option_argument('B', "bind", "HOST:PORT", + "HOST:PORT to bind to, defaults to localhost:5555"); + if (!args->run(argc, argv)) { + args->print_error(std::cerr); + std::cerr << "Try 'fake-api --help' for more information.\n"; + return 1; + } + if (opt_help->is_set()) { + std::cout << "Usage: fake-api [OPTION...]\n" + << "\n"; + args->print_help(std::cout); + return 0; + } + if (opt_version->is_set()) { + std::cout << "bluetooth-jukebox " << VERSION + << " written by Joel Klinghed <the_jk@spawned.biz>.\n"; + return 0; + } + + auto logger = logger::stderr(/* verbose */ true); + + std::string_view bind = "localhost:5555"; + if (opt_bind->is_set()) + bind = opt_bind->argument(); + + auto port = http::open_port(bind, *logger); + if (!port) + return 1; + + return run(*logger, std::move(port)) ? 0 : 1; +} diff --git a/src/main.cc b/src/main.cc index 1f4bfd4..ff05a55 100644 --- a/src/main.cc +++ b/src/main.cc @@ -3,15 +3,13 @@ #include "cfg.hh" #include "config.h" #include "http.hh" -#include "json.hh" #include "logger.hh" #include "looper.hh" +#include "server.hh" #include "signals.hh" -#include "uri.hh" #include "websocket.hh" #include <cerrno> -#include <cstdint> #include <cstring> #include <format> #include <functional> @@ -30,391 +28,15 @@ namespace { -class Api { - public: - virtual ~Api() = default; - - [[nodiscard]] - virtual bt::Adapter* adapter() const = 0; - - protected: - Api() = default; -}; - -const std::string_view kSignalUpdateAdapter("controller/update"); -const std::string kSignalUpdateDevicePrefix("device/update/"); - -class Signaler { - public: - virtual ~Signaler() = default; - - virtual void send(std::string_view signal) = 0; - virtual std::unique_ptr<http::Response> handle( - http::Request const& request) = 0; - - protected: - Signaler() = default; -}; - -std::optional<bool> 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<http::Response> 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<bool> 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<http::Response> 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<http::Response> 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> json_writer_; - std::string json_tmp_; - std::unique_ptr<http::MimeType> json_mimetype_; -}; - -class BluetoothManagerDelegate : public bt::Manager::Delegate, 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<void(std::optional<std::string>)> 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<void(std::optional<uint32_t>)> 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<void(bool)> 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<void(bool)> 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<void(bool)> 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<http::Response> handle( - http::Request const& request) override { - return server_->handle(request); - } - - std::unique_ptr<ws::Message> handle(ws::Message const& /* msg */) override { - // Ignore anything sent by clients - return nullptr; - } - - private: - std::unique_ptr<ws::Server> server_; -}; - bool run(logger::Logger& logger, cfg::Config const& cfg, std::unique_ptr<http::OpenPort> port) { auto looper = looper::create(); - SignalerImpl signaler(logger, cfg, *looper); - BluetoothManagerDelegate bt_delegate(logger, signaler); - HttpServerDelegate http_delegate(bt_delegate, signaler); - auto server = - http::create_server(logger, cfg, *looper, std::move(port), http_delegate); - auto manager = bt::create_manager(logger, cfg, *looper, bt_delegate); + auto signaler = server::create_signaler(logger, cfg, *looper); + auto bt_delegate = server::create_bt_delegate(logger, *signaler); + auto http_delegate = server::create_http_delegate(*bt_delegate, *signaler); + auto server = http::create_server(logger, cfg, *looper, std::move(port), + *http_delegate); + auto manager = bt::create_manager(logger, cfg, *looper, *bt_delegate); auto sigint_handler = signals::Handler::create( *looper, signals::Signal::INT, [&looper, &logger]() { logger.info("Received SIGINT, quitting..."); 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 <cstdint> +#include <format> +#include <functional> +#include <memory> +#include <optional> +#include <string> +#include <string_view> + +namespace server { + +namespace { + +const std::string_view kSignalUpdateAdapter("controller/update"); +const std::string kSignalUpdateDevicePrefix("device/update/"); + +std::optional<bool> 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<http::Response> 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<bool> 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<http::Response> 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<http::Response> 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> json_writer_; + std::string json_tmp_; + std::unique_ptr<http::MimeType> 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<void(std::optional<std::string>)> 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<void(std::optional<uint32_t>)> 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<void(bool)> 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<void(bool)> 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<void(bool)> 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<http::Response> handle( + http::Request const& request) override { + return server_->handle(request); + } + + std::unique_ptr<ws::Message> handle(ws::Message const& /* msg */) override { + // Ignore anything sent by clients + return nullptr; + } + + private: + std::unique_ptr<ws::Server> server_; +}; + +} // namespace + +std::unique_ptr<Signaler> create_signaler(logger::Logger& logger, + cfg::Config const& cfg, + looper::Looper& looper) { + return std::make_unique<SignalerImpl>(logger, cfg, looper); +} + +std::unique_ptr<Api> create_bt_delegate(logger::Logger& logger, + Signaler& signaler) { + return std::make_unique<BluetoothManagerDelegate>(logger, signaler); +} + +std::unique_ptr<http::Server::Delegate> create_http_delegate( + Api& api, Signaler& signaler) { + return std::make_unique<HttpServerDelegate>(api, signaler); +} + +} // namespace server diff --git a/src/server.hh b/src/server.hh new file mode 100644 index 0000000..0601ba8 --- /dev/null +++ b/src/server.hh @@ -0,0 +1,50 @@ +#ifndef SERVER_HH +#define SERVER_HH + +#include "bt.hh" +#include "cfg.hh" +#include "http.hh" +#include "logger.hh" +#include "looper.hh" + +#include <memory> +#include <string_view> + +namespace server { + +class Api : public bt::Manager::Delegate { + public: + virtual ~Api() = default; + + [[nodiscard]] + virtual bt::Adapter* adapter() const = 0; + + protected: + Api() = default; +}; + +class Signaler { + public: + virtual ~Signaler() = default; + + virtual void send(std::string_view signal) = 0; + virtual std::unique_ptr<http::Response> handle( + http::Request const& request) = 0; + + protected: + Signaler() = default; +}; + +std::unique_ptr<Signaler> create_signaler(logger::Logger& logger, + cfg::Config const& cfg, + looper::Looper& looper); + +std::unique_ptr<Api> create_bt_delegate(logger::Logger& logger, + Signaler& signaler); + +std::unique_ptr<http::Server::Delegate> create_http_delegate( + Api& api, Signaler& signaler); + +} // namespace server + +#endif // SERVER_HH |
