A basic twitch chat viewer for windows written in C++20. It streams chat from the twitch IRC server using sockets, and uses SDL2 and SDL2_ttf to render.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

221 lines
5.2 KiB

#include <stdio.h>
#include <iostream>
#include <string>
#include <sys/types.h>
#include <cstring>
#include <string>
#include <sstream>
#include <istream>
#include <deque>
#include <vector>
#include <SDL.h>
#include <SDL_syswm.h>
#include <SDL_ttf.h>
#include "csscolorparser.hpp"
#pragma comment(lib, "Ws2_32.lib")
#include "colours.h"
#include "draw.h"
#include "irc.h"
#include "net.h"
#include "cxxopts.hpp"
std::deque<msg> message_queue {};
std::deque<msg> message_display_queue {};
struct sdl_context create_window(int width, int height, const std::string &window_title) {
int err = SDL_Init(SDL_INIT_EVERYTHING);
TTF_Init();
if (err) {
std::cerr << "SDL Err " << SDL_GetError();
exit(3);
}
struct SDL_Window* win = SDL_CreateWindow(window_title.c_str(),
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width, height,
SDL_WINDOW_RESIZABLE);
SDL_Renderer* ren = SDL_CreateRenderer(win, -1, SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC);
SDL_SetRenderDrawColor(ren, 255, 0, 0, 50);
SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_NONE);
SDL_RenderPresent(ren);
return { ren, win, height, width };
}
struct fontpack load_fonts() {
fontpack f = { TTF_OpenFont("C:\\Users\\alistair\\source\\repos\\Project1\\x64\\Debug\\unifont.ttf", 32),
TTF_OpenFont("C:\\Users\\alistair\\source\\repos\\Project1\\x64\\Debug\\unifont.ttf", 32) };
if (!f.messages || !f.usernames) {
std::cerr << "Failed to load font." << std::endl;
exit(4);
}
return f;
}
void process_messagequeue(int sock) {
struct msg message;
while (!message_queue.empty()) {
message = message_queue.back();
if (message.cmd == ircommand::PRIVMSG) {
message_display_queue.push_back(message);
} else if (message.cmd == ircommand::PING) {
std::cout << "> " << message.complete_message << std::endl;
sendretry(sock, "PONG :tmi.twitch.tv\r\n");
}
message_queue.pop_back();
}
}
int main(int argc, char *argv[]) {
std::string server = "irc.chat.twitch.tv";
std::string port = "6667";
std::string nick;
std::string oauth_token;
std::string channel;
int maxfps = 8;
cxxopts::Options options("tcsdl", "twitch chat streamer");
options.add_options()
("n,nick", "twitch username (all lowercase)", cxxopts::value<std::string>())
("t,token", "twitch oauth token 'oauth:xxxxxxxxxxx....'", cxxopts::value<std::string>())
("f,fps", "FPS limit", cxxopts::value<int>())
("c,channel", "channel to connect to '#channelname'", cxxopts::value<std::string>())
("s,server", "irc server address (irc.chat.twitch.tv)", cxxopts::value<std::string>())
("p,port", "irc server port (6667)", cxxopts::value<std::string>())
("b,background", "chat background colour (css format)", cxxopts::value<std::string>())
("F,foregreound", "text colour (css format)", cxxopts::value<std::string>())
("h,help", "Print help")
;
auto result = options.parse(argc, argv);
if (result.count("help")) {
std::cout << options.help() << std::endl;
exit(0);
}
if (result.count("nick")) {
nick = result["nick"].as<std::string>();
} else {
std::cout << "Must specify a nick." << std::endl << options.help();
exit(1);
}
if (result.count("token")) {
oauth_token = result["token"].as<std::string>();
}
else {
std::cout << "Must specify oauth token." << std::endl << options.help();
exit(1);
}
if (result.count("channel")) {
auto chan = result["channel"].as<std::string>();
if (chan.at(0) != '#') {
channel = "#" + chan;
} else {
channel = chan;
}
}
if (result.count("fps")) {
maxfps = result["fps"].as<int>();
}
drawinfo info {};
info.drawcontext = create_window(640, 480, "Twitch chat " + channel);
info.fonts = load_fonts();
info.colours.background = { 230, 230, 230, 255 };
info.colours.messages = { 255, 255, 255, 255 };
if (result.count("background")) {
auto bg = CSSColorParser::parse(result["background"].as<std::string>());
if (bg) {
info.colours.background = { bg->r, bg->g, bg->b, 255 };
}
}
if (result.count("foregreound")) {
auto bg = CSSColorParser::parse(result["foreground"].as<std::string>());
if (bg) {
info.colours.messages = { bg->r, bg->g, bg->b, 255 };
}
}
SDL_Event event;
int sock = connect_to_server(server, port);
irclogin(sock, oauth_token, nick, channel);
long int prev = SDL_GetTicks();
long int now;
bool new_message = false;
while (true) {
new_message = false;
// cap framerate
now = SDL_GetTicks();
int delay = 1000 / maxfps - now + prev;
if (delay > 0) SDL_Delay(delay);
prev = now;
auto resp = readresponsenonblock(sock);
if (resp) {
new_message = true;
parse_chat_messages(*resp, message_queue);
process_messagequeue(sock);
}
while (SDL_PollEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
return 0;
case SDL_WINDOWEVENT:
switch (event.window.event) {
case SDL_WINDOWEVENT_RESIZED:
case SDL_WINDOWEVENT_MAXIMIZED:
case SDL_WINDOWEVENT_RESTORED:
case SDL_WINDOWEVENT_SIZE_CHANGED:
info.drawcontext.width = event.window.data1;
info.drawcontext.height = event.window.data2;
new_message = true; // trigger redraw
break;
default:
break;
}
break;
default:
;
}
}
if (new_message) {
draw(info, message_display_queue);
}
}
return 0;
}