Fix memory leaks and better structure
This commit is contained in:
parent
d5175d9f19
commit
ed7a9b8eb4
|
@ -0,0 +1,461 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
const json = std.json;
|
||||||
|
|
||||||
|
const sqlite = @import("sqlite.zig");
|
||||||
|
const twitch = @import("twitch.zig");
|
||||||
|
const Client = @import("client.zig").Client;
|
||||||
|
const webhook = @import("webhook.zig");
|
||||||
|
|
||||||
|
const Config = struct {
|
||||||
|
token: []const u8,
|
||||||
|
client_id: []const u8,
|
||||||
|
refresh_rate: u64,
|
||||||
|
user_logins: []const User,
|
||||||
|
webhook_url: []const u8,
|
||||||
|
|
||||||
|
pub fn fromFile(allocator: std.mem.Allocator, path: []const u8) anyerror!@This() {
|
||||||
|
var file = try std.fs.cwd().openFile(path, .{
|
||||||
|
.read = true,
|
||||||
|
.write = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
var stat = try file.stat();
|
||||||
|
const file_buffer = try allocator.alloc(u8, stat.size);
|
||||||
|
_ = try file.readAll(file_buffer);
|
||||||
|
file.close();
|
||||||
|
|
||||||
|
var stream = json.TokenStream.init(file_buffer);
|
||||||
|
|
||||||
|
const res = json.parse(@This(), &stream, .{ .allocator = allocator });
|
||||||
|
allocator.free(file_buffer);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *@This(), allocator: mem.Allocator) void {
|
||||||
|
allocator.free(self.token);
|
||||||
|
allocator.free(self.client_id);
|
||||||
|
|
||||||
|
for (self.user_logins) |user| {
|
||||||
|
allocator.free(user.user_login);
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator.free(self.user_logins);
|
||||||
|
|
||||||
|
allocator.free(self.webhook_url);
|
||||||
|
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const User = struct { user_login: []u8 };
|
||||||
|
|
||||||
|
const DATABASE_VERSION_CODE = 2;
|
||||||
|
|
||||||
|
const CREATE_TABLES =
|
||||||
|
\\ CREATE TABLE VERSION
|
||||||
|
\\ (
|
||||||
|
\\ versionCode INTEGER
|
||||||
|
\\ );
|
||||||
|
\\
|
||||||
|
\\ INSERT INTO VERSION(versionCode)
|
||||||
|
\\ VALUES (2);
|
||||||
|
\\
|
||||||
|
\\ CREATE TABLE STREAMER
|
||||||
|
\\ (
|
||||||
|
\\ idStreamer TEXT PRIMARY KEY NOT NULL,
|
||||||
|
\\ loginStreamer TEXT NOT NULL,
|
||||||
|
\\ nameStreamer TEXT NOT NULL,
|
||||||
|
\\ imageUrlStreamer TEXT
|
||||||
|
\\ );
|
||||||
|
\\
|
||||||
|
\\ CREATE TABLE STREAM
|
||||||
|
\\ (
|
||||||
|
\\ idStream TEXT PRIMARY KEY NOT NULL,
|
||||||
|
\\ idStreamer TEXT NOT NULL,
|
||||||
|
\\ isMatureStream BOOLEAN NOT NULL DEFAULT 'F',
|
||||||
|
\\ CONSTRAINT FK_STREAM_STREAMER_ID FOREIGN KEY (idStreamer) REFERENCES STREAMER (idStreamer)
|
||||||
|
\\ );
|
||||||
|
\\
|
||||||
|
\\ CREATE TABLE VIEWER_COUNT_STREAM
|
||||||
|
\\ (
|
||||||
|
\\ viewerCount INTEGER NOT NULL,
|
||||||
|
\\ dateViewerCount DATE NOT NULL,
|
||||||
|
\\ idStream TEXT NOT NULL,
|
||||||
|
\\ PRIMARY KEY (dateViewerCount, idStream),
|
||||||
|
\\ CONSTRAINT FK_VIEWER_COUNT_STREAM_ID FOREIGN KEY (idStream) REFERENCES STREAM (idStream)
|
||||||
|
\\ );
|
||||||
|
\\
|
||||||
|
\\ CREATE TABLE NAME_STREAM
|
||||||
|
\\ (
|
||||||
|
\\ nameStream TEXT NOT NULL,
|
||||||
|
\\ dateNameStream DATE NOT NULL,
|
||||||
|
\\ idStream TEXT NOT NULL,
|
||||||
|
\\ PRIMARY KEY (dateNameStream, idStream),
|
||||||
|
\\ CONSTRAINT FK_NAME_STREAM_STREAM_ID FOREIGN KEY (idStream) REFERENCES STREAM (idStream)
|
||||||
|
\\ );
|
||||||
|
\\
|
||||||
|
\\ CREATE TABLE GAME
|
||||||
|
\\ (
|
||||||
|
\\ gameId TEXT NOT NULL PRIMARY KEY,
|
||||||
|
\\ gameName TEXT
|
||||||
|
\\ );
|
||||||
|
\\
|
||||||
|
\\ CREATE TABLE IS_STREAMING_GAME
|
||||||
|
\\ (
|
||||||
|
\\ gameId TEXT NOT NULL,
|
||||||
|
\\ streamId TEXT NOT NULL,
|
||||||
|
\\ dateGameStream DATE NOT NULL,
|
||||||
|
\\ PRIMARY KEY (gameId, streamId, dateGameStream),
|
||||||
|
\\ CONSTRAINT FK_GAME_STREAM_GAME_ID FOREIGN KEY (gameId) REFERENCES GAME (gameId),
|
||||||
|
\\ CONSTRAINT FK_GAME_STREAM_STREAM_ID FOREIGN KEY (streamId) REFERENCES STREAM (idStream)
|
||||||
|
\\ );
|
||||||
|
;
|
||||||
|
|
||||||
|
const DROP_TABLES =
|
||||||
|
\\ DROP TABLE IF EXISTS VERSION;
|
||||||
|
\\ DROP TABLE IF EXISTS NAME_STREAM;
|
||||||
|
\\ DROP TABLE IF EXISTS VIEWER_COUNT_STREAM;
|
||||||
|
\\ DROP TABLE IF EXISTS IS_STREAMING_GAME;
|
||||||
|
\\ DROP TABLE IF EXISTS STREAM;
|
||||||
|
\\ DROP TABLE IF EXISTS STREAMER;
|
||||||
|
\\ DROP TABLE IF EXISTS GAME;
|
||||||
|
;
|
||||||
|
|
||||||
|
allocator: mem.Allocator,
|
||||||
|
db: sqlite.Database,
|
||||||
|
headers: std.StringHashMap([]const u8),
|
||||||
|
wait_event: std.Thread.StaticResetEvent = std.Thread.StaticResetEvent{},
|
||||||
|
config: Config,
|
||||||
|
|
||||||
|
pub fn init(allocator: mem.Allocator) anyerror!@This() {
|
||||||
|
var db = try sqlite.Database.open("data.db");
|
||||||
|
|
||||||
|
try createTables(&db);
|
||||||
|
|
||||||
|
var config = try Config.fromFile(allocator, "config.json");
|
||||||
|
|
||||||
|
try Client.globalInit();
|
||||||
|
|
||||||
|
var client = try Client.init(allocator);
|
||||||
|
|
||||||
|
var headers = std.StringHashMap([]const u8).init(allocator);
|
||||||
|
try headers.put("Authorization", config.token);
|
||||||
|
try headers.put("Client-Id", config.client_id);
|
||||||
|
|
||||||
|
try insertOrReplaceStreamers(allocator, &db, &client, &config, &headers);
|
||||||
|
|
||||||
|
client.deinit();
|
||||||
|
|
||||||
|
return @This(){
|
||||||
|
.allocator = allocator,
|
||||||
|
.db = db,
|
||||||
|
.headers = headers,
|
||||||
|
.config = config,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *@This()) anyerror!void {
|
||||||
|
std.log.debug("deinit app", .{});
|
||||||
|
self.config.deinit(self.allocator);
|
||||||
|
|
||||||
|
self.headers.deinit();
|
||||||
|
try self.db.close();
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(self: *@This()) anyerror!void {
|
||||||
|
var loop = true;
|
||||||
|
while (loop) {
|
||||||
|
var alertAllocator = std.heap.ArenaAllocator.init(self.allocator);
|
||||||
|
var allocator = alertAllocator.allocator();
|
||||||
|
|
||||||
|
var client = try Client.init(allocator);
|
||||||
|
try updateAlert(allocator, &client, &self.config, &self.db, &self.headers);
|
||||||
|
alertAllocator.deinit();
|
||||||
|
client.deinit(); // TODO Maybe don't recreate client every loop, just change allocator
|
||||||
|
|
||||||
|
std.log.debug("sleeping for {} ns", .{self.config.refresh_rate * std.time.ns_per_ms});
|
||||||
|
const res = self.wait_event.timedWait(self.config.refresh_rate * std.time.ns_per_ms);
|
||||||
|
loop = res == .timed_out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stop(self: *@This()) void {
|
||||||
|
self.wait_event.set();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn createTables(db: *sqlite.Database) anyerror!void {
|
||||||
|
var stm = db.prepare("SELECT versionCode FROM VERSION ORDER BY versionCode DESC") catch {
|
||||||
|
std.log.debug("Creating database", .{});
|
||||||
|
|
||||||
|
try db.exec(CREATE_TABLES);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (stm.next()) {
|
||||||
|
var code: isize = 0;
|
||||||
|
|
||||||
|
try stm.fetch(.{&code});
|
||||||
|
stm.finalize();
|
||||||
|
|
||||||
|
if (DATABASE_VERSION_CODE == code) {
|
||||||
|
std.log.debug("Database already created", .{});
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
try db.exec(DROP_TABLES);
|
||||||
|
std.log.debug("Creating database", .{});
|
||||||
|
|
||||||
|
try db.exec(CREATE_TABLES);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stm.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn updateAlert(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
client: *Client,
|
||||||
|
config: *Config,
|
||||||
|
database: *sqlite.Database,
|
||||||
|
headers: *std.StringHashMap([]const u8),
|
||||||
|
) anyerror!void {
|
||||||
|
var request = std.ArrayList(u8).init(allocator);
|
||||||
|
|
||||||
|
try request.appendSlice("https://api.twitch.tv/helix/streams?");
|
||||||
|
|
||||||
|
{
|
||||||
|
var i: u8 = 0;
|
||||||
|
while (i < config.user_logins.len) : (i += 1) {
|
||||||
|
if (i != 0)
|
||||||
|
try request.append('&');
|
||||||
|
try request.appendSlice("user_login=");
|
||||||
|
try request.appendSlice(config.user_logins[i].user_login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try request.append(0);
|
||||||
|
|
||||||
|
var streams: twitch.TwitchRes([]const twitch.Stream) = try client.getJSON(
|
||||||
|
twitch.TwitchRes([]const twitch.Stream),
|
||||||
|
@ptrCast([*:0]const u8, request.items),
|
||||||
|
headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
request.deinit();
|
||||||
|
|
||||||
|
if (streams.data.len > 0) {
|
||||||
|
var embeds = std.ArrayList(webhook.Embed).init(allocator);
|
||||||
|
|
||||||
|
for (streams.data) |s| {
|
||||||
|
if (try appendEmbed(allocator, &s, database)) |e| {
|
||||||
|
std.log.debug("sending {s}", .{s.title});
|
||||||
|
try embeds.append(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (embeds.items.len > 0) {
|
||||||
|
var res = try client.postJSON(config.webhook_url, webhook.Webhook{
|
||||||
|
.username = "Twitch",
|
||||||
|
.content = "Live alert",
|
||||||
|
.embeds = embeds.items,
|
||||||
|
}, null);
|
||||||
|
|
||||||
|
client.allocator.free(res);
|
||||||
|
}
|
||||||
|
embeds.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
streams.deinit(allocator);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn appendEmbed(allocator: std.mem.Allocator, stream: *const twitch.Stream, db: *sqlite.Database) anyerror!?webhook.Embed {
|
||||||
|
if (!try streamExist(db, stream.id)) {
|
||||||
|
try insertStream(db, stream);
|
||||||
|
try insertMetadatas(db, stream);
|
||||||
|
|
||||||
|
var fields = std.ArrayList(webhook.Field).init(allocator); // TODO BETTER WAY
|
||||||
|
var viewer = std.ArrayList(u8).init(allocator);
|
||||||
|
try std.fmt.format(viewer.writer(), "{}", .{stream.viewer_count});
|
||||||
|
|
||||||
|
try fields.append(.{
|
||||||
|
.name = "Viewer count",
|
||||||
|
.value = viewer.toOwnedSlice(),
|
||||||
|
.@"inline" = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
try fields.append(.{
|
||||||
|
.name = "Game name",
|
||||||
|
.value = stream.game_name,
|
||||||
|
.@"inline" = true,
|
||||||
|
});
|
||||||
|
|
||||||
|
var thumbnail = try std.mem.replaceOwned(u8, allocator, stream.thumbnail_url, "{width}", "1920");
|
||||||
|
thumbnail = try std.mem.replaceOwned(u8, allocator, thumbnail, "{height}", "1080");
|
||||||
|
|
||||||
|
var stream_url = std.ArrayList(u8).init(allocator);
|
||||||
|
_ = try stream_url.appendSlice("https://twitch.tv/");
|
||||||
|
_ = try stream_url.appendSlice(stream.user_login);
|
||||||
|
|
||||||
|
var icon_url: ?[]u8 = undefined;
|
||||||
|
var stm = try db.prepare("SELECT imageUrlStreamer FROM STREAMER WHERE idStreamer = ?");
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(stream.user_id));
|
||||||
|
if (stm.next()) {
|
||||||
|
var res = sqlite.U8Array.text(undefined);
|
||||||
|
try stm.fetch(.{&res});
|
||||||
|
|
||||||
|
icon_url = try allocator.alloc(u8, res.text.len);
|
||||||
|
std.mem.copy(u8, icon_url.?, res.text);
|
||||||
|
stm.finalize();
|
||||||
|
} else {
|
||||||
|
icon_url = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return webhook.Embed{
|
||||||
|
.title = stream.title,
|
||||||
|
.image = .{
|
||||||
|
.url = thumbnail,
|
||||||
|
},
|
||||||
|
.author = .{
|
||||||
|
.name = stream.user_name,
|
||||||
|
.url = stream_url.items,
|
||||||
|
.icon_url = icon_url,
|
||||||
|
},
|
||||||
|
.color = 0xa970ff,
|
||||||
|
.fields = fields.toOwnedSlice(),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
try insertMetadatas(db, stream);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn streamExist(db: *sqlite.Database, streamId: []const u8) anyerror!bool {
|
||||||
|
var stm = try db.prepare("SELECT \"foo\" FROM STREAM WHERE idStream = ?");
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(streamId));
|
||||||
|
|
||||||
|
const res = stm.next();
|
||||||
|
stm.finalize();
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insertStream(db: *sqlite.Database, stream: *const twitch.Stream) anyerror!void {
|
||||||
|
var stm = try db.prepare(
|
||||||
|
"INSERT INTO STREAM(idStream, idStreamer, isMatureStream) VALUES(?, ?, ?)",
|
||||||
|
);
|
||||||
|
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(stream.id));
|
||||||
|
try stm.bind(2, sqlite.U8Array.text(stream.user_id));
|
||||||
|
try stm.bind(3, @boolToInt(stream.is_mature));
|
||||||
|
|
||||||
|
try stm.exec();
|
||||||
|
stm.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn insertMetadatas(db: *sqlite.Database, stream: *const twitch.Stream) anyerror!void {
|
||||||
|
var stm = try db.prepare(
|
||||||
|
"INSERT INTO VIEWER_COUNT_STREAM(viewerCount, dateViewerCount, idStream) VALUES(?, datetime(\"now\"), ?)",
|
||||||
|
);
|
||||||
|
|
||||||
|
try stm.bind(1, stream.viewer_count);
|
||||||
|
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
||||||
|
|
||||||
|
try stm.exec();
|
||||||
|
stm.finalize();
|
||||||
|
|
||||||
|
if (try mustInsertName(db, stream)) {
|
||||||
|
std.log.debug("inserting name : {s} ({s})", .{ stream.title, stream.id });
|
||||||
|
stm = try db.prepare(
|
||||||
|
"INSERT INTO NAME_STREAM(nameStream, dateNameStream, idStream) VALUES(?, datetime(\"now\"), ?)",
|
||||||
|
);
|
||||||
|
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(stream.title));
|
||||||
|
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
||||||
|
|
||||||
|
try stm.exec();
|
||||||
|
stm.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
stm = try db.prepare(
|
||||||
|
"INSERT OR IGNORE INTO GAME(gameId, gameName) VALUES(?, ?)",
|
||||||
|
);
|
||||||
|
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(stream.game_id));
|
||||||
|
try stm.bind(2, sqlite.U8Array.text(stream.game_name));
|
||||||
|
|
||||||
|
try stm.exec();
|
||||||
|
stm.finalize();
|
||||||
|
|
||||||
|
stm = try db.prepare(
|
||||||
|
"INSERT INTO IS_STREAMING_GAME(gameId, streamId, dateGameStream) VALUES(?, ?, datetime(\"now\"))",
|
||||||
|
);
|
||||||
|
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(stream.game_id));
|
||||||
|
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
||||||
|
|
||||||
|
try stm.exec();
|
||||||
|
stm.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mustInsertName(db: *sqlite.Database, stream: *const twitch.Stream) anyerror!bool {
|
||||||
|
var stm = try db.prepare(
|
||||||
|
"SELECT nameStream != ? FROM NAME_STREAM WHERE idStream = ? ORDER BY dateNameStream DESC LIMIT 1",
|
||||||
|
);
|
||||||
|
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(stream.title));
|
||||||
|
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
||||||
|
|
||||||
|
var res: c_int = 1;
|
||||||
|
if (stm.next()) {
|
||||||
|
try stm.fetch(.{&res});
|
||||||
|
}
|
||||||
|
stm.finalize();
|
||||||
|
return res == 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insertOrReplaceStreamers(
|
||||||
|
allocator: std.mem.Allocator,
|
||||||
|
db: *sqlite.Database,
|
||||||
|
client: *Client,
|
||||||
|
config: *const Config,
|
||||||
|
headers: *std.StringHashMap([]const u8),
|
||||||
|
) anyerror!void {
|
||||||
|
var request = std.ArrayList(u8).init(allocator);
|
||||||
|
|
||||||
|
try request.appendSlice("https://api.twitch.tv/helix/users?");
|
||||||
|
defer {
|
||||||
|
request.deinit();
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
var i: u8 = 0;
|
||||||
|
while (i < config.user_logins.len) : (i += 1) {
|
||||||
|
if (i != 0)
|
||||||
|
try request.append('&');
|
||||||
|
try request.appendSlice("login=");
|
||||||
|
try request.appendSlice(config.user_logins[i].user_login);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try request.append(0);
|
||||||
|
|
||||||
|
var streamers: twitch.TwitchRes([]const twitch.User) = try client.getJSON(
|
||||||
|
twitch.TwitchRes([]const twitch.User),
|
||||||
|
@ptrCast([*:0]const u8, request.items),
|
||||||
|
headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (streamers.data) |streamer| {
|
||||||
|
var stm = try db.prepare("INSERT OR REPLACE INTO STREAMER(idStreamer, loginStreamer, nameStreamer, imageUrlStreamer) VALUES(?, ?, ?, ?)");
|
||||||
|
try stm.bind(1, sqlite.U8Array.text(streamer.id));
|
||||||
|
try stm.bind(2, sqlite.U8Array.text(streamer.login));
|
||||||
|
try stm.bind(3, sqlite.U8Array.text(streamer.display_name));
|
||||||
|
try stm.bind(4, sqlite.U8Array.text(streamer.profile_image_url));
|
||||||
|
|
||||||
|
try stm.exec();
|
||||||
|
stm.finalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
streamers.deinit(allocator);
|
||||||
|
}
|
|
@ -14,15 +14,23 @@ const ArrayListReader = struct {
|
||||||
|
|
||||||
pub const Client = struct {
|
pub const Client = struct {
|
||||||
ptr: *cURL.CURL,
|
ptr: *cURL.CURL,
|
||||||
allocator: *mem.Allocator,
|
allocator: mem.Allocator,
|
||||||
|
|
||||||
pub fn init(allocator: *mem.Allocator) ?@This() {
|
pub fn init(allocator: mem.Allocator) anyerror!@This() {
|
||||||
const ptr = cURL.curl_easy_init() orelse return null;
|
const ptr = cURL.curl_easy_init() orelse return error.CURLEasyInitFailed;
|
||||||
|
|
||||||
return @This(){
|
var self = @This(){
|
||||||
.ptr = ptr,
|
.ptr = ptr,
|
||||||
.allocator = allocator,
|
.allocator = allocator,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
try self.restoreDefaultSettings();
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *@This()) void {
|
||||||
|
cURL.curl_easy_cleanup(self.ptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn globalInit() anyerror!void {
|
pub fn globalInit() anyerror!void {
|
||||||
|
@ -34,12 +42,7 @@ pub const Client = struct {
|
||||||
cURL.curl_global_cleanup();
|
cURL.curl_global_cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn getJSON(self: *@This(), comptime T: type, url: [*:0]const u8, headers: ?*std.StringHashMap([]const u8)) anyerror!T {
|
pub fn restoreDefaultSettings(self: *@This()) anyerror!void {
|
||||||
var response_buffer = std.ArrayList(u8).init(self.allocator.*);
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_URL, url) != cURL.CURLE_OK)
|
|
||||||
return error.CURLPerformFailed;
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_HTTPGET, @as(c_long, 1)) != cURL.CURLE_OK)
|
|
||||||
return error.CURLPerformFailed;
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_NOPROGRESS, @as(c_long, 1)) != cURL.CURLE_OK)
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_NOPROGRESS, @as(c_long, 1)) != cURL.CURLE_OK)
|
||||||
return error.CURLPerformFailed;
|
return error.CURLPerformFailed;
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_MAXREDIRS, @as(c_long, 50)) != cURL.CURLE_OK)
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_MAXREDIRS, @as(c_long, 50)) != cURL.CURLE_OK)
|
||||||
|
@ -47,6 +50,20 @@ pub const Client = struct {
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_TCP_KEEPALIVE, @as(c_long, 1)) != cURL.CURLE_OK)
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_TCP_KEEPALIVE, @as(c_long, 1)) != cURL.CURLE_OK)
|
||||||
return error.CURLPerformFailed;
|
return error.CURLPerformFailed;
|
||||||
|
|
||||||
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_WRITEFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK)
|
||||||
|
return error.CURLSetOptFailed;
|
||||||
|
|
||||||
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_READFUNCTION, readFromArrayListCallback) != cURL.CURLE_OK)
|
||||||
|
return error.CURLPerformFailed;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn getJSON(self: *@This(), comptime T: type, url: [*:0]const u8, headers: ?*std.StringHashMap([]const u8)) anyerror!T {
|
||||||
|
var response_buffer = std.ArrayList(u8).init(self.allocator);
|
||||||
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_URL, url) != cURL.CURLE_OK)
|
||||||
|
return error.CURLPerformFailed;
|
||||||
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_HTTPGET, @as(c_long, 1)) != cURL.CURLE_OK)
|
||||||
|
return error.CURLPerformFailed;
|
||||||
|
|
||||||
var header_slist: [*c]cURL.curl_slist = null;
|
var header_slist: [*c]cURL.curl_slist = null;
|
||||||
|
|
||||||
if (headers) |header| {
|
if (headers) |header| {
|
||||||
|
@ -69,9 +86,6 @@ pub const Client = struct {
|
||||||
return error.CURLSetOptFailed;
|
return error.CURLSetOptFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_WRITEFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK)
|
|
||||||
return error.CURLSetOptFailed;
|
|
||||||
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_WRITEDATA, &response_buffer) != cURL.CURLE_OK)
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_WRITEDATA, &response_buffer) != cURL.CURLE_OK)
|
||||||
return error.CURLPerformFailed;
|
return error.CURLPerformFailed;
|
||||||
|
|
||||||
|
@ -81,10 +95,11 @@ pub const Client = struct {
|
||||||
if (header_slist != null)
|
if (header_slist != null)
|
||||||
cURL.curl_slist_free_all(header_slist);
|
cURL.curl_slist_free_all(header_slist);
|
||||||
|
|
||||||
var stream = json.TokenStream.init(response_buffer.toOwnedSlice());
|
var content = response_buffer.items;
|
||||||
|
var stream = json.TokenStream.init(content);
|
||||||
|
|
||||||
@setEvalBranchQuota(10_000);
|
@setEvalBranchQuota(10_000);
|
||||||
const res = json.parse(T, &stream, .{ .allocator = self.allocator.*, .ignore_unknown_fields = true });
|
const res = json.parse(T, &stream, .{ .allocator = self.allocator, .ignore_unknown_fields = true });
|
||||||
|
|
||||||
response_buffer.deinit();
|
response_buffer.deinit();
|
||||||
|
|
||||||
|
@ -92,22 +107,14 @@ pub const Client = struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn postJSON(self: *@This(), url: []const u8, data: anytype, headers: ?std.StringHashMap([]const u8)) anyerror![]const u8 {
|
pub fn postJSON(self: *@This(), url: []const u8, data: anytype, headers: ?std.StringHashMap([]const u8)) anyerror![]const u8 {
|
||||||
var post_buffer = std.ArrayList(u8).init(self.allocator.*);
|
var post_buffer = std.ArrayList(u8).init(self.allocator);
|
||||||
var response_buffer = std.ArrayList(u8).init(self.allocator.*);
|
var response_buffer = std.ArrayList(u8).init(self.allocator);
|
||||||
|
|
||||||
var rawUrl = try self.allocator.allocSentinel(u8, url.len, 0);
|
var rawUrl = try self.allocator.allocSentinel(u8, url.len, 0);
|
||||||
std.mem.copy(u8, rawUrl, url);
|
std.mem.copy(u8, rawUrl, url);
|
||||||
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_URL, rawUrl.ptr) != cURL.CURLE_OK)
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_URL, rawUrl.ptr) != cURL.CURLE_OK)
|
||||||
return error.CURLPerformFailed;
|
return error.CURLPerformFailed;
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_NOPROGRESS, @as(c_long, 1)) != cURL.CURLE_OK)
|
|
||||||
return error.CURLPerformFailed;
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_MAXREDIRS, @as(c_long, 50)) != cURL.CURLE_OK)
|
|
||||||
return error.CURLPerformFailed;
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_TCP_KEEPALIVE, @as(c_long, 1)) != cURL.CURLE_OK)
|
|
||||||
return error.CURLPerformFailed;
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_TCP_KEEPALIVE, @as(c_long, 1)) != cURL.CURLE_OK)
|
|
||||||
return error.CURLPerformFailed;
|
|
||||||
|
|
||||||
var header_slist: [*c]cURL.curl_slist = null;
|
var header_slist: [*c]cURL.curl_slist = null;
|
||||||
|
|
||||||
|
@ -136,9 +143,6 @@ pub const Client = struct {
|
||||||
}) != cURL.CURLE_OK)
|
}) != cURL.CURLE_OK)
|
||||||
return error.CURLPerformFailed;
|
return error.CURLPerformFailed;
|
||||||
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_READFUNCTION, readFromArrayListCallback) != cURL.CURLE_OK)
|
|
||||||
return error.CURLPerformFailed;
|
|
||||||
|
|
||||||
if (header_slist != null) {
|
if (header_slist != null) {
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_HTTPHEADER, header_slist) != cURL.CURLE_OK)
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_HTTPHEADER, header_slist) != cURL.CURLE_OK)
|
||||||
return error.CURLSetOptFailed;
|
return error.CURLSetOptFailed;
|
||||||
|
@ -147,9 +151,6 @@ pub const Client = struct {
|
||||||
return error.CURLSetOptFailed;
|
return error.CURLSetOptFailed;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_WRITEFUNCTION, writeToArrayListCallback) != cURL.CURLE_OK)
|
|
||||||
return error.CURLSetOptFailed;
|
|
||||||
|
|
||||||
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_WRITEDATA, &response_buffer) != cURL.CURLE_OK)
|
if (cURL.curl_easy_setopt(self.ptr, cURL.CURLOPT_WRITEDATA, &response_buffer) != cURL.CURLE_OK)
|
||||||
return error.CURLPerformFailed;
|
return error.CURLPerformFailed;
|
||||||
|
|
||||||
|
@ -167,10 +168,6 @@ pub const Client = struct {
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *@This()) void {
|
|
||||||
cURL.curl_easy_cleanup(self.ptr);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn writeToArrayListCallback(data: *anyopaque, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint {
|
fn writeToArrayListCallback(data: *anyopaque, size: c_uint, nmemb: c_uint, user_data: *anyopaque) callconv(.C) c_uint {
|
||||||
|
|
418
src/main.zig
418
src/main.zig
|
@ -1,116 +1,13 @@
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const json = std.json;
|
const json = std.json;
|
||||||
|
|
||||||
const twitch = @import("twitch.zig");
|
|
||||||
const Client = @import("client.zig").Client;
|
const Client = @import("client.zig").Client;
|
||||||
const webhook = @import("webhook.zig");
|
const App = @import("app.zig");
|
||||||
const sqlite = @import("sqlite.zig");
|
|
||||||
|
|
||||||
const DATABASE_VERSION_CODE = 2;
|
pub var app: ?App = null;
|
||||||
|
|
||||||
const CREATE_TABLES =
|
|
||||||
\\ CREATE TABLE VERSION
|
|
||||||
\\ (
|
|
||||||
\\ versionCode INTEGER
|
|
||||||
\\ );
|
|
||||||
\\
|
|
||||||
\\ INSERT INTO VERSION(versionCode)
|
|
||||||
\\ VALUES (2);
|
|
||||||
\\
|
|
||||||
\\ CREATE TABLE STREAMER
|
|
||||||
\\ (
|
|
||||||
\\ idStreamer TEXT PRIMARY KEY NOT NULL,
|
|
||||||
\\ loginStreamer TEXT NOT NULL,
|
|
||||||
\\ nameStreamer TEXT NOT NULL,
|
|
||||||
\\ imageUrlStreamer TEXT
|
|
||||||
\\ );
|
|
||||||
\\
|
|
||||||
\\ CREATE TABLE STREAM
|
|
||||||
\\ (
|
|
||||||
\\ idStream TEXT PRIMARY KEY NOT NULL,
|
|
||||||
\\ idStreamer TEXT NOT NULL,
|
|
||||||
\\ isMatureStream BOOLEAN NOT NULL DEFAULT 'F',
|
|
||||||
\\ CONSTRAINT FK_STREAM_STREAMER_ID FOREIGN KEY (idStreamer) REFERENCES STREAMER (idStreamer)
|
|
||||||
\\ );
|
|
||||||
\\
|
|
||||||
\\ CREATE TABLE VIEWER_COUNT_STREAM
|
|
||||||
\\ (
|
|
||||||
\\ viewerCount INTEGER NOT NULL,
|
|
||||||
\\ dateViewerCount DATE NOT NULL,
|
|
||||||
\\ idStream TEXT NOT NULL,
|
|
||||||
\\ PRIMARY KEY (dateViewerCount, idStream),
|
|
||||||
\\ CONSTRAINT FK_VIEWER_COUNT_STREAM_ID FOREIGN KEY (idStream) REFERENCES STREAM (idStream)
|
|
||||||
\\ );
|
|
||||||
\\
|
|
||||||
\\ CREATE TABLE NAME_STREAM
|
|
||||||
\\ (
|
|
||||||
\\ nameStream TEXT NOT NULL,
|
|
||||||
\\ dateNameStream DATE NOT NULL,
|
|
||||||
\\ idStream TEXT NOT NULL,
|
|
||||||
\\ PRIMARY KEY (dateNameStream, idStream),
|
|
||||||
\\ CONSTRAINT FK_NAME_STREAM_STREAM_ID FOREIGN KEY (idStream) REFERENCES STREAM (idStream)
|
|
||||||
\\ );
|
|
||||||
\\
|
|
||||||
\\ CREATE TABLE GAME
|
|
||||||
\\ (
|
|
||||||
\\ gameId TEXT NOT NULL PRIMARY KEY,
|
|
||||||
\\ gameName TEXT
|
|
||||||
\\ );
|
|
||||||
\\
|
|
||||||
\\ CREATE TABLE IS_STREAMING_GAME
|
|
||||||
\\ (
|
|
||||||
\\ gameId TEXT NOT NULL,
|
|
||||||
\\ streamId TEXT NOT NULL,
|
|
||||||
\\ dateGameStream DATE NOT NULL,
|
|
||||||
\\ PRIMARY KEY (gameId, streamId, dateGameStream),
|
|
||||||
\\ CONSTRAINT FK_GAME_STREAM_GAME_ID FOREIGN KEY (gameId) REFERENCES GAME (gameId),
|
|
||||||
\\ CONSTRAINT FK_GAME_STREAM_STREAM_ID FOREIGN KEY (streamId) REFERENCES STREAM (idStream)
|
|
||||||
\\ );
|
|
||||||
;
|
|
||||||
|
|
||||||
const DROP_TABLES =
|
|
||||||
\\ DROP TABLE IF EXISTS VERSION;
|
|
||||||
\\ DROP TABLE IF EXISTS NAME_STREAM;
|
|
||||||
\\ DROP TABLE IF EXISTS VIEWER_COUNT_STREAM;
|
|
||||||
\\ DROP TABLE IF EXISTS IS_STREAMING_GAME;
|
|
||||||
\\ DROP TABLE IF EXISTS STREAM;
|
|
||||||
\\ DROP TABLE IF EXISTS STREAMER;
|
|
||||||
\\ DROP TABLE IF EXISTS GAME;
|
|
||||||
;
|
|
||||||
|
|
||||||
const Config = struct {
|
|
||||||
token: []const u8,
|
|
||||||
client_id: []const u8,
|
|
||||||
refresh_rate: u64,
|
|
||||||
user_logins: []const User,
|
|
||||||
webhook_url: []const u8,
|
|
||||||
|
|
||||||
pub fn fromFile(allocator: std.mem.Allocator, path: []const u8) anyerror!@This() {
|
|
||||||
var file = try std.fs.cwd().openFile(path, .{
|
|
||||||
.read = true,
|
|
||||||
.write = false,
|
|
||||||
});
|
|
||||||
|
|
||||||
var stat = try file.stat();
|
|
||||||
const file_buffer = try allocator.alloc(u8, stat.size);
|
|
||||||
_ = try file.readAll(file_buffer);
|
|
||||||
file.close();
|
|
||||||
|
|
||||||
var stream = json.TokenStream.init(file_buffer);
|
|
||||||
|
|
||||||
const res = json.parse(@This(), &stream, .{ .allocator = allocator });
|
|
||||||
allocator.free(file_buffer);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const User = struct { user_login: []u8 };
|
|
||||||
|
|
||||||
var wait_event = std.Thread.StaticResetEvent{};
|
|
||||||
|
|
||||||
fn handler_fn(_: c_int) callconv(.C) void {
|
fn handler_fn(_: c_int) callconv(.C) void {
|
||||||
wait_event.set();
|
app.?.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn setupSigIntHandler() void {
|
fn setupSigIntHandler() void {
|
||||||
|
@ -128,318 +25,27 @@ fn setupSigIntHandler() void {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn main() anyerror!void {
|
pub fn main() anyerror!void {
|
||||||
var db = try sqlite.Database.open("data.db");
|
|
||||||
defer {
|
|
||||||
_ = db.close() catch |e| {
|
|
||||||
std.log.err("Failed to close db : {}", .{e});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
setupSigIntHandler();
|
setupSigIntHandler();
|
||||||
|
|
||||||
try createTables(&db);
|
|
||||||
|
|
||||||
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
|
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
|
||||||
|
|
||||||
var allocator = general_purpose_allocator.allocator();
|
var allocator = general_purpose_allocator.allocator();
|
||||||
|
|
||||||
var config = try Config.fromFile(allocator, "config.json");
|
app = try App.init(allocator);
|
||||||
|
|
||||||
try Client.globalInit();
|
app.?.run() catch |e| {
|
||||||
|
std.log.err("Error while running app: {}", .{e});
|
||||||
|
};
|
||||||
|
|
||||||
var client = try Client.init(&allocator) orelse error.FailedInitClient;
|
const stdout = std.io.getStdOut().writer();
|
||||||
|
stdout.print("stopping ...", .{}) catch {};
|
||||||
|
|
||||||
var headers = std.StringHashMap([]const u8).init(allocator);
|
_ = app.?.deinit() catch |e| {
|
||||||
try headers.put("Authorization", config.token);
|
std.log.err("Failed to deinit app: {}", .{e});
|
||||||
try headers.put("Client-Id", config.client_id);
|
};
|
||||||
|
|
||||||
try insertOrReplaceStreamers(allocator, &db, &client, &config, &headers);
|
|
||||||
|
|
||||||
var loop = true;
|
|
||||||
while (loop) {
|
|
||||||
var alertAllocator = std.heap.ArenaAllocator.init(allocator);
|
|
||||||
try updateAlert(alertAllocator.allocator(), &client, &config, &db, &headers);
|
|
||||||
alertAllocator.deinit();
|
|
||||||
|
|
||||||
std.log.info("sleeping for {} ns", .{config.refresh_rate * std.time.ns_per_ms});
|
|
||||||
const res = wait_event.timedWait(config.refresh_rate * std.time.ns_per_ms);
|
|
||||||
loop = res == .timed_out;
|
|
||||||
}
|
|
||||||
|
|
||||||
std.log.info("stopping ...", .{});
|
|
||||||
|
|
||||||
headers.deinit();
|
|
||||||
client.deinit();
|
|
||||||
|
|
||||||
try Client.cleanup();
|
try Client.cleanup();
|
||||||
if (general_purpose_allocator.deinit()) {
|
if (general_purpose_allocator.deinit()) {
|
||||||
std.log.err("leaked bytes", .{});
|
std.log.err("leaked bytes", .{});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn createTables(db: *sqlite.Database) anyerror!void {
|
|
||||||
var stm = db.prepare("SELECT versionCode FROM VERSION ORDER BY versionCode DESC") catch {
|
|
||||||
std.log.debug("Creating database", .{});
|
|
||||||
|
|
||||||
try db.exec(CREATE_TABLES);
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (stm.next()) {
|
|
||||||
var code: isize = 0;
|
|
||||||
|
|
||||||
try stm.fetch(.{&code});
|
|
||||||
stm.finalize();
|
|
||||||
|
|
||||||
if (DATABASE_VERSION_CODE == code) {
|
|
||||||
std.log.debug("Database already created", .{});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
try db.exec(DROP_TABLES);
|
|
||||||
std.log.debug("Creating database", .{});
|
|
||||||
|
|
||||||
try db.exec(CREATE_TABLES);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
stm.finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn updateAlert(
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
client: *Client,
|
|
||||||
config: *Config,
|
|
||||||
database: *sqlite.Database,
|
|
||||||
headers: *std.StringHashMap([]const u8),
|
|
||||||
) anyerror!void {
|
|
||||||
var request = std.ArrayList(u8).init(allocator);
|
|
||||||
|
|
||||||
try request.appendSlice("https://api.twitch.tv/helix/streams?");
|
|
||||||
|
|
||||||
{
|
|
||||||
var i: u8 = 0;
|
|
||||||
while (i < config.user_logins.len) : (i += 1) {
|
|
||||||
if (i != 0)
|
|
||||||
try request.append('&');
|
|
||||||
try request.appendSlice("user_login=");
|
|
||||||
try request.appendSlice(config.user_logins[i].user_login);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try request.append(0);
|
|
||||||
|
|
||||||
const streams: twitch.TwitchRes([]const twitch.Stream) = try client.getJSON(
|
|
||||||
twitch.TwitchRes([]const twitch.Stream),
|
|
||||||
@ptrCast([*:0]const u8, request.items),
|
|
||||||
headers,
|
|
||||||
);
|
|
||||||
|
|
||||||
request.deinit();
|
|
||||||
|
|
||||||
if (streams.data.len > 0) {
|
|
||||||
var embeds = std.ArrayList(webhook.Embed).init(allocator);
|
|
||||||
|
|
||||||
for (streams.data) |s| {
|
|
||||||
if (try appendEmbed(allocator, &s, database)) |e| {
|
|
||||||
std.log.info("{s}", .{s.title});
|
|
||||||
try embeds.append(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (embeds.items.len > 0) {
|
|
||||||
var res = try client.postJSON(config.webhook_url, webhook.Webhook{
|
|
||||||
.username = "Twitch",
|
|
||||||
.content = "Live alert",
|
|
||||||
.embeds = embeds.items,
|
|
||||||
}, null);
|
|
||||||
|
|
||||||
client.allocator.free(res);
|
|
||||||
}
|
|
||||||
embeds.deinit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fn appendEmbed(allocator: std.mem.Allocator, stream: *const twitch.Stream, db: *sqlite.Database) anyerror!?webhook.Embed {
|
|
||||||
if (!try streamExist(db, stream.id)) {
|
|
||||||
try insertStream(db, stream);
|
|
||||||
try insertMetadatas(db, stream);
|
|
||||||
|
|
||||||
var fields = std.ArrayList(webhook.Field).init(allocator); // TODO BETTER WAY
|
|
||||||
var viewer = std.ArrayList(u8).init(allocator);
|
|
||||||
try std.fmt.format(viewer.writer(), "{}", .{stream.viewer_count});
|
|
||||||
|
|
||||||
try fields.append(.{
|
|
||||||
.name = "Viewer count",
|
|
||||||
.value = viewer.toOwnedSlice(),
|
|
||||||
.@"inline" = true,
|
|
||||||
});
|
|
||||||
try fields.append(.{
|
|
||||||
.name = "Game name",
|
|
||||||
.value = stream.game_name,
|
|
||||||
.@"inline" = true,
|
|
||||||
});
|
|
||||||
|
|
||||||
var thumbnail = try std.mem.replaceOwned(u8, allocator, stream.thumbnail_url, "{width}", "1920");
|
|
||||||
thumbnail = try std.mem.replaceOwned(u8, allocator, thumbnail, "{height}", "1080");
|
|
||||||
|
|
||||||
var stream_url = std.ArrayList(u8).init(allocator);
|
|
||||||
_ = try stream_url.appendSlice("https://twitch.tv/");
|
|
||||||
_ = try stream_url.appendSlice(stream.user_login);
|
|
||||||
|
|
||||||
var icon_url: ?[]u8 = undefined;
|
|
||||||
var stm = try db.prepare("SELECT imageUrlStreamer FROM STREAMER WHERE idStreamer = ?");
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(stream.user_id));
|
|
||||||
if (stm.next()) {
|
|
||||||
var res = sqlite.U8Array.text(undefined);
|
|
||||||
try stm.fetch(.{&res});
|
|
||||||
|
|
||||||
icon_url = try allocator.alloc(u8, res.text.len);
|
|
||||||
std.mem.copy(u8, icon_url.?, res.text);
|
|
||||||
stm.finalize();
|
|
||||||
} else {
|
|
||||||
icon_url = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return webhook.Embed{
|
|
||||||
.title = stream.title,
|
|
||||||
.image = .{
|
|
||||||
.url = thumbnail,
|
|
||||||
},
|
|
||||||
.author = .{
|
|
||||||
.name = stream.user_name,
|
|
||||||
.url = stream_url.items,
|
|
||||||
.icon_url = icon_url,
|
|
||||||
},
|
|
||||||
.color = 0xa970ff,
|
|
||||||
.fields = fields.toOwnedSlice(),
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
try insertMetadatas(db, stream);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn streamExist(db: *sqlite.Database, streamId: []const u8) anyerror!bool {
|
|
||||||
var stm = try db.prepare("SELECT \"foo\" FROM STREAM WHERE idStream = ?");
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(streamId));
|
|
||||||
|
|
||||||
const res = stm.next();
|
|
||||||
stm.finalize();
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insertStream(db: *sqlite.Database, stream: *const twitch.Stream) anyerror!void {
|
|
||||||
var stm = try db.prepare(
|
|
||||||
"INSERT INTO STREAM(idStream, idStreamer, isMatureStream) VALUES(?, ?, ?)",
|
|
||||||
);
|
|
||||||
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(stream.id));
|
|
||||||
try stm.bind(2, sqlite.U8Array.text(stream.user_id));
|
|
||||||
try stm.bind(3, @boolToInt(stream.is_mature));
|
|
||||||
|
|
||||||
try stm.exec();
|
|
||||||
stm.finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn insertMetadatas(db: *sqlite.Database, stream: *const twitch.Stream) anyerror!void {
|
|
||||||
var stm = try db.prepare(
|
|
||||||
"INSERT INTO VIEWER_COUNT_STREAM(viewerCount, dateViewerCount, idStream) VALUES(?, datetime(\"now\"), ?)",
|
|
||||||
);
|
|
||||||
|
|
||||||
try stm.bind(1, stream.viewer_count);
|
|
||||||
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
|
||||||
|
|
||||||
try stm.exec();
|
|
||||||
stm.finalize();
|
|
||||||
|
|
||||||
if (try mustInsertName(db, stream)) {
|
|
||||||
std.log.debug("inserting name : {s} ({s})", .{stream.title, stream.id});
|
|
||||||
stm = try db.prepare(
|
|
||||||
"INSERT INTO NAME_STREAM(nameStream, dateNameStream, idStream) VALUES(?, datetime(\"now\"), ?)",
|
|
||||||
);
|
|
||||||
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(stream.title));
|
|
||||||
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
|
||||||
|
|
||||||
try stm.exec();
|
|
||||||
stm.finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
stm = try db.prepare(
|
|
||||||
"INSERT OR IGNORE INTO GAME(gameId, gameName) VALUES(?, ?)",
|
|
||||||
);
|
|
||||||
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(stream.game_id));
|
|
||||||
try stm.bind(2, sqlite.U8Array.text(stream.game_name));
|
|
||||||
|
|
||||||
try stm.exec();
|
|
||||||
stm.finalize();
|
|
||||||
|
|
||||||
stm = try db.prepare(
|
|
||||||
"INSERT INTO IS_STREAMING_GAME(gameId, streamId, dateGameStream) VALUES(?, ?, datetime(\"now\"))",
|
|
||||||
);
|
|
||||||
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(stream.game_id));
|
|
||||||
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
|
||||||
|
|
||||||
try stm.exec();
|
|
||||||
stm.finalize();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn mustInsertName(db: *sqlite.Database, stream: *const twitch.Stream) anyerror!bool {
|
|
||||||
var stm = try db.prepare(
|
|
||||||
"SELECT nameStream != ? FROM NAME_STREAM WHERE idStream = ? ORDER BY dateNameStream DESC LIMIT 1",
|
|
||||||
);
|
|
||||||
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(stream.title));
|
|
||||||
try stm.bind(2, sqlite.U8Array.text(stream.id));
|
|
||||||
|
|
||||||
var res: c_int = 1;
|
|
||||||
if (stm.next()) {
|
|
||||||
try stm.fetch(.{&res});
|
|
||||||
}
|
|
||||||
stm.finalize();
|
|
||||||
return res == 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insertOrReplaceStreamers(
|
|
||||||
allocator: std.mem.Allocator,
|
|
||||||
db: *sqlite.Database,
|
|
||||||
client: *Client,
|
|
||||||
config: *const Config,
|
|
||||||
headers: *std.StringHashMap([]const u8),
|
|
||||||
) anyerror!void {
|
|
||||||
var request = std.ArrayList(u8).init(allocator);
|
|
||||||
|
|
||||||
try request.appendSlice("https://api.twitch.tv/helix/users?");
|
|
||||||
|
|
||||||
{
|
|
||||||
var i: u8 = 0;
|
|
||||||
while (i < config.user_logins.len) : (i += 1) {
|
|
||||||
if (i != 0)
|
|
||||||
try request.append('&');
|
|
||||||
try request.appendSlice("login=");
|
|
||||||
try request.appendSlice(config.user_logins[i].user_login);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try request.append(0);
|
|
||||||
|
|
||||||
const streamers: twitch.TwitchRes([]const twitch.User) = try client.getJSON(
|
|
||||||
twitch.TwitchRes([]const twitch.User),
|
|
||||||
@ptrCast([*:0]const u8, request.items),
|
|
||||||
headers,
|
|
||||||
);
|
|
||||||
|
|
||||||
for (streamers.data) |streamer| {
|
|
||||||
var stm = try db.prepare("INSERT OR REPLACE INTO STREAMER(idStreamer, loginStreamer, nameStreamer, imageUrlStreamer) VALUES(?, ?, ?, ?)");
|
|
||||||
try stm.bind(1, sqlite.U8Array.text(streamer.id));
|
|
||||||
try stm.bind(2, sqlite.U8Array.text(streamer.login));
|
|
||||||
try stm.bind(3, sqlite.U8Array.text(streamer.display_name));
|
|
||||||
try stm.bind(4, sqlite.U8Array.text(streamer.profile_image_url));
|
|
||||||
|
|
||||||
try stm.exec();
|
|
||||||
stm.finalize();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,3 +1,6 @@
|
||||||
|
const std = @import("std");
|
||||||
|
const mem = std.mem;
|
||||||
|
|
||||||
pub const Stream = struct {
|
pub const Stream = struct {
|
||||||
id: []const u8,
|
id: []const u8,
|
||||||
user_id: []const u8,
|
user_id: []const u8,
|
||||||
|
@ -25,5 +28,80 @@ pub const User = struct {
|
||||||
pub const Pagination = struct { cursor: ?[]u8 = null };
|
pub const Pagination = struct { cursor: ?[]u8 = null };
|
||||||
|
|
||||||
pub fn TwitchRes(comptime T: type) type {
|
pub fn TwitchRes(comptime T: type) type {
|
||||||
return struct { data: T, pagination: ?Pagination = null};
|
return struct {
|
||||||
|
data: T,
|
||||||
|
pagination: ?Pagination = null,
|
||||||
|
pub fn deinit(self: *@This(), allocator: mem.Allocator) void {
|
||||||
|
deinitVar(allocator, self.data);
|
||||||
|
if (self.pagination) |p| {
|
||||||
|
if (p.cursor) |cursor| {
|
||||||
|
allocator.free(cursor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.* = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inline fn deinitVar(allocator: mem.Allocator, value: anytype) void {
|
||||||
|
const T = @TypeOf(value);
|
||||||
|
|
||||||
|
if (comptime std.meta.trait.hasFn("deinit")(T)) {
|
||||||
|
value.deinit(); // TODO pass allocator if needed
|
||||||
|
} else {
|
||||||
|
switch (@typeInfo(T)) {
|
||||||
|
.Optional => {
|
||||||
|
if (value) |payload| {
|
||||||
|
deinitVar(allocator, payload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Union => {
|
||||||
|
const info = @typeInfo(T).Union;
|
||||||
|
if (info.tag_type) |UnionTagType| {
|
||||||
|
inline for (info.fields) |u_field| {
|
||||||
|
if (value == @field(UnionTagType, u_field.name)) {
|
||||||
|
deinitVar(allocator, @field(value, u_field.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Struct => |S| {
|
||||||
|
inline for (S.fields) |Field| {
|
||||||
|
deinitVar(allocator, @field(value, Field.name));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.Pointer => |ptr_info| switch (ptr_info.size) {
|
||||||
|
.One => switch (@typeInfo(ptr_info.child)) {
|
||||||
|
.Array => {
|
||||||
|
const Slice = []const std.meta.Elem(ptr_info.child);
|
||||||
|
return deinitVar(allocator, @as(Slice, value));
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
deinitVar(allocator, value.*);
|
||||||
|
allocator.destroy(value);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
.Slice => {
|
||||||
|
const elem = std.meta.Elem(T);
|
||||||
|
switch (@typeInfo(elem)) {
|
||||||
|
.Type, .Void, .Bool, .Int, .Float, .Enum => {
|
||||||
|
//AVOID USELESS LOOPING
|
||||||
|
},
|
||||||
|
else => {
|
||||||
|
var i: usize = 0;
|
||||||
|
while (i < value.len) : (i += 1) {
|
||||||
|
deinitVar(allocator, value[i]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
allocator.free(value);
|
||||||
|
},
|
||||||
|
else => @compileError("Unable to deinit type '" ++ @typeName(T) ++ "'"),
|
||||||
|
},
|
||||||
|
.Array => deinitVar(allocator, &value),
|
||||||
|
.Type, .Void, .Bool, .Int, .Float, .Enum => {},
|
||||||
|
else => @compileError("Unable to deinit type '" ++ @typeName(T) ++ "'"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue