#include <thread>
#include <json/value.h>
+#include "ChannelInfo.h"
#include "DrawingGame.h"
#include "Mixer.h"
#include "Renderer.h"
+#include "Shoutout.h"
#include "State.h"
#include "Stream.h"
#include "../ffmpeg/Network.h"
, mixer(stream.GetAudioPlane(), stream.GetAudioChannels(), stream.GetAudioFrameSize())
, renderer(stream.GetVideoPlane(), stream.GetVideoLineSize(), width, height)
, state(width, height)
+ , own_channel_id("1020523186")
, drawing_game(renderer.GetContext(), 45, 50, { 1, 1, 1 }) {
state.SetGame(&drawing_game);
}
public:
void Start() {
- pusher_conn.Subscribe("ChatBotLog", &PusherHandler, this);
- twitch_conn.Join("#horstiebot", &TwitchHandler, this);
+ std::cout << "starting services" << std::endl;
+ pusher_conn.Subscribe("Channel").Then([this](const Json::Value &json) -> void {
+ HandlePusherChannel(json);
+ });
+ pusher_conn.Subscribe("ChatBotLog").Then([this](const Json::Value &json) -> void {
+ HandlePusherChatBotLog(json);
+ });
+ twitch_conn.Join("#horstiebot").Then([this](const twitch::IRCMessage &msg) -> void {
+ HandleTwitch(msg);
+ });
+ ws_ctx.HttpsRequest("GET", "alttp.localhorst.tv", "/api/channels?chatting=1").GetPromise().Then([this](ws::HttpsConnection &rsp) -> void {
+ InitChannels(rsp);
+ });
stream.Start();
//Media &media = state.AddMedia("test.mp4");
state.Clean();
- if (difference > 3000) {
- std::this_thread::sleep_for(std::chrono::milliseconds(difference - 3000));
+ if (difference > 1000) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(difference - 1000));
}
}
}
private:
- static void PusherHandler(void *user, const Json::Value &json) {
- Application *app = static_cast<Application *>(user);
- app->HandlePusher(json);
+ void InitChannels(const ws::HttpsConnection &rsp) {
+ Json::Value json;
+ Json::Reader json_reader;
+ json_reader.parse(rsp.GetBody(), json);
+ for (const Json::Value &channel : json) {
+ int channel_id = channel["id"].asInt();
+ ChannelInfo &info = state.GetChannelInfo(channel_id);
+ info.Update(channel);
+ }
}
- void HandlePusher(const Json::Value &json) {
+ void HandlePusherChannel(const Json::Value &json) {
+ const std::string event = json["event"].asString();
+ if (event != "ChannelUpdated") return;
+
+ const std::string data_string = json["data"].asString();
+ Json::Value data;
+ Json::Reader json_reader;
+ json_reader.parse(data_string, data);
+ UpdateChannel(data["model"]);
+ }
+
+ void UpdateChannel(const Json::Value &json) {
+ int channel_id = json["id"].asInt();
+ bool is_live =json["twitch_live"].asBool();
+ bool is_known = state.IsChannelKnown(channel_id);
+ ChannelInfo &channel = state.GetChannelInfo(channel_id);
+ bool went_live = !channel.twitch_live && is_live;
+ bool went_down = channel.twitch_live && !is_live;
+ channel.Update(json);
+ if (went_live) {
+ // channel went live
+ std::cout << "channel " << channel.title << " went live" << std::endl;
+ ShoutoutChannel(channel_id);
+ } else if (went_down) {
+ // channel went down
+ std::cout << "channel " << channel.title << " went down" << std::endl;
+ }
+ }
+
+ void ShoutoutChannel(int id) {
+ ChannelInfo &channel = state.GetChannelInfo(id);
+ Shoutout &shout = renderer.CreateShoutout(id, state);
+ if (!channel.twitch_id.empty() && channel.twitch_id != own_channel_id) {
+ ws::HttpsConnection &req = twitch_conn.AuthorizedRequest("POST", "api.twitch.tv", "/helix/chat/shoutouts");
+ req.SetHeader("Content-Type", "application/x-www-form-urlencoded");
+ req.AddFormUrlenc("from_broadcaster_id", own_channel_id);
+ req.AddFormUrlenc("to_broadcaster_id", channel.twitch_id);
+ req.AddFormUrlenc("moderator_id", own_channel_id);
+ req.SetContentLength();
+ }
+ }
+
+ void HandlePusherChatBotLog(const Json::Value &json) {
const std::string data_string = json["data"].asString();
Json::Value data;
Json::Reader json_reader;
msg.Update(renderer.GetContext());
}
- static void TwitchHandler(void *user, const twitch::IRCMessage &msg) {
- Application *app = static_cast<Application *>(user);
- app->HandleTwitch(msg);
- }
-
void HandleTwitch(const twitch::IRCMessage &msg) {
if (state.HasGame()) {
state.GetGame().Handle(msg);
Mixer mixer;
Renderer renderer;
State state;
+ std::string own_channel_id;
DrawingGame drawing_game;
--- /dev/null
+#ifndef TEST_APP_CHANNELINFO_H_
+#define TEST_APP_CHANNELINFO_H_
+
+#include <stdexcept>
+#include <string>
+#include <json/json.h>
+
+namespace app {
+
+struct ChannelInfo {
+
+ ChannelInfo()
+ : id(0), title(), chat(false), join(false), twitch_live(false) {
+ }
+ explicit ChannelInfo(const Json::Value &json)
+ : id(json["id"].asInt())
+ , title(json["title"].asString())
+ , twitch_id(json["twitch_id"].asString())
+ , twitch_title(json["twitch_title"].asString())
+ , twitch_category(json["twitch_category_name"].asString())
+ , chat(json["chat"].asBool())
+ , join(json["join"].asBool())
+ , twitch_live(json["twitch_live"].asBool()) {
+ }
+
+ void Update(const Json::Value &json) {
+ if (json["id"].asInt() != id) {
+ throw std::runtime_error("update channel ID mismatch");
+ }
+ title = json["title"].asString();
+ twitch_id = json["twitch_id"].asString();
+ twitch_title = json["twitch_title"].asString();
+ twitch_category = json["twitch_category_name"].asString();
+ chat = json["chat"].asBool();
+ join = json["join"].asBool();
+ twitch_live = json["twitch_live"].asBool();
+ }
+
+ int id;
+ std::string title;
+ std::string twitch_id;
+ std::string twitch_title;
+ std::string twitch_category;
+ bool chat;
+ bool join;
+ bool twitch_live;
+
+};
+
+}
+
+#endif
return av_rescale_q(counter, timebase, AVRational{1, 1000});
}
+ int64_t GetSeconds() const {
+ return av_rescale_q(counter, timebase, AVRational{1, 1});
+ }
+
const AVRational &GetTimebase() const {
return timebase;
}
#ifndef TEST_APP_RENDERER_H_
#define TEST_APP_RENDERER_H_
+#include "Shoutout.h"
#include <cstdint>
extern "C" {
}
#include "Message.h"
+#include "Shoutout.h"
#include "State.h"
#include "../cairo/Context.h"
#include "../cairo/Surface.h"
ctx.SetSourceRGB(0, 0, 0);
ctx.Paint();
- for (const Media &media : state.GetMedia()) {
- media.Render(ctx);
- }
-
for (const Message &msg : state.GetMessages()) {
msg.Render(ctx);
}
state.GetGame().Render(ctx);
}
+ for (const Media &media : state.GetMedia()) {
+ media.Render(ctx);
+ }
+
+ if (!state.GetShoutouts().empty()) {
+ state.GetShoutouts().front().Render(ctx);
+ }
+
surface.Flush();
}
return msg;
}
+ Shoutout &CreateShoutout(int channel_id, State &state) {
+ Shoutout &shout = state.AddShoutout(channel_id, ctx);
+ shout.SetTitleFont(text_font);
+ shout.SetChannelFont(channel_font);
+ shout.SetCategoryFont(channel_font);
+ shout.Recalc(ctx);
+ return shout;
+ }
+
cairo::Context &GetContext() {
return ctx;
}
--- /dev/null
+#ifndef TEST_APP_SHOUTOUT_H_
+#define TEST_APP_SHOUTOUT_H_
+
+#include "ChannelInfo.h"
+#include "Clock.h"
+#include "../cairo/Context.h"
+#include "../gfx/ColorRGB.h"
+#include "../gfx/Position.h"
+#include "../gfx/Spacing.h"
+#include <cstdint>
+
+namespace app {
+
+class Shoutout {
+
+public:
+ Shoutout(const ChannelInfo &channel, cairo::Context &ctx)
+ : channel(channel)
+ , title_layout(ctx.CreateLayout())
+ , channel_layout(ctx.CreateLayout())
+ , category_layout(ctx.CreateLayout())
+ , bg_color{ 0.1, 0.1, 0.1 }
+ , title_color{ 1, 1, 1 }
+ , channel_color{ 0.392, 0.255, 0.647 }
+ , anchor{ 1280, 720 - 75 }
+ , size{ 1280, 720 }
+ , padding(10)
+ , category_color{ 0.6, 0.6, 0.6 }
+ , start_time()
+ , running(false)
+ , done(false) {
+ title_layout.SetText(channel.twitch_title);
+ channel_layout.SetText(channel.title);
+ category_layout.SetText(channel.twitch_category);
+
+ size.w = 1280 - 2 * 75;
+ title_layout.SetWidth(size.w - padding.Horizontal());
+ channel_layout.SetWidth((size.w - padding.Horizontal(2)) / 2.0);
+ category_layout.SetWidth((size.w - padding.Horizontal(2)) / 2.0);
+ ctx.UpdateLayout(title_layout);
+ ctx.UpdateLayout(channel_layout);
+ ctx.UpdateLayout(category_layout);
+
+ size.h = padding.Vertical(2) + title_layout.GetLogicalRect().height + std::max(channel_layout.GetLogicalRect().height, category_layout.GetLogicalRect().height);
+ }
+ ~Shoutout() {
+ }
+
+ Shoutout(const Shoutout &) = delete;
+ Shoutout &operator =(const Shoutout &) = delete;
+
+public:
+ bool Loading() const {
+ return false;
+ }
+
+ bool Running() const {
+ return running;
+ }
+
+ bool Done() const {
+ return done;
+ }
+
+ void SetTitleFont(pango::Font &font) {
+ title_layout.SetFont(font);
+ }
+
+ void SetChannelFont(pango::Font &font) {
+ channel_layout.SetFont(font);
+ }
+
+ void SetCategoryFont(pango::Font &font) {
+ category_layout.SetFont(font);
+ }
+
+ void Start(const Clock &clock) {
+ start_time = clock;
+ running = true;
+ }
+
+ void Update(cairo::Context &ctx, const Clock &clock) {
+ const Clock runtime = clock.Difference(start_time);
+ int64_t ms = runtime.GetMS();
+ if (ms < 600) {
+ anchor.x = runtime.InterpolateClamp(1280, 75, 0, 600);
+ anchor.y = 720 - 75;
+ } else if (ms > 59000) {
+ anchor.x = 75;
+ anchor.y = runtime.InterpolateClamp(720 - 75, 720 + size.h, 59000, 59400);
+ } else {
+ anchor.x = 75;
+ anchor.y = 720 - 75;
+ }
+ if (ms > 125000) {
+ done = true;
+ }
+ }
+
+ void Recalc(cairo::Context &ctx) {
+ ctx.UpdateLayout(title_layout);
+ ctx.UpdateLayout(channel_layout);
+ ctx.UpdateLayout(category_layout);
+
+ size.h = padding.Vertical(2) + title_layout.GetLogicalRect().height + std::max(channel_layout.GetLogicalRect().height, category_layout.GetLogicalRect().height);
+ }
+
+ void Render(cairo::Context &ctx) const {
+ gfx::Position pos = anchor - gfx::Position{ 0, size.h };
+
+ ctx.SetSourceColor(bg_color);
+ ctx.Rectangle(pos, size);
+ ctx.Fill();
+
+ gfx::Position title_pos = pos + padding.InnerTL(size);
+ ctx.MoveTo(title_pos);
+ ctx.SetSourceColor(title_color);
+ ctx.DrawLayout(title_layout);
+
+ gfx::Position channel_pos = pos + padding.InnerBL(size) - gfx::Position{ 0.0, double(channel_layout.GetLogicalRect().height) };
+ ctx.MoveTo(channel_pos);
+ ctx.SetSourceColor(channel_color);
+ ctx.DrawLayout(channel_layout);
+
+ gfx::Position category_pos = pos + padding.InnerBR(size) - gfx::Position{ double(category_layout.GetLogicalRect().width), double(category_layout.GetLogicalRect().height) };
+ ctx.MoveTo(category_pos);
+ ctx.SetSourceColor(category_color);
+ ctx.DrawLayout(category_layout);
+ }
+
+private:
+ const ChannelInfo &channel;
+
+ pango::Layout title_layout;
+ pango::Layout channel_layout;
+ pango::Layout category_layout;
+ gfx::ColorRGB bg_color;
+ gfx::ColorRGB title_color;
+ gfx::ColorRGB channel_color;
+ gfx::ColorRGB category_color;
+
+ gfx::Position anchor;
+ gfx::Size size;
+ gfx::Spacing padding;
+
+ Clock start_time;
+ bool running;
+ bool done;
+
+};
+
+}
+
+#endif
#define TEST_APP_STATE_H_
#include <list>
+#include <map>
#include <ostream>
+#include "ChannelInfo.h"
#include "Clock.h"
#include "Game.h"
#include "Media.h"
#include "Message.h"
+#include "Shoutout.h"
#include "../cairo/Context.h"
#include "../gfx/Position.h"
game = nullptr;
}
+ bool IsChannelKnown(int id) const {
+ auto it = channels.find(id);
+ return it != channels.end();
+ }
+
+ ChannelInfo &GetChannelInfo(int id) {
+ auto it = channels.find(id);
+ if (it != channels.end()) {
+ return it->second;
+ }
+ ChannelInfo &info = channels[id];
+ info.id = id;
+ return info;
+ }
+
const std::list<Media> &GetMedia() const {
return media;
}
return msgs;
}
+ const std::list<Shoutout> &GetShoutouts() const {
+ return shoutouts;
+ }
+
Media &AddMedia(const char *url) {
- std::cout << "adding media " << url << std::endl;
media.emplace_back(url);
return media.back();
}
return msgs.front();
}
+ Shoutout &AddShoutout(int channel_id, cairo::Context &ctx) {
+ shoutouts.emplace_back(GetChannelInfo(channel_id), ctx);
+ return shoutouts.back();
+ }
+
int GetWidth() const {
return width;
}
if (HasGame()) {
GetGame().Update(ctx, clock);
}
+ if (!shoutouts.empty()) {
+ if (shoutouts.front().Done()) {
+ shoutouts.pop_front();
+ } else if (shoutouts.front().Running()) {
+ shoutouts.front().Update(ctx, clock);
+ } else if (!shoutouts.front().Loading()) {
+ shoutouts.front().Start(clock);
+ }
+ }
}
void Clean() {
Game *game;
+ std::map<int, ChannelInfo> channels;
+
std::list<Media> media;
std::list<Message> msgs;
+ std::list<Shoutout> shoutouts;
};
};
+inline gfx::Position operator +(const Position &a, const Position &b) {
+ return gfx::Position{a.x + b.x, a.y + b.y};
}
-inline gfx::Position operator +(const gfx::Position &a, const gfx::Position &b) {
- return gfx::Position{a.x + b.x, a.y + b.y};
+inline gfx::Position operator -(const Position &a, const Position &b) {
+ return gfx::Position{a.x - b.x, a.y - b.y};
}
-inline std::ostream &operator <<(std::ostream &out, const gfx::Position &pos) {
+inline std::ostream &operator <<(std::ostream &out, const Position &pos) {
return out << '(' << pos.x << ", " << pos.y << ')';
}
+}
+
#endif
#define TEST_GFX_SPACING_H_
#include "Position.h"
+#include "Size.h"
namespace gfx {
double top = 0.0;
double bottom = 0.0;
double right = 0.0;
+ double h_inter = 0.0;
+ double v_inter = 0.0;
explicit Spacing(double all)
- : left(all), top(all), bottom(all), right(all) {
+ : left(all), top(all), bottom(all), right(all), h_inter(all), v_inter(all) {
}
Spacing(double horiz, double vert)
- : left(horiz), top(vert), bottom(horiz), right(vert) {
+ : left(horiz), top(vert), bottom(horiz), right(vert), h_inter(horiz), v_inter(vert) {
}
Position Offset() const {
return Position{left, top};
}
+ Position Offset(int nx, int ny) const {
+ return Position{left + h_inter * double(nx), top + v_inter * double(ny)};
+ }
+
+ double Horizontal(int n = 1) const {
+ return left + right + h_inter * double(n - 1);
+ }
+
+ double Vertical(int n = 1) const {
+ return top + bottom + v_inter * double(n - 1);
+ }
+
+ Position InnerTL(const Size &bounds) const {
+ return Position{ left, top };
+ }
+
+ Position InnerTR(const Size &bounds) const {
+ return Position{ bounds.w - right, top };
+ }
+
+ Position InnerBL(const Size &bounds) const {
+ return Position{ left, bounds.h - bottom };
+ }
+
+ Position InnerBR(const Size &bounds) const {
+ return Position{ bounds.w - right, bounds.h - bottom };
+ }
+
};
}
#ifndef TEST_SYS_PROMISE_H_
#define TEST_SYS_PROMISE_H_
+#include <exception>
#include <functional>
#include <iostream>
#include <vector>
for (Callback &callback : success) {
try {
callback(args...);
+ } catch (const std::exception &e) {
+ std::cerr << "exception in promise resolution: " << e.what() << std::endl;
} catch (...) {
std::cerr << "exception in promise resolution" << std::endl;
}
for (Callback &callback : error) {
try {
callback(args...);
+ } catch (const std::exception &e) {
+ std::cerr << "exception in promise rejection: " << e.what() << std::endl;
} catch (...) {
std::cerr << "exception in promise rejection" << std::endl;
}
return access_token;
}
+ const std::string &GetClientId() const {
+ return client_id;
+ }
+
PromiseType &Refresh(ws::Context &ws);
private:
}
break;
case LWS_CALLBACK_WSI_CREATE:
+ case LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED:
case LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION:
+ case LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH:
case LWS_CALLBACK_CLIENT_HTTP_DROP_PROTOCOL:
case LWS_CALLBACK_CLOSED_CLIENT_HTTP:
break;
break;
case LWS_CALLBACK_CLIENT_WRITEABLE:
if (out_buffer.length() > LWS_PRE) {
- int res = lws_write(wsi, reinterpret_cast<unsigned char *>(&out_buffer[LWS_PRE]), out_buffer.length() - LWS_PRE, LWS_WRITE_TEXT);
+ size_t pos = out_buffer.find('\0', LWS_PRE);
+ size_t len = pos == std::string::npos ? out_buffer.length() : pos;
+ int res = lws_write(wsi, reinterpret_cast<unsigned char *>(&out_buffer[LWS_PRE]), len - LWS_PRE, LWS_WRITE_TEXT);
if (res > 0) {
- out_buffer.erase(LWS_PRE, res);
+ if (res == len - LWS_PRE && pos != std::string::npos) {
+ out_buffer.erase(LWS_PRE, res + 1);
+ } else {
+ out_buffer.erase(LWS_PRE, res);
+ }
+ }
+ if (out_buffer.length() > LWS_PRE) {
+ lws_callback_on_writable(wsi);
}
}
break;
info.ietf_version_or_minus_one = -1;
info.userdata = &ctx;
info.pwsi = &wsi;
- wsi = lws_client_connect_via_info(&info);
- if (!wsi) {
- throw std::runtime_error("failed to connect client");
- }
- lws_set_timer_usecs(wsi, 30000000);
- out_buffer.insert(0, LWS_PRE, '\0');
+ Connect();
token.Load();
}
switch (reason) {
case LWS_CALLBACK_CLIENT_ESTABLISHED:
connected = true;
+ std::cout << "twitch connection established" << std::endl;
OnConnect();
if (out_buffer.length() > LWS_PRE) {
lws_callback_on_writable(wsi);
break;
case LWS_CALLBACK_CLIENT_CLOSED:
connected = false;
+ authenticated = false;
std::cout << "twitch connection closed" << std::endl;
+ Connect();
break;
case LWS_CALLBACK_CLIENT_RECEIVE:
if (lws_is_first_fragment(wsi)) {
return 0;
}
+HttpsConnection &TwitchConnection::AuthorizedRequest(const char *method, const char *host, const char *path) {
+ HttpsConnection &req = ctx.HttpsRequest(method, host, path);
+ req.SetHeader("Authorization", "Bearer " + token.GetAccessToken());
+ req.SetHeader("Client-Id", token.GetClientId());
+ return req;
+}
+
}
HttpsConnection(const HttpsConnection &) = delete;
HttpsConnection &operator =(const HttpsConnection &) = delete;
-private:
- struct Callback {
- void *user;
- void (*callback)(void *, HttpsConnection &);
- void Call(HttpsConnection &val) const {
- (*callback)(user, val);
- }
- };
-
public:
void SetHeader(const std::string &name, const std::string &value) {
headers[name + ":"] = value;
#include <cstring>
#include <map>
#include <string>
-#include <vector>
#include <json/json.h>
#include <libwebsockets.h>
+#include "../sys/Promise.h"
+
namespace ws {
class Context;
class PusherConnection {
+public:
+ typedef sys::Promise<const Json::Value &> PromiseType;
+
public:
explicit PusherConnection(Context &ctx);
~PusherConnection() {
PusherConnection(const PusherConnection &) = delete;
PusherConnection &operator =(const PusherConnection &) = delete;
-private:
- struct Callback {
- void *user;
- void (*callback)(void *, const Json::Value &);
- void Call(const Json::Value &val) const {
- (*callback)(user, val);
- }
- };
-
public:
void Ping() {
SendMessage("{\"event\":\"pusher:ping\"}");
}
- void Subscribe(const std::string &chan, void (*callback)(void *, const Json::Value &), void *user = nullptr) {
- callbacks[chan].push_back({ user, callback });
- Json::Value json;
- json["event"] = "pusher:subscribe";
- json["data"]["channel"] = chan;
- SendMessage(json);
+ PromiseType &Subscribe(const std::string &chan) {
+ auto it = callbacks.find(chan);
+ if (it != callbacks.end()) {
+ return it->second;
+ } else {
+ Json::Value json;
+ json["event"] = "pusher:subscribe";
+ json["data"]["channel"] = chan;
+ SendMessage(json);
+ return callbacks[chan];
+ }
}
void SendMessage(const Json::Value &json) {
}
void SendMessage(const std::string &msg) {
+ if (out_buffer.length() > LWS_PRE) {
+ out_buffer.push_back('\0');
+ }
out_buffer.append(msg);
lws_callback_on_writable(wsi);
}
void SendMessage(const char *msg) {
+ if (out_buffer.length() > LWS_PRE) {
+ out_buffer.push_back('\0');
+ }
out_buffer.append(msg);
lws_callback_on_writable(wsi);
}
Json::Value json;
json_reader.parse(msg, json);
const std::string channel = json["channel"].asString();
- for (const Callback &callback : callbacks[channel]) {
- callback.Call(json);
- }
+ callbacks[channel].Resolve(json);
}
private:
Json::Reader json_reader;
Json::FastWriter json_writer;
- std::map<std::string, std::vector<Callback>> callbacks;
+ std::map<std::string, PromiseType> callbacks;
};
#include "../twitch/IRCMessage.h"
#include "../twitch/LoginToken.h"
+#include "../sys/Promise.h"
+#include "HttpsConnection.h"
namespace ws {
class TwitchConnection {
+public:
+ typedef sys::Promise<const twitch::IRCMessage &> PromiseType;
+
public:
explicit TwitchConnection(Context &ctx);
~TwitchConnection() {
TwitchConnection(const TwitchConnection &) = delete;
TwitchConnection &operator =(const TwitchConnection &) = delete;
-private:
- struct Callback {
- void *user;
- void (*callback)(void *, const twitch::IRCMessage &);
- void Call(const twitch::IRCMessage &val) const {
- (*callback)(user, val);
- }
- };
-
public:
void OnConnect() {
SendMessage("CAP REQ :twitch.tv/tags twitch.tv/commands");
SendMessage("NICK HorstieBot");
}
- void Join(const std::string &chan, void (*callback)(void *, const twitch::IRCMessage &), void *user = nullptr) {
- callbacks[chan].push_back({ user, callback });
- if (authenticated && callbacks[chan].size() == 1) {
- SendMessage("JOIN " + chan);
+ PromiseType &Join(const std::string &chan) {
+ auto it = callbacks.find(chan);
+ if (it != callbacks.end()) {
+ return it->second;
+ } else {
+ if (authenticated) {
+ SendMessage("JOIN " + chan);
+ }
+ return callbacks[chan];
}
}
lws_callback_on_writable(wsi);
}
+ HttpsConnection &AuthorizedRequest(const char *method, const char *host, const char *path);
+
public:
int ProtoCallback(lws_callback_reasons reason, void *in, size_t len);
if (msg.params.empty()) return;
auto it = callbacks.find(msg.params[0]);
if (it != callbacks.end()) {
- for (const Callback &callback : it->second) {
- callback.Call(msg);
- }
+ it->second.Resolve(msg);
+ }
+ }
+
+private:
+ void Connect() {
+ wsi = lws_client_connect_via_info(&info);
+ if (!wsi) {
+ throw std::runtime_error("failed to connect client");
}
+ lws_set_timer_usecs(wsi, 30000000);
+ in_buffer.clear();
+ out_buffer.clear();
+ out_buffer.insert(0, LWS_PRE, '\0');
}
private:
std::string in_buffer;
std::string out_buffer;
- std::map<std::string, std::vector<Callback>> callbacks;
+ std::map<std::string, PromiseType> callbacks;
twitch::LoginToken token;
twitch::IRCMessage in_msg;