Group
Extension

Telegram-CamshotBot/lib/Telegram/CamshotBot.pm

#!/usr/bin/env perl

# ABSTRACT: Telegram bot that send you a snapshot from IP camera using ffmpeg (don't forget to install it!)


package Telegram::CamshotBot;
$Telegram::CamshotBot::VERSION = '0.03';
use Telegram::CamshotBot::Util qw(first_existing_file random_caption abs_path_of_sample_mojo_conf fev);
use Mojolicious::Lite;
use Mojolicious::Plugin::JSONConfig;
use Mojolicious::Plugin::Webtail;
use WWW::Telegram::BotAPI;
use Date::Format;
use Telegram::Bot::Message;
use feature 'say';
use Data::Dumper;
use Data::Printer;
use Cwd;
use Net::Ping;
use Regexp::Common qw /net/;

my $config_file_path = first_existing_file(
  $ENV{"CAMSHOTBOT_CONFIG"},
  $ENV{"HOME"}.'/.camshotbot',
  getcwd.'/camshotbot.conf.json',
  abs_path_of_sample_mojo_conf(__PACKAGE__),
);

print "Using config: ".$config_file_path."\n";

my $config_values = plugin 'JSONConfig' => { file => $config_file_path };

  plugin( 'Webtail', file => $ENV{CAMSHOTBOT_WEBTAIL_LOG_FILE} || $config_values->{log_file} ); # https://metacpan.org/pod/Mojolicious::Plugin::Webtail

# BEGIN { $ENV{TELEGRAM_BOTAPI_DEBUG}=1 };

my $api;
my $bot_name = '';
my $telegram_token = $ENV{CAMSHOTBOT_TELEGRAM_API_TOKEN} || $config_values->{telegram_api_token};
my $screenshot_file = $ENV{CAMSHOTBOT_LAST_SHOT_FILENAME} || $config_values->{last_shot_filename} || 'latest.jpg'; # name of last screenshot or env
my $stream_url = $ENV{CAMSHOTBOT_STREAM_URL} || $config_values->{stream_url};
my $ffmpeg_cmd = 'ffmpeg -hide_banner -loglevel panic -i '.$stream_url.' -f image2 -vframes 1 '.$screenshot_file if ($stream_url);
my ($camera_ip) = ($stream_url  =~ /($RE{net}{IPv4})/) if ($stream_url);
my $bot_domain = $ENV{VIRTUAL_HOST} || $ENV{LETSENCRYPT_HOST} || $ENV{CAMSHOTBOT_DOMAIN} || $config_values->{bot_domain};
my $polling_flag = $ENV{CAMSHOTBOT_POLLING} || $config_values->{polling}; # 0 or not set -> webhook, 1 -> polling
my $polling_timeout = 3; # default
if ($polling_flag) {
  $polling_timeout = $ENV{CAMSHOTBOT_POLLING_TIMEOUT} || $config_values->{polling_timeout};
} else {
  $polling_timeout = undef;
}
my $docker_flag = $ENV{CAMSHOTBOT_FFMPEG_DOCKER} || $config_values->{ffmpeg_docker}; # any value to send cached image

if ($telegram_token) { # maybe add
  $api = WWW::Telegram::BotAPI->new (
      token => $telegram_token
  );
  $bot_name = $api->getMe->{result}{username};
} else {
  say "Attention! Telegram API token isn't specified. Please edit ".$config_file_path." or CAMSHOTBOT_TELEGRAM_API_TOKEN";
}


helper answer => sub {
	my ($c, $update) = @_;

	app->log->info("Processing new update...");
	my $mo = Telegram::Bot::Message->create_from_hash($update->{message});

	my $msg = $mo->text;
  my $chat_id = $mo->chat->id;
  my $from_id = $mo->from->id;
  my $date = $mo->date;
  my $date_str = time2str("%R %a %o %b %Y" ,$mo->date); # 11:59 Sun 29th Jan 2017


  ###### Loggging
	if ($ENV{CAMSHOTBOT_TELEGRAM_DEBUG} || $config_values->{debug}) {
		# full log, convenient if you need to restict chat_id's and check what's wrong
		app->log->info("Update from Telegram API: ".Dumper $update);
		app->log->info("Update parsed by Telegram::Bot::Message: ".Dumper $mo);
	} else {
		my $from_str = '';
		my $username = $mo->from->username;
		if ($username) {
			$from_str = $username;
		} else {
			$from_str = $mo->from->first_name." ".$mo->from->first_name." (id ".$from_id.")";
		}
		app->log->info($msg." from ".$from_str." sent at ".$date_str);
	};
  ###### end loggging

  if ($docker_flag) {
    app->log->info("Sending a screenshot generated by ffmpeg docker container some time ago");
  } else {
    `rm -f $screenshot_file`; # remove old screenshot
   	my $o = `$ffmpeg_cmd`;
    app->log->info("Screenshot got with command: ".$ffmpeg_cmd.', result : '.$o);
  }

   	if ( ($msg eq "/shot") || ($msg eq '/shot@'.$bot_name )) {

		$api->sendPhoto ({
		    chat_id => $chat_id,
		    photo   => {
		        file => $screenshot_file
		    },
		    caption => random_caption(),
		    reply_to_message_id => $mo->message_id
		});
	}

	if ($msg eq "/help") {

		$api->sendMessage ({
		    chat_id => $chat_id,
		    text => '/shot - Get online camera shot',
		    reply_to_message_id => $mo->message_id
		});
	}

};

