Skip to content

Instantly share code, notes, and snippets.

@Achterstraat
Created June 19, 2022 19:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Achterstraat/7422ecb6a12e64cdbbd1c7428fc4b25d to your computer and use it in GitHub Desktop.
Save Achterstraat/7422ecb6a12e64cdbbd1c7428fc4b25d to your computer and use it in GitHub Desktop.
Use SSH, Telegram or any Webbrowser to Sync Amazon Blink Mediafiles to Synology (with or without Cronjobs)
<?php
class Blink
{
public $account = [
'credentials' => [
'email' => '',
'password' => '',
],
'region' => 'rest-prod.immedia-semi.com',
];
public $regions = [];
public $storage = '/volume1/surveillance';
public $telegram = [
'bot' => '',
'chat' => '',
'data' => null
];
public $networks = [];
public $sync_modules = [];
public $cameras = [];
public $sirens = [];
public $chimes = [];
public $doorbells = [];
public $device_limits = [];
public $domain = null;
public $error = null;
public function __construct()
{
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
error_reporting(E_ALL);
$this->account(null, 'account');
$this->domain = substr($_SERVER['HTTP_HOST'], strpos($_SERVER['HTTP_HOST'], '.')+1);
if(!$this->regions())
{
$this->error = 'Regions';
}
return $this;
}
public function account($data = null, $name = 'account')
{
if($this->is('telegram'))
{
$file = dirname(__FILE__).'/account.txt';
if(is_null($data))
{
if(is_file($file))
{
$account = json_decode($this->cryptor(file_get_contents($file), 'decrypt'), true);
$this->account = $account;
return $account;
}
return false;
}
elseif(is_array($data))
{
return file_put_contents($file, $this->cryptor(json_encode($data)));
}
return @unlink($file);
}
$account = $this->cookie('account', $data);
if($account)
{
$this->account = $account;
}
return $account;
}
public function cookie($name = null, $value = null, $secs = 3600)
{
if(is_string($name))
{
if(isset($_COOKIE[$name]))
{
if(is_null($value))
{
$data = (empty($_COOKIE[$name]) ? '' : stripslashes($_COOKIE[$name]));
return (empty($data) ? false : json_decode($this->cryptor($data, 'decrypt'), true));
}
elseif($value)
{
return setcookie($name, $this->cryptor((is_array($value) ? json_encode($value) : $value)), (time()+$secs), '/', '.'.$this->domain);
}
else
{
return setcookie($name, $this->cryptor(false), (time()-$secs), '/', '.'.$this->domain);
}
}
elseif(is_null($value))
{
return false;
}
else
{
unset($_COOKIE[$name]);
return setcookie($name, $this->cryptor((is_array($value) ? json_encode($value) : $value)), (time()+$secs), '/', '.'.$this->domain);
}
}
return false;
}
public function cryptor($data, $action = 'encrypt')
{
$vars = [
'iv' => '1234567890987654',
'key' => 'AmazonBl!nk',
'method' => 'AES-128-CBC',
'options' => 0,
];
$length = openssl_cipher_iv_length($vars['method']);
switch($action)
{
case 'encrypt': {
return openssl_encrypt($data, $vars['method'], $vars['key'], $vars['options'], $vars['iv']);
}
case 'decrypt': {
return openssl_decrypt($data, $vars['method'], $vars['key'], $vars['options'], $vars['iv']);
}
default: {
return false;
}
}
}
public function curl($uri, $fields = [], $headers = [], $type = 'POST', $file = false)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $uri);
if(in_array($type, ['POST']))
{
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, (is_array($fields) ? json_encode($fields) : $fields));
}
curl_setopt($curl, CURLOPT_USERAGENT, 'AmazonBl!nkert');
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
curl_setopt($curl, CURLOPT_HTTPHEADER, array_merge([
'Content-Type: application/json'
], $headers));
$response = curl_exec($curl);
curl_close($curl);
return ($file ? $response : json_decode($response, true));
}
public function form($type = 'login')
{
$form = false;
switch($type)
{
case 'login': {
$form = '<form action="/" method="POST">
<div>
<label for="email">E-mail:</label>
<input type="text" name="email" id="email" value="'.(empty($this->account['credentials']['email']) ? '' : $this->account['credentials']['email']).'" autocomplete="off">
</div>
<div>
<label for="pin">Password:</label>
<input type="password" name="password" id="password" value="'.(empty($this->account['credentials']['password']) ? '' : $this->account['credentials']['password']).'" autocomplete="off">
</div>
<div>
<label for="region">Region:</label>
<select name="region" id="region">';
foreach($this->regions as $region => $name)
{
$form .= '<option value="'.$region.'" '.(!empty($this->account['region']) && $region == $this->account['region'] ? 'selected="selected"' : '').'">'.$name.'</option>';
}
$form .= '</select>
</div>
<div>
<label for="pin">Storage:</label>
<input type="text" name="storage" id="storage" value="'.(empty($this->storage) || !is_dir($this->storage) ? '' : $this->storage).'" autocomplete="off">
</div>
<button type="submit">Check</button>
</form>';
break;
}
case 'pin': {
$form = '<form action="/" method="POST">
<div>
<label for="pin">PIN:</label>
<input type="text" name="pin" id="pin" inputmode="numeric" pattern="[0-9]{6}" autocomplete="off">
</div>
<button type="submit">Check</button>
</form>';
break;
}
}
return $form;
}
public function homescreen()
{
$result = $this->curl('https://rest-prod.immedia-semi.com/api/v3/accounts/'.$this->account['account']['account_id'].'/homescreen', [], [
'Host: '.$this->account['region'],
'Token-auth: '.$this->account['auth']['token'],
], 'GET');
foreach(['networks', 'sync_modules', 'cameras', 'doorbells', 'device_limits'] as $name)
{
if(isset($result[$name]))
{
$this->{$name} = $result[$name];
}
}
return $result;
}
public function is($type, $data = [])
{
if(is_array($type))
{
foreach($type as $t)
{
if(!$this->is($t, $data))
{
return false;
}
}
return true;
}
else
{
switch($type)
{
case 'account':
case 'verify': {
return (array_key_exists($type, $this->account) ? true : false);
}
case 'cli':
case 'curl':
case 'wget':
case 'cron': {
return (isset($_SERVER['HTTP_USER_AGENT']) && preg_match('~^(curl|wget)~i', $_SERVER['HTTP_USER_AGENT']) || php_sapi_name() == 'cli');
}
case 'json': {
$json = json_decode($data, true);
return ($json && $data != $json);
}
case 'telegram': {
return (substr($_SERVER['REMOTE_ADDR'], 0, 7) == '91.108.' || isset($_SERVER['HTTP_USER_AGENT']) && preg_match('~^telegram~i', $_SERVER['HTTP_USER_AGENT']));
}
case 'temperature': {
return ((($data-32)*5)/9);
}
}
}
return false;
}
public function login($data = [])
{
$email = (empty($data['email']) ? false : $data['email']);
$password = (empty($data['password']) ? false : $data['password']);
$region = (empty($data['region']) || !array_key_exists($data['region'], $this->regions) ? false : $data['region']);
if($email && $password && $region)
{
$this->account = array_merge($this->account, [
'credentials' => [
'email' => $email,
'password' => $password,
],
'region' => $region
]);
$result = $this->curl('https://rest-prod.immedia-semi.com/api/v5/account/login', array_merge($this->account['credentials'], [
'unique_id' => '00000000-1111-0000-1111-00000000000'
], [
'Host: '.$this->account['region'],
]));
if(empty($result['account']))
{
$this->error = 'Auth'.(empty($result['message']) ? '</b>!' : ':</b> '.$result['message']);
return false;
}
$this->account = array_merge($this->account, $result);
$this->account($this->account, 'account');
return $this->account;
}
return false;
}
public function logout()
{
$this->account(false, 'account');
$this->refresh();
}
public function medias()
{
$m = 0;
$p = 1;
while(true)
{
$result = $this->curl('https://'.$this->account['region'].'/api/v1/accounts/'.$this->account['account']['account_id'].'/media/changed?since=2015-04-19T23:11:20+0000&page='.$p, [], [
'Host: '.$this->account['region'],
'token-auth: '.$this->account['auth']['token']
], 'GET');
if(!empty($result['media']))
{
foreach($result['media'] as $media)
{
$dir = $this->storage.'/'.$media['device_name'].'/'.date('YmdA', strtotime($media['created_at']));
if(!is_dir($dir))
{
mkdir($dir, 0755, true);
}
$source = '';
if(in_array($media['source'], ['button_press', 'pir', 'snapshot']))
{
$sources = ['button_press' => 'button', 'liveview' => 'watching', 'pir' => 'motion'];
$source = '-'.(array_key_exists($media['source'], $sources) ? $sources[$media['source']] : $media['source']);
}
$type = '.'.substr($media['media'], strrpos($media['media'], '.')+1);
$file = $dir.'/'.$media['device_name'].'-'.date('YmdA', strtotime($media['created_at'])).'-'.date('His', strtotime($media['created_at'])).'-'.strtotime($media['created_at']).$source.$type;
if(!is_file($file))
{
$result = $this->curl('https://'.$this->account['region'].$media['media'], [], [
'Host: '.$this->account['region'],
'token-auth: '.$this->account['auth']['token']
], 'GET', $file);
file_put_contents($file, $result);
touch($file, strtotime($media['created_at']));
$m++;
}
}
$p++;
}
else
{
break;
}
}
return $m;
}
public function redirect($uri)
{
header('Location: '.$uri);
exit();
}
public function refresh()
{
return $this->redirect('/');
}
public function regions()
{
$result = $this->curl('https://rest-prod.immedia-semi.com/regions', [], [], 'GET');
if(!empty($result['regions']))
{
foreach($result['regions'] as $region => $name)
{
$this->regions['rest-'.$region.'.immedia-semi.com'] = ucwords(strtolower($name['friendly_name']), '- ');
}
$this->regions['rest-prod.immedia-semi.com'] = 'General';
ksort($this->regions);
return $this->regions;
}
return false;
}
public function telegram($message = false, $chat = null, $reply = null)
{
$chat = (is_null($chat) ? $this->telegram['chat'] : $chat);
$message = (empty($message) ? false : $message);
return $this->curl('https://api.telegram.org/bot'.$this->telegram['bot'].'/sendMessage', [
'chat_id' => $this->telegram['chat'],
'parse_mode' => 'HTML',
'reply_to_message_id' => (is_null($reply) ? '' : $reply),
'text' => $message
], [
'Content-Type: application/json'
]);
}
public function verify($data = [])
{
if($this->is('telegram'))
{
$this->account(null, 'account');
}
$result = $this->curl('https://'.$this->account['region'].'/api/v4/account/'.$this->account['account']['account_id'].'/client/'.$this->account['account']['client_id'].'/pin/verify', [
'pin' => $data['pin']
], [
'Host: '.$this->account['region'],
'token-auth: '.$this->account['auth']['token']
]);
if(empty($result['valid']))
{
$this->error = 'Verify'.(empty($result['message']) ? '</b>!' : ':</b> '.$result['message']);
return false;
}
$this->account = array_merge($this->account, ['verify' => $data['pin']]);
$this->account($this->account, 'account');
return $this->account;
}
}
$blink = (new \Blink());
if($blink->is('cron'))
{
if($blink->login(array_merge($blink->account['credentials'], ['region' => $blink->account['region']])))
{
$blink->telegram('What\'s Amazon\'s Blink verificationcode?');
}
}
elseif($blink->is('telegram'))
{
$data = json_decode(file_get_contents('php://input'), true);
if(is_array($data))
{
$chat = $data['message']['chat']['id'];
$reply = $data['message']['message_id'];
$text = trim($data['message']['text']);
if(preg_match('~^([0-9]{6})$~', $text))
{
$verify = $blink->verify(['pin' => $text]);
if($verify)
{
if($blink->homescreen())
{
$medias = $blink->medias();
$blink->telegram($medias.' files downloaded!', $chat, $reply);
}
else
{
$blink->telegram('Can\'t find any file..', $chat, $reply);
}
}
else
{
$blink->telegram('Code "'.$text.'" is incorrect, try again?', $chat, $reply);
}
}
else
{
switch($text)
{
case '/blink': {
if($blink->login(array_merge($blink->account['credentials'], ['region' => $blink->account['region']])))
{
$blink->telegram('What\'s Amazon\'s Blink verificationcode?', $chat, $reply);
}
break;
}
}
}
}
}
else
{ ?>
<!DOCTYPE html>
<html>
<head>
<title>Sync Blink 2 Synology</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
h1 {
font-size: 1.5em;
color: #525252;
}
body {
font-family: 'Open Sans', sans-serif;
background: #3498db;
margin: 0 auto;
width: 100%;
text-align: center;
margin: 20px 0px 20px 0px;
}
.box {
background: #fff;
width: 300px;
border-radius: 6px;
margin: 0 auto 0 auto;
padding: 0px 0px 10px 0px;
border: #2980b9 4px solid;
}
.footer {
padding: 5px;
text-align: left;
}
button {
background: #3498db;
width: 90%;
padding: 8px;
color: white;
border-radius: 4px;
border: #3498db 1px solid;
margin: 25px auto 0px;
font-weight: bold;
font-size: 0.8em;
}
button:hover {
background: #fff;
color: #3498db;
cursor: pointer;
}
input, select {
border: #ccc 1px solid;
border-bottom: #ccc 2px solid;
padding: 8px;
width: 250px;
margin-top: 10px;
font-size: 1em;
border-radius: 4px;
}
select {
width: 265px;
}
input:focus, select:focus {
background: #eee;
}
label {
display: inline-block;
padding: 8px;
width: 250px;
font-size: 1em;
font-weight: bold;
text-align: left !important;
}
</style>
</head>
<body>
<div class="box">
<h1>Sync Blink 2 Synology</h1>
<?php
if(isset($_GET['logout']))
{
$blink->logout();
}
elseif(isset($_REQUEST['email'], $_REQUEST['password'], $_REQUEST['region']))
{
if($blink->login($_REQUEST))
{
echo $blink->form('pin');
}
else
{
echo $blink->form('login');
}
}
elseif(isset($_REQUEST['pin']))
{
if($blink->verify($_REQUEST))
{
$blink->refresh();
}
else
{
echo $blink->form('pin');
}
}
else
{
if($blink->is(['account', 'verify']))
{
if($blink->homescreen())
{
$medias = $blink->medias();
echo $medias.' files downloaded!';
}
}
else
{
echo $blink->form('login');
}
}
?>
<hr>
<div class="footer">
<b>Error <?=(is_null($blink->error) ? ':</b> -' : $blink->error);?>
</div>
<?php
if(isset($blink->account['auth']))
{
echo '<p><a href="/?logout">Logout</a></p>';
}
?>
</div>
</body>
</html>
<?php }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment