summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Uko Kokņevičs2025-08-03 12:54:12 +0300
committerGravatar Uko Kokņevičs2025-08-03 12:54:12 +0300
commite5185f65051f881bf61e88542a1acd4957f8383b (patch)
treea030a8b32cd13ed6a7d9ed736f8fc626501c2749
parentMoved inline bot handling to a new file (diff)
downloadukkobot-e5185f65051f881bf61e88542a1acd4957f8383b.tar.gz
ukkobot-e5185f65051f881bf61e88542a1acd4957f8383b.tar.xz
ukkobot-e5185f65051f881bf61e88542a1acd4957f8383b.zip
Move bot configuration to SQL land
-rw-r--r--.gitignore7
-rw-r--r--config.default.json1
-rw-r--r--src/Bot.zig9
-rw-r--r--src/Config.zig1
-rw-r--r--src/DB.zig100
-rw-r--r--src/inline_bots.zig60
-rw-r--r--src/main.zig74
-rw-r--r--src/types.zig1
-rw-r--r--src/types/AnswerCallbackQueryParams.zig5
-rw-r--r--src/types/InlineKeyboardMarkup.zig2
-rw-r--r--src/types/SendMessageParams.zig4
11 files changed, 216 insertions, 48 deletions
diff --git a/.gitignore b/.gitignore
index 9ae398c..91006a3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
1.zig-cache/ 1/config.json
2zig-out/ 2/data.db
3config.json \ No newline at end of file 3/.zig-cache/
4/zig-out/
diff --git a/config.default.json b/config.default.json
index 51cdda6..c17bf09 100644
--- a/config.default.json
+++ b/config.default.json
@@ -2,6 +2,7 @@
2 "_": "Copy this file to `config.json` and modify it there", 2 "_": "Copy this file to `config.json` and modify it there",
3 3
4 "bot_token": "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi", 4 "bot_token": "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi",
5 "db_path": "./data.db",
5 "dev_group": -1001234567890, 6 "dev_group": -1001234567890,
6 "owner": 12345678 7 "owner": 12345678
7} 8}
diff --git a/src/Bot.zig b/src/Bot.zig
index b0eb972..fb91b3f 100644
--- a/src/Bot.zig
+++ b/src/Bot.zig
@@ -6,6 +6,7 @@ const Allocator = std.mem.Allocator;
6const ArrayList = std.ArrayList; 6const ArrayList = std.ArrayList;
7const Bot = @This(); 7const Bot = @This();
8const Config = @import("Config.zig"); 8const Config = @import("Config.zig");
9const DB = @import("DB.zig");
9const HttpClient = std.http.Client; 10const HttpClient = std.http.Client;
10const HttpMethod = std.http.Method; 11const HttpMethod = std.http.Method;
11const Parsed = std.json.Parsed; 12const Parsed = std.json.Parsed;
@@ -14,6 +15,7 @@ const Uri = std.Uri;
14allocator: Allocator, 15allocator: Allocator,
15http_client: HttpClient, 16http_client: HttpClient,
16config: Config, 17config: Config,
18db: *DB,
17base_uri: Uri = Uri.parse("https://api.telegram.org/") catch unreachable, 19base_uri: Uri = Uri.parse("https://api.telegram.org/") catch unreachable,
18uri_path_data: ArrayList(u8), 20uri_path_data: ArrayList(u8),
19poweron: bool = true, 21poweron: bool = true,
@@ -21,7 +23,7 @@ server_header_buffer: [4096]u8 = undefined,
21username: ?[]const u8 = null, 23username: ?[]const u8 = null,
22id: ?i64 = null, 24id: ?i64 = null,
23 25
24pub fn init(allocator: Allocator, config: Config) !Bot { 26pub fn init(allocator: Allocator, config: Config, db: *DB) !Bot {
25 var uri_path_data = try ArrayList(u8).initCapacity(allocator, 5 + config.bot_token.len); 27 var uri_path_data = try ArrayList(u8).initCapacity(allocator, 5 + config.bot_token.len);
26 errdefer uri_path_data.deinit(); 28 errdefer uri_path_data.deinit();
27 29
@@ -35,6 +37,7 @@ pub fn init(allocator: Allocator, config: Config) !Bot {
35 .allocator = allocator, 37 .allocator = allocator,
36 }, 38 },
37 .config = config, 39 .config = config,
40 .db = db,
38 .uri_path_data = uri_path_data, 41 .uri_path_data = uri_path_data,
39 }; 42 };
40} 43}
@@ -47,6 +50,10 @@ pub fn deinit(self: *Bot) void {
47 self.* = undefined; 50 self.* = undefined;
48} 51}
49 52
53pub inline fn answerCallbackQuery(self: *Bot, args: types.AnswerCallbackQueryParams) !void {
54 (try self.post(bool, "answerCallbackQuery", args)).deinit();
55}
56
50pub inline fn deleteMessage(self: *Bot, args: types.DeleteMessageParams) !void { 57pub inline fn deleteMessage(self: *Bot, args: types.DeleteMessageParams) !void {
51 (try self.post(bool, "deleteMessage", args)).deinit(); 58 (try self.post(bool, "deleteMessage", args)).deinit();
52} 59}
diff --git a/src/Config.zig b/src/Config.zig
index f9d6dab..2732df3 100644
--- a/src/Config.zig
+++ b/src/Config.zig
@@ -41,6 +41,7 @@ pub const Wrapper = struct {
41}; 41};
42 42
43bot_token: []const u8, 43bot_token: []const u8,
44db_path: [:0]const u8,
44dev_group: i64, 45dev_group: i64,
45owner: i64, 46owner: i64,
46 47
diff --git a/src/DB.zig b/src/DB.zig
new file mode 100644
index 0000000..6510e7c
--- /dev/null
+++ b/src/DB.zig
@@ -0,0 +1,100 @@
1const sqlite = @import("sqlite");
2const std = @import("std");
3
4const DB = @This();
5
6const target_version = 1;
7
8sql: sqlite.Db,
9
10pub const InlineBotType = enum(u32) {
11 blacklisted = 0,
12 whitelisted = 1,
13};
14
15pub fn init(db_path: [:0]const u8) !DB {
16 const sql = try sqlite.Db.init(.{
17 .mode = .{ .File = db_path },
18 .open_flags = .{
19 .write = true,
20 .create = true,
21 },
22 .threading_mode = .MultiThread,
23 });
24
25 return DB{
26 .sql = sql,
27 };
28}
29
30pub fn deinit(self: *DB) void {
31 self.sql.deinit();
32}
33
34pub fn getInlineBotType(self: *DB, id: i64) !?InlineBotType {
35 const row = try self.sql.one(u32, "SELECT type FROM inline_bots WHERE id = ?", .{}, .{ .id = id });
36 if (row) |r| {
37 return @enumFromInt(r);
38 }
39 return null;
40}
41
42pub fn setInlineBotType(self: *DB, id: i64, ty: InlineBotType) !void {
43 try self.sql.exec("INSERT OR REPLACE INTO inline_bots (id, type) VALUES (?, ?)", .{}, .{ id, @intFromEnum(ty) });
44}
45
46pub fn upgrade(self: *DB) !void {
47 try self.sql.exec("CREATE TABLE IF NOT EXISTS version(id INTEGER PRIMARY KEY, version INTEGER)", .{}, .{});
48 const row = try self.sql.one(struct { version: u32 }, "SELECT version FROM version WHERE id = 0", .{}, .{});
49 var current_ver: u32 = if (row) |r| r.version else 0;
50
51 if (current_ver == target_version) {
52 std.log.info("Database is up to date", .{});
53 return;
54 } else if (current_ver > target_version) {
55 std.log.err("Database has a higher version than supported?", .{});
56 return error.CorruptedDatabase;
57 }
58
59 std.log.info("Updating database from version {} to {}", .{ current_ver, target_version });
60
61 var setVerStmt = try self.sql.prepare("INSERT OR REPLACE INTO version(id, version) VALUES (0, ?)");
62 defer setVerStmt.deinit();
63
64 while (current_ver < target_version) : (current_ver += 1) {
65 std.log.info("Updating database step from {}", .{current_ver});
66 try self.upgradeStep(current_ver + 1);
67 setVerStmt.reset();
68 try setVerStmt.exec(.{}, .{ current_ver + 1 });
69 }
70}
71
72fn upgradeStep(self: *DB, new_version: u32) !void {
73 switch (new_version) {
74 1 => {
75 try self.sql.exec("DROP TABLE IF EXISTS inline_bots_enum", .{}, .{});
76 try self.sql.exec(
77 \\CREATE TABLE inline_bots_enum (
78 \\ id INTEGER PRIMARY KEY,
79 \\ value TEXT UNIQUE
80 \\)
81 , .{}, .{});
82 try self.sql.exec(
83 \\INSERT INTO inline_bots_enum(id, value)
84 \\VALUES (?, 'blacklisted'), (?, 'whitelisted')
85 , .{}, .{
86 .blacklisted = @intFromEnum(InlineBotType.blacklisted),
87 .whitelisted = @intFromEnum(InlineBotType.whitelisted),
88 });
89
90 try self.sql.exec("DROP TABLE IF EXISTS inline_bots", .{}, .{});
91 try self.sql.exec(
92 \\CREATE TABLE inline_bots (
93 \\ id INTEGER PRIMARY KEY,
94 \\ type INTEGER REFERENCES inline_bots_enum(id)
95 \\)
96 , .{}, .{});
97 },
98 else => unreachable,
99 }
100}
diff --git a/src/inline_bots.zig b/src/inline_bots.zig
index c6fa2b7..29824eb 100644
--- a/src/inline_bots.zig
+++ b/src/inline_bots.zig
@@ -4,44 +4,18 @@ const utils = @import("utils.zig");
4 4
5const Bot = @import("Bot.zig"); 5const Bot = @import("Bot.zig");
6 6
7const whitelist = [_]i64{ 7pub inline fn blacklistBot(bot: *Bot, inline_bot_id: i64) !void {
8 90832338, // @vid 8 return bot.db.setInlineBotType(inline_bot_id, .blacklisted);
9 109158646, // @bing
10 114528005, // @pic
11 136269978, // @ImageFetcherBot
12 140267078, // @gif
13 154595593, // @wiki
14 184730458, // @UnitConversionBot
15 223493268, // @minroobot
16 296635833, // @lastfmrobot
17 473587803, // @LyBot
18 595898211, // @DeezerMusicBot
19 733460033, // @crabravebot
20 870410041, // @HowGayBot
21 7904498194, // @tanstiktokbot
22};
23
24const blacklist = [_]i64{
25 6465471545, // @DickGrowerBot
26 7759097490, // @CookieGrowerBot
27};
28
29comptime {
30 std.testing.expect(utils.isSorted(i64, &whitelist)) catch unreachable;
31 std.testing.expect(utils.isSorted(i64, &blacklist)) catch unreachable;
32} 9}
33 10
34inline fn isWhitelisted(bot: types.User) bool { 11pub inline fn whitelistBot(bot: *Bot, inline_bot_id: i64) !void {
35 return utils.isIn(i64, bot.id, &whitelist); 12 return bot.db.setInlineBotType(inline_bot_id, .whitelisted);
36}
37
38inline fn isBlacklisted(bot: types.User) bool {
39 return utils.isIn(i64, bot.id, &blacklist);
40} 13}
41 14
42// Returns true if processing of message should continue 15// Returns true if processing of message should continue
43pub fn onInlineBot(bot: *Bot, msg: types.Message, via: types.User) !bool { 16pub fn onInlineBot(bot: *Bot, msg: types.Message, via: types.User) !bool {
44 if (isWhitelisted(via)) { 17 const ty = try bot.db.getInlineBotType(via.id);
18 if (ty == .whitelisted) {
45 return true; 19 return true;
46 } 20 }
47 21
@@ -51,7 +25,7 @@ pub fn onInlineBot(bot: *Bot, msg: types.Message, via: types.User) !bool {
51 .message_id = msg.message_id, 25 .message_id = msg.message_id,
52 }); 26 });
53 27
54 if (!isBlacklisted(via)) { 28 if (ty != .blacklisted) {
55 // Not explicitly blacklisted, notify dev group 29 // Not explicitly blacklisted, notify dev group
56 const text = try std.fmt.allocPrint( 30 const text = try std.fmt.allocPrint(
57 bot.allocator, 31 bot.allocator,
@@ -60,10 +34,30 @@ pub fn onInlineBot(bot: *Bot, msg: types.Message, via: types.User) !bool {
60 ); 34 );
61 defer bot.allocator.free(text); 35 defer bot.allocator.free(text);
62 36
37 const whitelist_cb = try std.fmt.allocPrint(
38 bot.allocator,
39 "bwl:{}",
40 .{ via.id },
41 );
42 defer bot.allocator.free(whitelist_cb);
43
44 const blacklist_cb = try std.fmt.allocPrint(
45 bot.allocator,
46 "bbl:{}",
47 .{ via.id },
48 );
49 defer bot.allocator.free(blacklist_cb);
50
63 try bot.sendMessage_(.{ 51 try bot.sendMessage_(.{
64 .chat_id = bot.config.dev_group, 52 .chat_id = bot.config.dev_group,
65 .text = text, 53 .text = text,
66 .parse_mode = .html, 54 .parse_mode = .html,
55 .reply_markup = .{
56 .inline_keyboard = &.{&.{
57 .{ .text = "Whitelist", .callback_data = whitelist_cb },
58 .{ .text = "Blacklist", .callback_data = blacklist_cb },
59 }},
60 },
67 }); 61 });
68 } 62 }
69 63
diff --git a/src/main.zig b/src/main.zig
index 942fd90..5931250 100644
--- a/src/main.zig
+++ b/src/main.zig
@@ -1,3 +1,4 @@
1const inline_bots = @import("inline_bots.zig");
1const std = @import("std"); 2const std = @import("std");
2const types = @import("types.zig"); 3const types = @import("types.zig");
3const utils = @import("utils.zig"); 4const utils = @import("utils.zig");
@@ -6,10 +7,9 @@ const Allocator = std.mem.Allocator;
6const ArrayList = std.ArrayList; 7const ArrayList = std.ArrayList;
7const Bot = @import("Bot.zig"); 8const Bot = @import("Bot.zig");
8const Config = @import("Config.zig"); 9const Config = @import("Config.zig");
10const DB = @import("DB.zig");
9const GPA = std.heap.GeneralPurposeAllocator(.{}); 11const GPA = std.heap.GeneralPurposeAllocator(.{});
10 12
11const onInlineBot = @import("inline_bots.zig").onInlineBot;
12
13pub fn main() !void { 13pub fn main() !void {
14 defer std.log.info("We're done", .{}); 14 defer std.log.info("We're done", .{});
15 15
@@ -22,7 +22,11 @@ pub fn main() !void {
22 defer config.deinit(); 22 defer config.deinit();
23 try config.merge("config.json"); 23 try config.merge("config.json");
24 24
25 var bot = try Bot.init(allocator, config.config); 25 var db = try DB.init(config.config.db_path);
26 defer db.deinit();
27 try db.upgrade();
28
29 var bot = try Bot.init(allocator, config.config, &db);
26 defer bot.deinit(); 30 defer bot.deinit();
27 31
28 // TODO: Catch fatal errors, report them 32 // TODO: Catch fatal errors, report them
@@ -48,15 +52,15 @@ fn loadConfig(allocator: Allocator, filename: []const u8) !std.json.Parsed(Confi
48 ); 52 );
49} 53}
50 54
51fn reportError(bot: *Bot, msg: types.Message, err: anyerror) !void { 55fn reportError(bot: *Bot, evt: anytype, err: anyerror) !void {
52 std.log.err("While handling {}: {}", .{ msg, err }); 56 std.log.err("While handling {}: {}", .{ evt, err });
53 const msgStr = try std.json.stringifyAlloc(bot.allocator, msg, .{ 57 const evtStr = try std.json.stringifyAlloc(bot.allocator, evt, .{
54 .whitespace = .indent_2, 58 .whitespace = .indent_2,
55 .emit_null_optional_fields = false, 59 .emit_null_optional_fields = false,
56 }); 60 });
57 defer bot.allocator.free(msgStr); 61 defer bot.allocator.free(evtStr);
58 62
59 const devMsg = try std.fmt.allocPrint(bot.allocator, "<code>{}</code> while handling\n<pre>{s}</pre>", .{ err, msgStr }); 63 const devMsg = try std.fmt.allocPrint(bot.allocator, "<code>{}</code> while handling\n<pre>{s}</pre>", .{ err, evtStr });
60 defer bot.allocator.free(devMsg); 64 defer bot.allocator.free(devMsg);
61 65
62 bot.sendMessage_(.{ 66 bot.sendMessage_(.{
@@ -90,6 +94,12 @@ fn wrappedMain(bot: *Bot) !void {
90 try reportError(bot, message, err); 94 try reportError(bot, message, err);
91 }; 95 };
92 } 96 }
97
98 if (update.callback_query) |cb| {
99 onCallbackQuery(bot, cb) catch |err| {
100 try reportError(bot, cb, err);
101 };
102 }
93 } 103 }
94 } 104 }
95 105
@@ -104,9 +114,55 @@ fn wrappedMain(bot: *Bot) !void {
104 }); 114 });
105} 115}
106 116
117fn onCallbackQuery(bot: *Bot, cb: types.CallbackQuery) !void {
118 if (cb.data) |cb_data| blk: {
119 if (std.mem.startsWith(u8, cb_data, "bbl:")) {
120 if (cb.from.id != bot.config.owner) {
121 break :blk;
122 }
123
124 const inline_bot_id = try std.fmt.parseInt(i64, cb_data[4..], 10);
125 try inline_bots.blacklistBot(bot, inline_bot_id);
126 if (cb.message) |msg| {
127 try bot.deleteMessage(.{
128 .chat_id = msg.chat.id,
129 .message_id = msg.message_id,
130 });
131 }
132 } else if (std.mem.startsWith(u8, cb_data, "bwl:")) {
133 if (cb.from.id != bot.config.owner) {
134 break :blk;
135 }
136
137 const inline_bot_id = try std.fmt.parseInt(i64, cb_data[4..], 10);
138 try inline_bots.whitelistBot(bot, inline_bot_id);
139 if (cb.message) |msg| {
140 try bot.deleteMessage(.{
141 .chat_id = msg.chat.id,
142 .message_id = msg.message_id,
143 });
144 }
145 } else {
146 break :blk;
147 }
148
149 return bot.answerCallbackQuery(.{
150 .callback_query_id = cb.id,
151 .text = "OK",
152 });
153 }
154
155 std.log.info("Unrecognised callback query data: {?s}", .{ cb.data });
156 return bot.answerCallbackQuery(.{
157 .callback_query_id = cb.id,
158 .text = "Unallowed callback query, don't press the button again",
159 .show_alert = true,
160 });
161}
162
107fn onMessage(bot: *Bot, msg: types.Message) !void { 163fn onMessage(bot: *Bot, msg: types.Message) !void {
108 if (msg.via_bot) |via| { 164 if (msg.via_bot) |via| {
109 if (!try onInlineBot(bot, msg, via)) { 165 if (!try inline_bots.onInlineBot(bot, msg, via)) {
110 return; 166 return;
111 } 167 }
112 } 168 }
diff --git a/src/types.zig b/src/types.zig
index b99d24e..d203652 100644
--- a/src/types.zig
+++ b/src/types.zig
@@ -1,4 +1,5 @@
1pub const Animation = @import("types/Animation.zig"); 1pub const Animation = @import("types/Animation.zig");
2pub const AnswerCallbackQueryParams = @import("types/AnswerCallbackQueryParams.zig");
2pub const Audio = @import("types/Audio.zig"); 3pub const Audio = @import("types/Audio.zig");
3pub const BackgroundFill = @import("types/background_fill.zig").BackgroundFill; 4pub const BackgroundFill = @import("types/background_fill.zig").BackgroundFill;
4pub const BackgroundType = @import("types/background_type.zig").BackgroundType; 5pub const BackgroundType = @import("types/background_type.zig").BackgroundType;
diff --git a/src/types/AnswerCallbackQueryParams.zig b/src/types/AnswerCallbackQueryParams.zig
new file mode 100644
index 0000000..875cec1
--- /dev/null
+++ b/src/types/AnswerCallbackQueryParams.zig
@@ -0,0 +1,5 @@
1callback_query_id: []const u8,
2text: ?[]const u8 = null,
3show_alert: bool = false,
4url: ?[]const u8 = null,
5cache_time: u64 = 0,
diff --git a/src/types/InlineKeyboardMarkup.zig b/src/types/InlineKeyboardMarkup.zig
index 388d4fc..a246851 100644
--- a/src/types/InlineKeyboardMarkup.zig
+++ b/src/types/InlineKeyboardMarkup.zig
@@ -1,3 +1,3 @@
1const InlineKeyboardButton = @import("InlineKeyboardButton.zig"); 1const InlineKeyboardButton = @import("InlineKeyboardButton.zig");
2 2
3inline_keyboard: [][]InlineKeyboardButton, 3inline_keyboard: []const []const InlineKeyboardButton,
diff --git a/src/types/SendMessageParams.zig b/src/types/SendMessageParams.zig
index 8c84940..587055c 100644
--- a/src/types/SendMessageParams.zig
+++ b/src/types/SendMessageParams.zig
@@ -1,3 +1,4 @@
1const InlineKeyboardMarkup = @import("InlineKeyboardMarkup.zig");
1const LinkPreviewOptions = @import("LinkPreviewOptions.zig"); 2const LinkPreviewOptions = @import("LinkPreviewOptions.zig");
2const MessageEntity = @import("MessageEntity.zig"); 3const MessageEntity = @import("MessageEntity.zig");
3const ParseMode = @import("parse_mode.zig").ParseMode; 4const ParseMode = @import("parse_mode.zig").ParseMode;
@@ -15,4 +16,5 @@ disable_notification: ?bool = null,
15protect_content: ?bool = null, 16protect_content: ?bool = null,
16message_effect_id: ?[]const u8 = null, 17message_effect_id: ?[]const u8 = null,
17reply_parameters: ?ReplyParameters = null, 18reply_parameters: ?ReplyParameters = null,
18// TODO: reply_markup: InlineKeyboardMarkup OR ReplyKeyboardMarkup OR ReplyKeyboardRemove OR ForceReply 19// TODO: InlineKeyboardMarkup OR ReplyKeyboardMarkup OR ReplyKeyboardRemove OR ForceReply
20reply_markup: ?InlineKeyboardMarkup = null,