# for local testing purposes. also shows how many unprocessed updates in queue on server
helper check_for_updates => sub {
	my $c = shift;
	my $res = $api->deleteWebhook() ; # disable webhooks
	# warn Dumper $res;
	my $updates = $api->getUpdates();
	my $h = {
		updates_in_queue => {}
	};
	$h->{updates_in_queue}{count} = scalar @{$updates->{result}};
	$h->{updates_in_queue}{details} = \@{$updates->{result}};

	my @u_ids;
	for (@{$updates->{result}}) {
		push @u_ids, $_->{update_id};
	}

	$h->{updates_in_queue}{update_ids} = \@u_ids;

	$c->setWebhook() if !($polling_flag); # set Webhook again if needed

	return $h;
};

helper setWebhook => sub {
	my $c = shift;
	return $api->setWebhook({ url => 'https://'.$bot_domain.'/'.$telegram_token });
};


post '/'.$telegram_token => sub {
  my $c = shift;
  my $update = $c->req->json;
  $c->answer($update);
  $c->render(json => "ok");
};

get '/' => sub {
	shift->render(text => 'bot is running');
};

get '/status' => sub {
	my $c = shift;
	my $status = {};
	$status->{telegram_api} = eval { $api->getMe } or $status->{telegram_api} = $api->parse_error->{msg};
  $status->{stream_url} = $stream_url;
  $status->{ffmpeg_docker} = $docker_flag;
	my $p = Net::Ping->new();
	$status->{vpn_status} = 'down';
	$status->{vpn_status} = 'up' if $p->ping($camera_ip);
	$status->{WebhookInfo} = $api->getWebhookInfo;
	$p->close();
	$c->render(json => $status);
};

get '/setwebhook' => sub {
	my $c = shift;
	my $res = $c->setWebhook();
	$c->render( json => $res );
};

# shows info about unprocessed updates on server
get '/debug' => sub {
	my $c = shift; # $c = Mojolicious::Controller object
	$c->render( json => $c->check_for_updates() );
};




if ($telegram_token && $polling_flag) {

	my $res = $api->deleteWebhook();
	app->log->info("Webhook was deleted. Starting polling with ".$polling_timeout."secs timeout ...") if $res;

	Mojo::IOLoop->recurring($polling_timeout => sub {

		my @updates = @{$api->getUpdates->{result}};

		if (@updates) {
			for my $u (@updates) {
				#app->build_controller->answer($u); # Mojolicious::Lite ->  Mojolicious::Controller -> Mojolicious::Helper
        app->answer($u); # Mojolicious::Lite ->  Mojolicious::Controller -> Mojolicious::Helper
        $api->getUpdates({ offset => $u->{update_id} + 1.0 }); # clear buffer
			}
		}

	});
}

# my $queue = app->build_controller->check_for_updates()->{updates_in_queue};
# if ne daemon

if ($telegram_token) {
  my $queue = app->check_for_updates()->{updates_in_queue};
  app->log->info('Starting bot @'.$bot_name."...");
  app->log->info("Having ".$queue->{count}." stored Updates at Telegram server");
  app->log->info("Unprocessed update ids (for offset debug): ".join(',', @{$queue->{update_ids}}) );
}

push @{app->commands->namespaces}, 'Telegram::CamshotBot::Command';
app->start;

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

Telegram::CamshotBot - Telegram bot that send you a snapshot from IP camera using ffmpeg (don't forget to install it!)

=head1 VERSION

version 0.03

=head1 RUNNING

Docker way

  wget https://raw.githubusercontent.com/pavelsr/camshotbot/master/docker-compose.yml.example > docker-compose.yml

then edit CAMSHOTBOT_* variables and change network if needed

  docker-compose up -d

Standalone way

1) Place .camshotbot file in home user directory or camshotbot.conf.json in directory from what you will run camshotbot
Add all essential variables:  telegram_api_token, stream_url, bot_domain

2) As alternative to (1) you can set all CAMSHOTBOT_* environment variables (see ENVIRONMENT VARIABLES section)

3) run

  camshotbot daemon

For performance you can run ffmpeg in a separate "caching" docker container.
String below will output a single image that is continuously overwritten with new images

  docker run -d -it -v $(pwd):/tmp/workdir --network=host jrottenberg/ffmpeg:3.3-alpine -hide_banner -loglevel error -i rtsp://10.132.193.9//ch0.h264 -f image2 -vf fps=1/3 -y -update 1 latest.jpg

For more details please see docker-compose.yml.example

!! ATTENTION ! Bot is working correctly only if version of Telegram::CamshotBot >= 0.03.
There are some critical errors in previous versions, sorry for that.

=head1 ENVIRONMENT VARIABLES

Environment variables are always checked firstly, before any config files

To get list of all available environment variables plese run after git clone:

  grep -o -P "CAMSHOTBOT_\w+" lib/Telegram/CamshotBot.pm | sort -u

Actual List (useful for Docker deployment):

  CAMSHOTBOT_CONFIG
  CAMSHOTBOT_DOMAIN
  CAMSHOTBOT_FFMPEG_DOCKER
  CAMSHOTBOT_LAST_SHOT_FILENAME
  CAMSHOTBOT_POLLING
  CAMSHOTBOT_POLLING_TIMEOUT
  CAMSHOTBOT_STREAM_URL
  CAMSHOTBOT_TELEGRAM_API_TOKEN
  CAMSHOTBOT_TELEGRAM_DEBUG
  CAMSHOTBOT_WEBTAIL_LOG_FILE

Check more details about their usage at docker-compose.yml.example

To check which variables are set you can run

  printenv | grep CAMSHOTBOT_* | sort -u

For setting environment variable you can use

  export CAMSHOTBOT_POLLING=1

=head1 DEVELOPMENT

If you want to run unit tests without dzil test

  prove -l -v t

or

  perl -Ilib

=head1 AUTHOR

Pavel Serikov <pavelsr@cpan.org>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2017 by Pavel Serikov.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.