commit f6cf0c6dc62b8cca017bb12c7e673317d7e6dc37 Author: LordMZTE Date: Mon Feb 6 00:28:41 2023 +0100 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c80a22 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +zig-* diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..8860ad4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "deps/ws"] + path = deps/ws + url = https://github.com/nikneym/ws.git diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..b8e9854 --- /dev/null +++ b/build.zig @@ -0,0 +1,43 @@ +const std = @import("std"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const exe = b.addExecutable(.{ + .name = "ntfylisten", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + const zuri_mod = b.createModule(.{ + .source_file = .{ .path = "deps/ws/lib/zuri/src/zuri.zig" }, + }); + + exe.addAnonymousModule("ws", .{ + .source_file = .{ .path = "deps/ws/src/main.zig" }, + .dependencies = &.{.{ .name = "zuri", .module = zuri_mod }}, + }); + + // need libnotify, so might as well link glib and use glib logging + exe.linkLibC(); + exe.linkSystemLibrary("glib-2.0"); + + // needed for libnotify + exe.linkSystemLibrary("gdk-pixbuf-2.0"); + exe.linkSystemLibrary("notify"); + + exe.install(); + + const run_cmd = exe.run(); + + run_cmd.step.dependOn(b.getInstallStep()); + + if (b.args) |args| { + run_cmd.addArgs(args); + } + + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); +} diff --git a/deps/ws b/deps/ws new file mode 160000 index 0000000..d11513b --- /dev/null +++ b/deps/ws @@ -0,0 +1 @@ +Subproject commit d11513b70fa7b062bfa327af3377436a86d05b89 diff --git a/src/Message.zig b/src/Message.zig new file mode 100644 index 0000000..e095008 --- /dev/null +++ b/src/Message.zig @@ -0,0 +1,4 @@ +event: []const u8, +message: ?[:0]const u8 = null, +title: ?[]const u8 = null, +priority: u8 = 4, diff --git a/src/ffi.zig b/src/ffi.zig new file mode 100644 index 0000000..8f1b380 --- /dev/null +++ b/src/ffi.zig @@ -0,0 +1,4 @@ +pub const c = @cImport({ + @cInclude("libnotify/notify.h"); + @cInclude("glib.h"); +}); diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..8e0c193 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const ws = @import("ws"); +const c = @import("ffi.zig").c; + +const log = std.log.scoped(.ntfylisten); + +const Message = @import("Message.zig"); + +pub const std_options = struct { + // level filtering handled by glib + pub const log_level = .debug; + + pub fn logFn( + comptime message_level: std.log.Level, + comptime scope: @TypeOf(.enum_literal), + comptime format: []const u8, + args: anytype, + ) void { + var printbuf: [1024 * 4]u8 = undefined; + const msg = std.fmt.bufPrint(&printbuf, format, args) catch return; + + const level = switch (message_level) { + .debug => c.G_LOG_LEVEL_DEBUG, + .info => c.G_LOG_LEVEL_INFO, + .warn => c.G_LOG_LEVEL_WARNING, + .err => c.G_LOG_LEVEL_CRITICAL, + }; + + const domain = @tagName(scope); + + const fields = [_]c.GLogField{ + .{ + .key = "GLIB_DOMAIN", + .value = domain, + .length = @intCast(c.gssize, domain.len), + }, + .{ + .key = "MESSAGE", + .value = msg.ptr, + .length = @intCast(c.gssize, msg.len), + }, + }; + + c.g_log_structured_array(level, &fields, fields.len); + } +}; + +pub fn main() !void { + if (std.os.argv.len != 4) { + log.err( + \\Expected 3 arguments! + \\Usage: + \\ {s} [server] [topic] [title_prefix] + , + .{std.os.argv[0]}, + ); + std.os.exit(1); + } + + const server = std.mem.span(std.os.argv[1]); + const topic = std.mem.span(std.os.argv[2]); + const title_prefix = std.mem.span(std.os.argv[3]); + + if (c.notify_init("ntfylisten") == 0) + return error.LibnotifyInit; + + const addr = try std.fmt.allocPrint(std.heap.c_allocator, "ws://{s}/{s}/ws", .{ server, topic }); + defer std.heap.c_allocator.free(addr); + + log.info("connecting to WebSocket URL {s}", .{addr}); + var ws_client = try ws.connect(std.heap.c_allocator, addr, &.{ + .{ "Host", server }, + }); + defer ws_client.deinit(std.heap.c_allocator); + + while (true) { + var msg = try ws_client.receive(); + + switch (msg.type) { + .text, .binary => { + var ts = std.json.TokenStream.init(msg.data); + const data = std.json.parse( + Message, + &ts, + .{ .allocator = std.heap.c_allocator, .ignore_unknown_fields = true }, + ) catch |e| { + log.warn("invalid data from server: {}", .{e}); + continue; + }; + defer std.json.parseFree(Message, data, .{ .allocator = std.heap.c_allocator }); + + if (!std.mem.eql(u8, data.event, "message")) + continue; + + const text = data.message orelse continue; + + const title = if (data.title) |title| + try std.fmt.allocPrintZ(std.heap.c_allocator, "{s}: {s}", .{ title_prefix, title }) + else + try std.heap.c_allocator.dupeZ(u8, title_prefix); + + defer std.heap.c_allocator.free(title); + + const urgency = switch (std.math.order(data.priority, 3)) { + .lt => c.NOTIFY_URGENCY_LOW, + .eq => c.NOTIFY_URGENCY_NORMAL, + .gt => c.NOTIFY_URGENCY_CRITICAL, + }; + + const notif = c.notify_notification_new(title, text, null); + c.notify_notification_set_urgency(notif, @intCast(c_uint, urgency)); + _ = c.notify_notification_show(notif, null); + }, + + .ping => { + try ws_client.pong(); + }, + + .close => { + log.warn("server sent close message, exiting", .{}); + break; + }, + + else => {}, + } + } + + try ws_client.close(); +}