mnemosyne/lib/Mnemosyne/Telegram.pm

228 lines
7.8 KiB
Perl

package Mnemosyne::Telegram;
use strict;
use warnings;
use Mojo::UserAgent;
use Mojo::JSON qw(encode_json);
use Carp qw(croak);
use constant API_BASE => 'https://api.telegram.org/bot';
use constant TITLE_TRUNCATE => 24;
# new($class, $token) — create client
sub new {
my ($class, $token) = @_;
croak "token required" unless $token;
return bless {
_token => $token,
_ua => Mojo::UserAgent->new,
}, $class;
}
# -----------------------------------------------------------------------
# Core API calls
# -----------------------------------------------------------------------
# send_message($self, $chat_id, $text, %opts) → response hashref
# opts: parse_mode, reply_markup, disable_notification
sub send_message {
my ($self, $chat_id, $text, %opts) = @_;
return $self->_post('sendMessage', {
chat_id => $chat_id,
text => $text,
parse_mode => $opts{parse_mode} // 'HTML',
defined $opts{reply_markup} ? (reply_markup => $opts{reply_markup}) : (),
defined $opts{disable_notification} ? (disable_notification => $opts{disable_notification}) : (),
});
}
# edit_message($self, $chat_id, $message_id, $text, %opts) → response hashref
sub edit_message {
my ($self, $chat_id, $message_id, $text, %opts) = @_;
return $self->_post('editMessageText', {
chat_id => $chat_id,
message_id => $message_id,
text => $text,
parse_mode => $opts{parse_mode} // 'HTML',
defined $opts{reply_markup} ? (reply_markup => $opts{reply_markup}) : (),
});
}
# edit_reply_markup($self, $chat_id, $message_id, $reply_markup) → response hashref
# Pass undef to remove the keyboard entirely.
sub edit_reply_markup {
my ($self, $chat_id, $message_id, $reply_markup) = @_;
return $self->_post('editMessageReplyMarkup', {
chat_id => $chat_id,
message_id => $message_id,
reply_markup => $reply_markup // {},
});
}
# answer_callback_query($self, $callback_query_id, %opts)
# opts: text (notification text), show_alert (bool)
sub answer_callback_query {
my ($self, $cbq_id, %opts) = @_;
return $self->_post('answerCallbackQuery', {
callback_query_id => $cbq_id,
defined $opts{text} ? (text => $opts{text}) : (),
defined $opts{show_alert} ? (show_alert => $opts{show_alert} ? \1 : \0) : (),
});
}
# set_webhook($self, $url, $secret) → response hashref
sub set_webhook {
my ($self, $url, $secret) = @_;
croak "url required" unless $url;
croak "secret required" unless $secret;
return $self->_post('setWebhook', {
url => $url,
secret_token => $secret,
allowed_updates => ['message', 'callback_query'],
});
}
# delete_webhook($self) → response hashref
sub delete_webhook {
my ($self) = @_;
return $self->_post('deleteWebhook', { drop_pending_updates => \1 });
}
# -----------------------------------------------------------------------
# Keyboard factories — all return inline_keyboard hashrefs
# -----------------------------------------------------------------------
# mark_done_keyboard(\@tasks) → reply_markup hashref
# Each $task: { id => ..., title => ... }
# One button per task: "✓ <truncated title>", callback: done:<id>
sub mark_done_keyboard {
my ($class, $tasks) = @_;
my @rows = map {
[{ text => "\x{2713} " . _trunc($_->{title}), callback_data => "done:$_->{id}" }]
} @$tasks;
return { inline_keyboard => \@rows };
}
# undo_keyboard($completion_id) → reply_markup hashref
sub undo_keyboard {
my ($class, $cid) = @_;
return { inline_keyboard => [[
{ text => "\x{21A9} Undo", callback_data => "undo:$cid" }
]]};
}
# confirm_delete_keyboard($task_id) → reply_markup hashref
sub confirm_delete_keyboard {
my ($class, $task_id) = @_;
return { inline_keyboard => [[
{ text => "\x{274C} Delete", callback_data => "confirm_delete:$task_id" },
{ text => "\x{2716} Cancel", callback_data => "cancel" },
]]};
}
# class_keyboard() → reply_markup
sub class_keyboard {
my ($class) = @_;
return { inline_keyboard => [
[{ text => 'Monthly date', callback_data => 'add_class:monthly_date' }],
[{ text => 'Monthly weekday', callback_data => 'add_class:monthly_weekday' }],
[{ text => 'Every N period', callback_data => 'add_class:every_n_period' }],
[{ text => 'Interval (reset on done)', callback_data => 'add_class:interval' }],
[{ text => 'Floating reminder', callback_data => 'add_class:floating' }],
[{ text => "\x{2716} Cancel", callback_data => 'add_cancel' }],
]};
}
# weekday_keyboard() → reply_markup
sub weekday_keyboard {
my ($class) = @_;
my @days = (['Mon','1'],['Tue','2'],['Wed','3'],['Thu','4'],
['Fri','5'],['Sat','6'],['Sun','7']);
my @row = map { { text => $_->[0], callback_data => "add_weekday:$_->[1]" } } @days;
return { inline_keyboard => [
\@row,
[{ text => "\x{2716} Cancel", callback_data => 'add_cancel' }],
]};
}
# ordinal_keyboard() → reply_markup
sub ordinal_keyboard {
my ($class) = @_;
return { inline_keyboard => [
[map { { text => "${_}.", callback_data => "add_ordinal:$_" } } 1..4],
[{ text => 'Last', callback_data => 'add_ordinal:-1' },
{ text => "\x{2716} Cancel", callback_data => 'add_cancel' }],
]};
}
# period_unit_keyboard() → reply_markup
sub period_unit_keyboard {
my ($class) = @_;
return { inline_keyboard => [[
{ text => 'Day', callback_data => 'add_unit:day' },
{ text => 'Week', callback_data => 'add_unit:week' },
{ text => 'Month', callback_data => 'add_unit:month' },
],[
{ text => "\x{2716} Cancel", callback_data => 'add_cancel' },
]]};
}
# priority_keyboard() → reply_markup
sub priority_keyboard {
my ($class) = @_;
return { inline_keyboard => [[
{ text => "\x{2605}\x{2605}\x{2605} High", callback_data => 'add_priority:high' },
{ text => "\x{2605}\x{2605} Medium", callback_data => 'add_priority:medium' },
{ text => "\x{2605} Low", callback_data => 'add_priority:low' },
],[
{ text => "\x{2716} Cancel", callback_data => 'add_cancel' },
]]};
}
# skip_keyboard() → reply_markup — "Skip" + "Cancel" for optional fields
sub skip_keyboard {
my ($class) = @_;
return { inline_keyboard => [[
{ text => 'Skip', callback_data => 'add_skip' },
{ text => "\x{2716} Cancel", callback_data => 'add_cancel' },
]]};
}
# save_cancel_keyboard() → reply_markup — final wizard confirmation
sub save_cancel_keyboard {
my ($class) = @_;
return { inline_keyboard => [[
{ text => "\x{2714} Save", callback_data => 'add_save' },
{ text => "\x{2716} Cancel", callback_data => 'add_cancel' },
]]};
}
# -----------------------------------------------------------------------
# Internal
# -----------------------------------------------------------------------
sub _post {
my ($self, $method, $body) = @_;
my $url = API_BASE . $self->{_token} . '/' . $method;
my $tx = $self->{_ua}->post(
$url => { 'Content-Type' => 'application/json' } => encode_json($body)
);
my $res = eval { $tx->result };
if ($@) {
warn "Telegram connection error ($method): $@\n";
return {};
}
unless ($res->is_success) {
warn "Telegram API error ($method): " . $res->body . "\n";
}
return $res->json // {};
}
sub _trunc {
my ($s) = @_;
return length($s) <= TITLE_TRUNCATE
? $s
: substr($s, 0, TITLE_TRUNCATE - 1) . "\x{2026}";
}
1;