const inline_bots = @import("inline_bots.zig");
const std = @import("std");
const types = @import("types.zig");
const utils = @import("utils.zig");
const Allocator = std.mem.Allocator;
const ArrayList = std.ArrayList;
const Bot = @import("Bot.zig");
const Config = @import("Config.zig");
const DB = @import("DB.zig");
const GPA = std.heap.GeneralPurposeAllocator(.{});
pub fn main() !void {
defer std.log.info("We're done", .{});
var gpa = GPA{};
const allocator = gpa.allocator();
defer _ = gpa.deinit();
// Load config
var config = try Config.load(allocator, "config.default.json");
defer config.deinit();
try config.merge("config.json");
var db = try DB.init(config.config.db_path);
defer db.deinit();
try db.upgrade();
var bot = try Bot.init(allocator, config.config, &db);
defer bot.deinit();
// TODO: Catch fatal errors, report them
try wrappedMain(&bot);
}
fn loadConfig(allocator: Allocator, filename: []const u8) !std.json.Parsed(Config) {
const file = try std.fs.cwd().openFile(filename, .{});
defer file.close();
var reader = std.json.reader(allocator, file.reader());
defer reader.deinit();
return try std.json.parseFromTokenSource(
Config,
allocator,
&reader,
.{
.duplicate_field_behavior = .use_last,
.ignore_unknown_fields = true,
.allocate = .alloc_always,
},
);
}
fn reportError(bot: *Bot, evt: anytype, err: anyerror) !void {
std.log.err("While handling {}: {}", .{ evt, err });
const evtStr = try std.json.stringifyAlloc(bot.allocator, evt, .{
.whitespace = .indent_2,
.emit_null_optional_fields = false,
});
defer bot.allocator.free(evtStr);
const devMsg = try std.fmt.allocPrint(bot.allocator, "{} while handling\n
{s}", .{ err, evtStr });
defer bot.allocator.free(devMsg);
bot.sendMessage_(.{
.chat_id = bot.config.dev_group,
.text = devMsg,
.parse_mode = .html,
}) catch |err2| {
std.log.err("While trying to report the error: {}", .{err2});
return err2;
};
}
fn wrappedMain(bot: *Bot) !void {
try bot.sendMessage_(.{
.chat_id = bot.config.dev_group,
.text = "Initializing...",
});
try bot.setMyName(.{ .name = bot.config.bot_name });
var gup = types.GetUpdatesParams{ .timeout = 60 };
while (bot.poweron) {
// TODO: Catch major errors, report them (and crash after 5 of them or so)
const updates = try bot.getUpdates(gup);
defer updates.deinit();
for (updates.value) |update| {
defer gup.offset = update.update_id + 1;
if (update.message) |message| {
onMessage(bot, message) catch |err| {
try reportError(bot, message, err);
};
}
if (update.callback_query) |cb| {
onCallbackQuery(bot, cb) catch |err| {
try reportError(bot, cb, err);
};
}
}
}
// one last getUpdates to make sure offset is saved
gup.timeout = 0;
gup.limit = 1;
(try bot.getUpdates(gup)).deinit();
try bot.sendMessage_(.{
.chat_id = bot.config.dev_group,
.text = "Shutting down...",
});
}
fn onCallbackQuery(bot: *Bot, cb: types.CallbackQuery) !void {
if (cb.data) |cb_data| blk: {
if (std.mem.startsWith(u8, cb_data, "bbl:")) {
if (cb.from.id != bot.config.owner) {
break :blk;
}
const inline_bot_id = try std.fmt.parseInt(i64, cb_data[4..], 10);
try inline_bots.blacklistBot(bot, inline_bot_id);
if (cb.message) |msg| {
try bot.deleteMessage(.{
.chat_id = msg.chat.id,
.message_id = msg.message_id,
});
}
} else if (std.mem.startsWith(u8, cb_data, "bwl:")) {
if (cb.from.id != bot.config.owner) {
break :blk;
}
const inline_bot_id = try std.fmt.parseInt(i64, cb_data[4..], 10);
try inline_bots.whitelistBot(bot, inline_bot_id);
if (cb.message) |msg| {
try bot.deleteMessage(.{
.chat_id = msg.chat.id,
.message_id = msg.message_id,
});
}
} else {
break :blk;
}
return bot.answerCallbackQuery(.{
.callback_query_id = cb.id,
.text = "OK",
});
}
std.log.info("Unrecognised callback query data: {?s}", .{ cb.data });
return bot.answerCallbackQuery(.{
.callback_query_id = cb.id,
.text = "Unallowed callback query, don't press the button again",
.show_alert = true,
});
}
fn onMessage(bot: *Bot, msg: types.Message) !void {
if (msg.via_bot) |via| {
if (!try inline_bots.onInlineBot(bot, msg, via)) {
return;
}
}
if (msg.text) |text| {
try onTextMessage(bot, msg, text);
}
if (msg.new_chat_members) |new_chat_members| {
for (new_chat_members) |new_chat_member| {
try onNewMember(bot, msg, new_chat_member);
}
}
}
fn onNewMember(bot: *Bot, msg: types.Message, new_member: types.User) !void {
if (new_member.id == try bot.getId()) {
return;
// TODO:
// Bot is added to a new group
// return bot.sendAnimation_(.{
// .chat_id = msg.chat.id,
// // TODO: lol
// .animation = "CgACAgQAAx0CVcPEEgACDC5mo7YHMgOE2n3qo3e9UOyd4N-uxQACNAMAAlbuDFMRWj9LxNLBkDUE",
// });
}
var sb = ArrayList(u8).init(bot.allocator);
defer sb.deinit();
const w = sb.writer();
try w.writeAll("Hello there, ");
try new_member.writeFormattedName(w);
try w.writeAll("! Be on your bestest behaviour now!!");
// TODO:
// try bot.sendAnimation_(.{
// .chat_id = msg.chat.id,
// // TODO: lol
// .animation = "CgACAgQAAx0CVcPEEgACC9Vmo6_zCxMp3ZNXSMM1nI6nMkIhgwACNwMAAtDmDFMop6BHmV7lUTUE",
// .caption = sb.items,
// .parse_mode = .html,
// .show_caption_above_media = true,
// .reply_parameters = .{
// .allow_sending_without_reply = true,
// .message_id = msg.message_id,
// .chat_id = msg.chat.id,
// },
// });
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = sb.items,
.parse_mode = .html,
.reply_parameters = .{
.allow_sending_without_reply = true,
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
}
fn isBadText(text: []const u8) bool {
_ = text;
return false;
}
fn onTextMessage(bot: *Bot, msg: types.Message, text: []const u8) !void {
if (isBadText(text)) {
// TODO: Delete message, mute & warn user
// 0 current warns: 5 minute mute, +1 warn
// 1 current warn : 10 minute mute, +1 warn
// 2 current warns: 30 minute mute, +1 warn
// 3 current warns: 1 hour mute, +1 warn
// 4 current warns: 1 day mute, +1 warn
// 5 current warns: Ban
//
// warn gets removed after a month of no warns
//
// Lines to say in response:
// Your head will be my new trophy!
// Your cursed bloodline ends here!
// This is the end of you, s'wit!
// Your life's end is approaching.
// Surrender your life to me and I will end your pain!
// Your pain is nearing an end.
// May our Lords be merciful!
// You have sealed your fate!
// You cannot escape the righteous!
// You will pay with your blood!
// You will die.
// There is no escape.
// Die, fetcher.
// I shall enjoy watching you take your last breath.
// You'll soon be nothing more than a bad memory!
// You will die in disgrace.
// I'll see you dead.
// One of us will die here and it won't be me.
// You don't deserve to live.
// Surrender now and I might let you live!
// I will bathe in your blood.
// Your bones will be my dinner.
// So small and tasty. I will enjoy eating you.
return;
}
if (msg.entities) |entities| {
for (entities) |entity| {
if (entity.type == .bot_command and entity.offset == 0) {
const cmd = try entity.extract(text);
try onTextCommand(bot, msg, text, cmd);
}
}
}
if (std.mem.eql(u8, text, ":3")) {
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = ">:3",
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
} else if (std.mem.eql(u8, text, ">:3")) {
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = ">:3",
.parse_mode = .html,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
} else if (std.ascii.startsWithIgnoreCase(text, "big ")) {
const the_text = text[4..];
if (!try utils.isTgWhitespaceStr(the_text)) {
const lc = try utils.getLetterCasing();
const uppercased = try lc.toUpperStr(bot.allocator, the_text);
defer bot.allocator.free(uppercased);
var output = ArrayList(u8).init(bot.allocator);
defer output.deinit();
try output.appendSlice("");
try utils.escapeXml(output.writer(), uppercased);
try output.appendSlice("");
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = output.items,
.parse_mode = .html,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
}
} else if (std.ascii.eqlIgnoreCase(text, "dio cane")) {
const reply_to = if (msg.reply_to_message) |r|
r.message_id
else
msg.message_id;
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "porco dio",
.reply_parameters = .{
.message_id = reply_to,
.chat_id = msg.chat.id,
},
});
} else if (std.ascii.eqlIgnoreCase(text, "forgor")) {
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "💀",
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
} else if (std.ascii.eqlIgnoreCase(text, "huh")) {
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "idgi",
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
} else if (std.mem.eql(u8, text, "H")) {
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "Randomly selected reminder that h > H.",
.parse_mode = .html,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
} else if (std.ascii.eqlIgnoreCase(text, "porco dio")) {
const reply_to = if (msg.reply_to_message) |r|
r.message_id
else
msg.message_id;
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "dio cane",
.reply_parameters = .{
.message_id = reply_to,
.chat_id = msg.chat.id,
},
});
} else if (std.ascii.startsWithIgnoreCase(text, "say ")) {
const the_text = text[4..];
if (!try utils.isTgWhitespaceStr(the_text)) {
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = the_text,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
}
} else if (std.ascii.eqlIgnoreCase(text, "uwu")) {
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "OwO",
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
} else if (std.ascii.eqlIgnoreCase(text, "waow")) {
const reply_to = if (msg.reply_to_message) |r|
r.message_id
else
msg.message_id;
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED BASED",
.reply_parameters = .{
.message_id = reply_to,
.chat_id = msg.chat.id,
},
});
} else if (std.ascii.eqlIgnoreCase(text, "what")) {
var sb = try ArrayList(u8).initCapacity(bot.allocator, 9);
defer sb.deinit();
if (text[0] == 'w') {
sb.appendSliceAssumeCapacity("g");
} else {
sb.appendSliceAssumeCapacity("G");
}
if (text[1] == 'h') {
sb.appendSliceAssumeCapacity("ood ");
} else {
sb.appendSliceAssumeCapacity("OOD ");
}
if (text[2] == 'a') {
sb.appendSliceAssumeCapacity("gir");
} else {
sb.appendSliceAssumeCapacity("GIR");
}
if (text[3] == 't') {
sb.appendSliceAssumeCapacity("l");
} else {
sb.appendSliceAssumeCapacity("L");
}
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = sb.items,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
}
}
fn onTextCommand(bot: *Bot, msg: types.Message, text: []const u8, cmd: []const u8) !void {
_ = text;
const simple_cmd = if (std.mem.indexOfScalar(u8, cmd, '@')) |idx| blk: {
const cmd_username = cmd[idx + 1 ..];
if (!std.mem.eql(u8, cmd_username, try bot.getUsername())) {
return;
}
break :blk cmd[1..idx];
} else cmd[1..];
// TODO: StaticStringMap :)
if (std.mem.eql(u8, simple_cmd, "chatid")) {
var sb = ArrayList(u8).init(bot.allocator);
defer sb.deinit();
try sb.writer().print("{}", .{msg.chat.id});
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = sb.items,
.parse_mode = .html,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
} else if (std.mem.eql(u8, simple_cmd, "msginfo")) {
if (msg.reply_to_message) |replied| {
const str_data = try std.json.stringifyAlloc(bot.allocator, replied.*, .{
.whitespace = .indent_2,
.emit_null_optional_fields = false,
});
defer bot.allocator.free(str_data);
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = str_data,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
}
} else if (std.mem.eql(u8, simple_cmd, "ping")) {
var timer = try std.time.Timer.start();
var sb = ArrayList(u8).init(bot.allocator);
defer sb.deinit();
try sb.writer().print("Pong!\nSend time: ...", .{});
const reply = try bot.sendMessage(.{
.chat_id = msg.chat.id,
.text = sb.items,
.reply_parameters = .{
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
defer reply.deinit();
const send = @as(f64, @floatFromInt(timer.read())) / std.time.ns_per_ms;
sb.clearRetainingCapacity();
try sb.writer().print("Pong!\n\nSend time: {d}ms", .{send});
try bot.editMessageText_(.{
.chat_id = reply.value.chat.id,
.message_id = reply.value.message_id,
.text = sb.items,
});
} else if (std.mem.eql(u8, simple_cmd, "shutdown")) blk: {
if (msg.from == null or msg.from.?.id != bot.config.owner) {
break :blk;
}
bot.poweron = false;
try bot.sendMessage_(.{
.chat_id = msg.chat.id,
.text = "Initialising shutdown...",
.reply_parameters = .{
.allow_sending_without_reply = true,
.message_id = msg.message_id,
.chat_id = msg.chat.id,
},
});
}
}