228 lines
7.8 KiB
Perl
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;
|