twitch_webhook/src/main.zig

414 lines
12 KiB
Zig
Raw Normal View History

2022-01-06 07:38:20 +00:00
const std = @import("std");
const json = std.json;
const twitch = @import("twitch.zig");
const Client = @import("client.zig").Client;
const webhook = @import("webhook.zig");
2022-01-08 14:09:44 +00:00
const sqlite = @import("sqlite.zig");
2022-01-06 07:38:20 +00:00
const DATABASE_VERSION_CODE = 2;
2022-01-08 16:13:41 +00:00
2022-01-08 14:58:10 +00:00
const CREATE_TABLES =
\\ CREATE TABLE VERSION
\\ (
\\ versionCode INTEGER
\\ );
\\
\\ INSERT INTO VERSION(versionCode)
\\ VALUES (2);
2022-01-08 14:58:10 +00:00
\\
\\ CREATE TABLE STREAMER
\\ (
\\ idStreamer TEXT PRIMARY KEY NOT NULL,
\\ loginStreamer TEXT NOT NULL,
\\ nameStreamer TEXT NOT NULL,
\\ imageUrlStreamer TEXT
2022-01-08 14:58:10 +00:00
\\ );
\\
\\ 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;
;
2022-01-06 07:38:20 +00:00
const Config = struct {
token: []const u8,
client_id: []const u8,
refresh_rate: u64,
2022-01-06 07:38:20 +00:00
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);
var stream = json.TokenStream.init(file_buffer);
return json.parse(@This(), &stream, .{ .allocator = allocator });
}
};
const User = struct { user_login: []u8 };
2022-01-06 07:38:20 +00:00
pub fn main() anyerror!void {
2022-01-08 14:58:10 +00:00
var db = try sqlite.Database.open("data.db");
2022-01-08 14:09:44 +00:00
defer {
_ = db.close() catch |e| {
std.log.err("Failed to close db : {}", .{e});
};
}
2022-01-08 16:13:41 +00:00
try createTables(&db);
2022-01-08 14:58:10 +00:00
//var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
var general_purpose_allocator = std.heap.GeneralPurposeAllocator(.{}){};
var allocator = general_purpose_allocator.allocator();
2022-01-06 07:38:20 +00:00
var config = try Config.fromFile(allocator, "config.json");
try Client.globalInit();
var client = try Client.init(&allocator) orelse error.FailedInitClient;
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);
while (true) {
var alertAllocator = std.heap.ArenaAllocator.init(allocator);
try updateAlert(alertAllocator.allocator(), &client, &config, &db, &headers);
alertAllocator.deinit();
std.log.info("waiting for {} ns", .{config.refresh_rate * std.time.ns_per_ms});
std.time.sleep(config.refresh_rate * std.time.ns_per_ms);
}
2022-01-06 07:38:20 +00:00
client.deinit();
try Client.cleanup();
if (general_purpose_allocator.deinit()) {
std.log.err("leaked bytes", .{});
}
2022-01-06 07:38:20 +00:00
}
2022-01-08 16:13:41 +00:00
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();
2022-01-08 16:13:41 +00:00
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);
2022-01-08 16:13:41 +00:00
}
}
stm.finalize();
}
pub fn updateAlert(
allocator: std.mem.Allocator,
client: *Client,
config: *Config,
database: *sqlite.Database,
headers: *std.StringHashMap([]const u8),
) anyerror!void {
2022-01-06 07:38:20 +00:00
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,
);
2022-01-06 07:38:20 +00:00
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| {
try embeds.append(e);
2022-01-06 07:38:20 +00:00
}
}
if (embeds.items.len > 0) {
_ = try client.postJSON(config.webhook_url, webhook.Webhook{
.username = "Twitch",
.content = "Live alert",
.embeds = embeds.items,
}, null);
embeds.deinit();
}
}
}
const VIEWER_COUNT_NAME = "Viewer count";
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});
2022-01-06 07:38:20 +00:00
try fields.append(.{
.name = VIEWER_COUNT_NAME,
.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;
2022-01-06 07:38:20 +00:00
}
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", .{});
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();
2022-01-06 07:38:20 +00:00
}
}