init: workspace opencode sync
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
# Telegram Expiry Notifier + Reset Button — Deploy ke Server
|
||||
|
||||
## Overview
|
||||
|
||||
Mengganti direct call Telegram API dari RouterOS dengan PHP intermediary agar bisa kirim inline button "Reset" untuk user hotspot yang expired.
|
||||
|
||||
### Arsitektur
|
||||
|
||||
```
|
||||
RouterOS scheduler (Family)
|
||||
↓ call HTTP GET
|
||||
↓
|
||||
notify-expire.php (server)
|
||||
↓ POST ke Telegram API
|
||||
↓
|
||||
⚠️ User X expired [🔄 Reset] (ke topik Notif Expire)
|
||||
↓ admin klik "Reset"
|
||||
↓ callback_query = "reset_X" ke Bot API
|
||||
↓
|
||||
tg-cb-poll scheduler (router, tiap 60s)
|
||||
↓ call HTTP GET
|
||||
↓
|
||||
telegram-cb.php (server)
|
||||
↓ polling getUpdates → proses callback
|
||||
↓ RouterOS API: set limit-uptime=0, comment="", reset-counters
|
||||
↓
|
||||
✅ User X has been reset (ke topik Notif Expire)
|
||||
```
|
||||
|
||||
### Files
|
||||
|
||||
| File | Fungsi |
|
||||
|------|--------|
|
||||
| `notify-expire.php` | Kirim notif + inline button ke Telegram |
|
||||
| `telegram-cb.php` | Polling callback query + eksekusi reset |
|
||||
| `setup.sh` | Setup script untuk server (install PHP + deploy) |
|
||||
|
||||
### Prerequisites (Server)
|
||||
|
||||
- Linux server (tested on Ubuntu/Debian)
|
||||
- PHP 7.4+ dengan `curl` extension
|
||||
- Akses keluar ke `api.telegram.org` (port 443)
|
||||
- Akses ke RouterOS API (`192.168.100.2:8728`)
|
||||
- `curl` binary di PATH (untuk internal call dari PHP ke Telegram)
|
||||
|
||||
### Setup Cepat
|
||||
|
||||
```bash
|
||||
# 1. Copy files ke server
|
||||
scp notify-expire.php player@somewhere:/home/player/telegram-notif/
|
||||
scp telegram-cb.php player@somewhere:/home/player/telegram-notif/
|
||||
scp setup.sh player@somewhere:/home/player/telegram-notif/
|
||||
|
||||
# 2. SSH ke server
|
||||
ssh player@somewhere
|
||||
|
||||
# 3. Run setup
|
||||
cd ~/telegram-notif
|
||||
chmod +x setup.sh
|
||||
./setup.sh
|
||||
|
||||
# 4. Start PHP built-in server (background)
|
||||
nohup php -S 0.0.0.0:8080 -t /home/player/telegram-notif > /tmp/telegram-notif.log 2>&1 &
|
||||
```
|
||||
|
||||
### Update RouterOS Scheduler
|
||||
|
||||
#### A. Family scheduler — notifikasi
|
||||
|
||||
Ganti on-event:
|
||||
|
||||
```bash
|
||||
# OLD
|
||||
/tool fetch url=("https://api.telegram.org/bot" . $bot . "/sendMessage?chat_id=" . $chat . "&text=" . $msg) http-method=get keep-result=no
|
||||
|
||||
# NEW — arahkan ke IP Tailscale server
|
||||
/tool fetch url=("http://100.100.31.46:8080/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no
|
||||
```
|
||||
|
||||
#### B. Tambah scheduler polling callback
|
||||
|
||||
```bash
|
||||
/system scheduler add name=tg-cb-poll interval=00:01:00 start-time=startup on-event=":delay 10s;/tool fetch url=\"http://100.100.31.46:8080/telegram-cb.php?key=msr-2026-reset\" http-method=get keep-result=no" disabled=no
|
||||
```
|
||||
|
||||
### Secret Key
|
||||
|
||||
`msr-2026-reset` — shared secret di URL parameter `key`. Diganti sesuai kebutuhan.
|
||||
|
||||
### Bot Config
|
||||
|
||||
- Token: `5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk`
|
||||
- Group: `-1002554941429` (laporkan)
|
||||
- Topic ID: `42` (Notif Expire)
|
||||
|
||||
### RouterOS Credentials (untuk callback reset)
|
||||
|
||||
- IP: `192.168.100.2`
|
||||
- User: `mi4`
|
||||
- Password: `m14`
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
**Logs**:
|
||||
```bash
|
||||
tail -f /tmp/telegram-notif.log
|
||||
```
|
||||
|
||||
**Test notifikasi**:
|
||||
```bash
|
||||
curl "http://localhost:8080/notify-expire.php?key=msr-2026-reset&name=test_user"
|
||||
```
|
||||
|
||||
**Test reset langsung**:
|
||||
```bash
|
||||
curl "http://localhost:8080/telegram-cb.php?key=msr-2026-reset&test=test_user"
|
||||
```
|
||||
|
||||
**PHP curl extension**:
|
||||
```bash
|
||||
sudo apt install php-curl
|
||||
```
|
||||
|
||||
### File Reference
|
||||
|
||||
Source files di `C:\Users\Admin\dev\Mikhmon Server\mikhmon\`:
|
||||
- `notify-expire.php`
|
||||
- `process/telegram-cb.php`
|
||||
- `lib/routeros_api.class.php` (dependency untuk koneksi ke RouterOS)
|
||||
@@ -0,0 +1,44 @@
|
||||
# Telegram Expiry Notifier — Deploy ke somewhere
|
||||
# Jalankan script ini. Masukkan password SSH player@somewhere ketika diminta.
|
||||
|
||||
$server = "player@somewhere"
|
||||
$remoteDir = "/home/player/telegram-notif"
|
||||
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
|
||||
Write-Host "=== Deploy Telegram Notifier ke $server ===" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
|
||||
# 1. Buat remote directory via SSH
|
||||
Write-Host "[1/4] Membuat remote directory..." -ForegroundColor Yellow
|
||||
Start-Process -Wait -WindowStyle Normal -FilePath "ssh" -ArgumentList "-t $server mkdir -p $remoteDir"
|
||||
Write-Host ""
|
||||
|
||||
# 2. Copy files via SCP
|
||||
Write-Host "[2/4] Copy files via SCP..." -ForegroundColor Yellow
|
||||
Start-Process -Wait -WindowStyle Normal -FilePath "scp" -ArgumentList "$scriptDir\notify-expire.php $scriptDir\telegram-cb.php $scriptDir\routeros_api.class.php $scriptDir\setup.sh ${server}:${remoteDir}/"
|
||||
Write-Host ""
|
||||
|
||||
# 3. Run setup via SSH
|
||||
Write-Host "[3/4] Run remote setup..." -ForegroundColor Yellow
|
||||
Write-Host " (akan menginstall PHP + membuat systemd service)" -ForegroundColor Gray
|
||||
Start-Process -Wait -WindowStyle Normal -FilePath "ssh" -ArgumentList "-t $server cd $remoteDir && chmod +x setup.sh && ./setup.sh"
|
||||
Write-Host ""
|
||||
|
||||
# 4. Test
|
||||
Write-Host "[4/4] Test..." -ForegroundColor Yellow
|
||||
Start-Process -Wait -WindowStyle Normal -FilePath "ssh" -ArgumentList "-t $server curl -s 'http://localhost:8080/notify-expire.php?key=msr-2026-reset&name=test_deploy'"
|
||||
Write-Host ""
|
||||
|
||||
Write-Host "=== Done ===" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Update RouterOS scheduler:"
|
||||
Write-Host " Family on-event:"
|
||||
Write-Host ' /tool fetch url=("http://100.100.31.46:8080/notify-expire.php?key=msr-2026-reset&name=" . $name) http-method=get keep-result=no'
|
||||
Write-Host ""
|
||||
Write-Host " Tambah scheduler:"
|
||||
Write-Host ' /system scheduler add name=tg-cb-poll interval=00:01:00 start-time=startup on-event=":delay 10s;/tool fetch url=\"http://100.100.31.46:8080/telegram-cb.php?key=msr-2026-reset\" http-method=get keep-result=no" disabled=no'
|
||||
Write-Host ""
|
||||
Write-Host "Remote management:"
|
||||
Write-Host " ssh $server"
|
||||
Write-Host " sudo systemctl status telegram-notif"
|
||||
Write-Host " sudo journalctl -u telegram-notif -f"
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
error_reporting(0);
|
||||
|
||||
$secret = 'msr-2026-reset';
|
||||
if (!isset($_GET['key']) || $_GET['key'] !== $secret) { http_response_code(403); die('auth failed'); }
|
||||
|
||||
$user = preg_replace('/[^a-zA-Z0-9_.@-]/', '', $_GET['name']);
|
||||
if (!$user) { http_response_code(400); die('missing name'); }
|
||||
|
||||
$bot = '5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk';
|
||||
$chat = '-1002554941429';
|
||||
$thread = 42;
|
||||
|
||||
$text = "⚠️ User *{$user}* expired";
|
||||
$markup = json_encode(array(
|
||||
'inline_keyboard' => array(array(array(
|
||||
'text' => '🔄 Reset',
|
||||
'callback_data' => "reset_{$user}"
|
||||
)))
|
||||
));
|
||||
|
||||
$data = http_build_query(array(
|
||||
'chat_id' => $chat,
|
||||
'message_thread_id' => $thread,
|
||||
'text' => $text,
|
||||
'parse_mode' => 'Markdown',
|
||||
'reply_markup' => $markup,
|
||||
));
|
||||
|
||||
function tg_post($url, $data) {
|
||||
$ctx = stream_context_create(array('http' => array(
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-Type: application/x-www-form-urlencoded',
|
||||
'content' => $data,
|
||||
'timeout' => 10,
|
||||
)));
|
||||
return @file_get_contents($url, false, $ctx);
|
||||
}
|
||||
|
||||
$r = tg_post("https://api.telegram.org/bot{$bot}/sendMessage", $data);
|
||||
if ($r === false) { http_response_code(500); echo "fail: http error"; exit; }
|
||||
|
||||
$result = json_decode($r, true);
|
||||
if (isset($result['ok']) && $result['ok']) {
|
||||
echo "sent: " . $result['result']['message_id'];
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo "fail: " . (isset($result['description']) ? $result['description'] : 'no response');
|
||||
}
|
||||
@@ -0,0 +1,647 @@
|
||||
<?php
|
||||
/*****************************
|
||||
*
|
||||
* RouterOS PHP API class v1.6
|
||||
* Author: Denis Basta
|
||||
* Contributors:
|
||||
* Nick Barnes
|
||||
* Ben Menking (ben [at] infotechsc [dot] com)
|
||||
* Jeremy Jefferson (http://jeremyj.com)
|
||||
* Cristian Deluxe (djcristiandeluxe [at] gmail [dot] com)
|
||||
* Mikhail Moskalev (mmv.rus [at] gmail [dot] com)
|
||||
*
|
||||
* http://www.mikrotik.com
|
||||
* http://wiki.mikrotik.com/wiki/API_PHP_class
|
||||
*
|
||||
******************************/
|
||||
|
||||
class RouterosAPI
|
||||
{
|
||||
var $debug = false; // Show debug information
|
||||
var $connected = false; // Connection state
|
||||
var $port = 8728; // Port to connect to (default 8729 for ssl)
|
||||
var $ssl = false; // Connect using SSL (must enable api-ssl in IP/Services)
|
||||
var $timeout = 3; // Connection attempt timeout and data read timeout
|
||||
var $attempts = 5; // Connection attempt count
|
||||
var $delay = 3; // Delay between connection attempts in seconds
|
||||
|
||||
var $socket; // Variable for storing socket resource
|
||||
var $error_no; // Variable for storing connection error number, if any
|
||||
var $error_str; // Variable for storing connection error text, if any
|
||||
|
||||
/* Check, can be var used in foreach */
|
||||
public function isIterable($var)
|
||||
{
|
||||
return $var !== null
|
||||
&& (is_array($var)
|
||||
|| $var instanceof Traversable
|
||||
|| $var instanceof Iterator
|
||||
|| $var instanceof IteratorAggregate
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Print text for debug purposes
|
||||
*
|
||||
* @param string $text Text to print
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function debug($text)
|
||||
{
|
||||
if ($this->debug) {
|
||||
echo $text . "\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
* @param string $length
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function encodeLength($length)
|
||||
{
|
||||
if ($length < 0x80) {
|
||||
$length = chr($length);
|
||||
} elseif ($length < 0x4000) {
|
||||
$length |= 0x8000;
|
||||
$length = chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
|
||||
} elseif ($length < 0x200000) {
|
||||
$length |= 0xC00000;
|
||||
$length = chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
|
||||
} elseif ($length < 0x10000000) {
|
||||
$length |= 0xE0000000;
|
||||
$length = chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
|
||||
} elseif ($length >= 0x10000000) {
|
||||
$length = chr(0xF0) . chr(($length >> 24) & 0xFF) . chr(($length >> 16) & 0xFF) . chr(($length >> 8) & 0xFF) . chr($length & 0xFF);
|
||||
}
|
||||
|
||||
return $length;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Login to RouterOS
|
||||
*
|
||||
* @param string $ip Hostname (IP or domain) of the RouterOS server
|
||||
* @param string $login The RouterOS username
|
||||
* @param string $password The RouterOS password
|
||||
*
|
||||
* @return boolean If we are connected or not
|
||||
*/
|
||||
public function connect($ip, $login, $password)
|
||||
{
|
||||
for ($ATTEMPT = 1; $ATTEMPT <= $this->attempts; $ATTEMPT++) {
|
||||
$this->connected = false;
|
||||
$PROTOCOL = ($this->ssl ? 'ssl://' : '' );
|
||||
$context = stream_context_create(array('ssl' => array('ciphers' => 'ADH:ALL', 'verify_peer' => false, 'verify_peer_name' => false)));
|
||||
$this->debug('Connection attempt #' . $ATTEMPT . ' to ' . $PROTOCOL . $ip . ':' . $this->port . '...');
|
||||
$this->socket = @stream_socket_client($PROTOCOL . $ip.':'. $this->port, $this->error_no, $this->error_str, $this->timeout, STREAM_CLIENT_CONNECT,$context);
|
||||
if ($this->socket) {
|
||||
socket_set_timeout($this->socket, $this->timeout);
|
||||
$this->write('/login', false);
|
||||
$this->write('=name=' . $login, false);
|
||||
$this->write('=password=' . $password);
|
||||
$RESPONSE = $this->read(false);
|
||||
if (isset($RESPONSE[0])) {
|
||||
if ($RESPONSE[0] == '!done') {
|
||||
if (!isset($RESPONSE[1])) {
|
||||
// Login method post-v6.43
|
||||
$this->connected = true;
|
||||
break;
|
||||
} else {
|
||||
// Login method pre-v6.43
|
||||
$MATCHES = array();
|
||||
if (preg_match_all('/[^=]+/i', $RESPONSE[1], $MATCHES)) {
|
||||
if ($MATCHES[0][0] == 'ret' && strlen($MATCHES[0][1]) == 32) {
|
||||
$this->write('/login', false);
|
||||
$this->write('=name=' . $login, false);
|
||||
$this->write('=response=00' . md5(chr(0) . $password . pack('H*', $MATCHES[0][1])));
|
||||
$RESPONSE = $this->read(false);
|
||||
if (isset($RESPONSE[0]) && $RESPONSE[0] == '!done') {
|
||||
$this->connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose($this->socket);
|
||||
}
|
||||
sleep($this->delay);
|
||||
}
|
||||
|
||||
if ($this->connected) {
|
||||
$this->debug('Connected...');
|
||||
} else {
|
||||
$this->debug('Error...');
|
||||
}
|
||||
return $this->connected;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Disconnect from RouterOS
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
// let's make sure this socket is still valid. it may have been closed by something else
|
||||
if( is_resource($this->socket) ) {
|
||||
fclose($this->socket);
|
||||
}
|
||||
$this->connected = false;
|
||||
$this->debug('Disconnected...');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse response from Router OS
|
||||
*
|
||||
* @param array $response Response data
|
||||
*
|
||||
* @return array Array with parsed data
|
||||
*/
|
||||
public function parseResponse($response)
|
||||
{
|
||||
if (is_array($response)) {
|
||||
$PARSED = array();
|
||||
$CURRENT = null;
|
||||
$singlevalue = null;
|
||||
foreach ($response as $x) {
|
||||
if (in_array($x, array('!fatal','!re','!trap'))) {
|
||||
if ($x == '!re') {
|
||||
$CURRENT =& $PARSED[];
|
||||
} else {
|
||||
$CURRENT =& $PARSED[$x][];
|
||||
}
|
||||
} elseif ($x != '!done') {
|
||||
$MATCHES = array();
|
||||
if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
|
||||
if ($MATCHES[0][0] == 'ret') {
|
||||
$singlevalue = $MATCHES[0][1];
|
||||
}
|
||||
$CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($PARSED) && !is_null($singlevalue)) {
|
||||
$PARSED = $singlevalue;
|
||||
}
|
||||
|
||||
return $PARSED;
|
||||
} else {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Parse response from Router OS
|
||||
*
|
||||
* @param array $response Response data
|
||||
*
|
||||
* @return array Array with parsed data
|
||||
*/
|
||||
public function parseResponse4Smarty($response)
|
||||
{
|
||||
if (is_array($response)) {
|
||||
$PARSED = array();
|
||||
$CURRENT = null;
|
||||
$singlevalue = null;
|
||||
foreach ($response as $x) {
|
||||
if (in_array($x, array('!fatal','!re','!trap'))) {
|
||||
if ($x == '!re') {
|
||||
$CURRENT =& $PARSED[];
|
||||
} else {
|
||||
$CURRENT =& $PARSED[$x][];
|
||||
}
|
||||
} elseif ($x != '!done') {
|
||||
$MATCHES = array();
|
||||
if (preg_match_all('/[^=]+/i', $x, $MATCHES)) {
|
||||
if ($MATCHES[0][0] == 'ret') {
|
||||
$singlevalue = $MATCHES[0][1];
|
||||
}
|
||||
$CURRENT[$MATCHES[0][0]] = (isset($MATCHES[0][1]) ? $MATCHES[0][1] : '');
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($PARSED as $key => $value) {
|
||||
$PARSED[$key] = $this->arrayChangeKeyName($value);
|
||||
}
|
||||
return $PARSED;
|
||||
if (empty($PARSED) && !is_null($singlevalue)) {
|
||||
$PARSED = $singlevalue;
|
||||
}
|
||||
} else {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change "-" and "/" from array key to "_"
|
||||
*
|
||||
* @param array $array Input array
|
||||
*
|
||||
* @return array Array with changed key names
|
||||
*/
|
||||
public function arrayChangeKeyName(&$array)
|
||||
{
|
||||
if (is_array($array)) {
|
||||
foreach ($array as $k => $v) {
|
||||
$tmp = str_replace("-", "_", $k);
|
||||
$tmp = str_replace("/", "_", $tmp);
|
||||
if ($tmp) {
|
||||
$array_new[$tmp] = $v;
|
||||
} else {
|
||||
$array_new[$k] = $v;
|
||||
}
|
||||
}
|
||||
return $array_new;
|
||||
} else {
|
||||
return $array;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read data from Router OS
|
||||
*
|
||||
* @param boolean $parse Parse the data? default: true
|
||||
*
|
||||
* @return array Array with parsed or unparsed data
|
||||
*/
|
||||
public function read($parse = true)
|
||||
{
|
||||
$RESPONSE = array();
|
||||
$receiveddone = false;
|
||||
while (true) {
|
||||
// Read the first byte of input which gives us some or all of the length
|
||||
// of the remaining reply.
|
||||
$BYTE = ord(fread($this->socket, 1));
|
||||
$LENGTH = 0;
|
||||
// If the first bit is set then we need to remove the first four bits, shift left 8
|
||||
// and then read another byte in.
|
||||
// We repeat this for the second and third bits.
|
||||
// If the fourth bit is set, we need to remove anything left in the first byte
|
||||
// and then read in yet another byte.
|
||||
if ($BYTE & 128) {
|
||||
if (($BYTE & 192) == 128) {
|
||||
$LENGTH = (($BYTE & 63) << 8) + ord(fread($this->socket, 1));
|
||||
} else {
|
||||
if (($BYTE & 224) == 192) {
|
||||
$LENGTH = (($BYTE & 31) << 8) + ord(fread($this->socket, 1));
|
||||
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
|
||||
} else {
|
||||
if (($BYTE & 240) == 224) {
|
||||
$LENGTH = (($BYTE & 15) << 8) + ord(fread($this->socket, 1));
|
||||
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
|
||||
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
|
||||
} else {
|
||||
$LENGTH = ord(fread($this->socket, 1));
|
||||
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
|
||||
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
|
||||
$LENGTH = ($LENGTH << 8) + ord(fread($this->socket, 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$LENGTH = $BYTE;
|
||||
}
|
||||
|
||||
$_ = "";
|
||||
|
||||
// If we have got more characters to read, read them in.
|
||||
if ($LENGTH > 0) {
|
||||
$_ = "";
|
||||
$retlen = 0;
|
||||
while ($retlen < $LENGTH) {
|
||||
$toread = $LENGTH - $retlen;
|
||||
$_ .= fread($this->socket, $toread);
|
||||
$retlen = strlen($_);
|
||||
}
|
||||
$RESPONSE[] = $_;
|
||||
$this->debug('>>> [' . $retlen . '/' . $LENGTH . '] bytes read.');
|
||||
}
|
||||
|
||||
// If we get a !done, make a note of it.
|
||||
if ($_ == "!done") {
|
||||
$receiveddone = true;
|
||||
}
|
||||
|
||||
$STATUS = socket_get_status($this->socket);
|
||||
if ($LENGTH > 0) {
|
||||
$this->debug('>>> [' . $LENGTH . ', ' . $STATUS['unread_bytes'] . ']' . $_);
|
||||
}
|
||||
|
||||
if ((!$this->connected && !$STATUS['unread_bytes']) || ($this->connected && !$STATUS['unread_bytes'] && $receiveddone)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($parse) {
|
||||
$RESPONSE = $this->parseResponse($RESPONSE);
|
||||
}
|
||||
|
||||
return $RESPONSE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write (send) data to Router OS
|
||||
*
|
||||
* @param string $command A string with the command to send
|
||||
* @param mixed $param2 If we set an integer, the command will send this data as a "tag"
|
||||
* If we set it to boolean true, the funcion will send the comand and finish
|
||||
* If we set it to boolean false, the funcion will send the comand and wait for next command
|
||||
* Default: true
|
||||
*
|
||||
* @return boolean Return false if no command especified
|
||||
*/
|
||||
public function write($command, $param2 = true)
|
||||
{
|
||||
if ($command) {
|
||||
$data = explode("\n", $command);
|
||||
foreach ($data as $com) {
|
||||
$com = trim($com);
|
||||
fwrite($this->socket, $this->encodeLength(strlen($com)) . $com);
|
||||
$this->debug('<<< [' . strlen($com) . '] ' . $com);
|
||||
}
|
||||
|
||||
if (gettype($param2) == 'integer') {
|
||||
fwrite($this->socket, $this->encodeLength(strlen('.tag=' . $param2)) . '.tag=' . $param2 . chr(0));
|
||||
$this->debug('<<< [' . strlen('.tag=' . $param2) . '] .tag=' . $param2);
|
||||
} elseif (gettype($param2) == 'boolean') {
|
||||
fwrite($this->socket, ($param2 ? chr(0) : ''));
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Write (send) data to Router OS
|
||||
*
|
||||
* @param string $com A string with the command to send
|
||||
* @param array $arr An array with arguments or queries
|
||||
*
|
||||
* @return array Array with parsed
|
||||
*/
|
||||
public function comm($com, $arr = array())
|
||||
{
|
||||
$count = count($arr);
|
||||
$this->write($com, !$arr);
|
||||
$i = 0;
|
||||
if ($this->isIterable($arr)) {
|
||||
foreach ($arr as $k => $v) {
|
||||
switch ($k[0]) {
|
||||
case "?":
|
||||
$el = "$k=$v";
|
||||
break;
|
||||
case "~":
|
||||
$el = "$k~$v";
|
||||
break;
|
||||
default:
|
||||
$el = "=$k=$v";
|
||||
break;
|
||||
}
|
||||
|
||||
$last = ($i++ == $count - 1);
|
||||
$this->write($el, $last);
|
||||
}
|
||||
}
|
||||
|
||||
return $this->read();
|
||||
}
|
||||
|
||||
/**
|
||||
* Standard destructor
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
$this->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
// encrypt decript
|
||||
|
||||
function encrypt($string, $key=128) {
|
||||
$result = '';
|
||||
for($i=0, $k= strlen($string); $i<$k; $i++) {
|
||||
$char = substr($string, $i, 1);
|
||||
$keychar = substr($key, ($i % strlen($key))-1, 1);
|
||||
$char = chr(ord($char)+ord($keychar));
|
||||
$result .= $char;
|
||||
}
|
||||
return base64_encode($result);
|
||||
}
|
||||
function decrypt($string, $key=128) {
|
||||
$result = '';
|
||||
$string = base64_decode($string);
|
||||
for($i=0, $k=strlen($string); $i< $k ; $i++) {
|
||||
$char = substr($string, $i, 1);
|
||||
$keychar = substr($key, ($i % strlen($key))-1, 1);
|
||||
$char = chr(ord($char)-ord($keychar));
|
||||
$result .= $char;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Reformat date time MikroTik
|
||||
// by Laksamadi Guko
|
||||
|
||||
function formatInterval($dtm){
|
||||
$val_convert = $dtm;
|
||||
$new_format = str_replace("s", "", str_replace("m", "m ", str_replace("h", "h ", str_replace("d", "d ", str_replace("w", "w ", $val_convert)))));
|
||||
return $new_format;
|
||||
}
|
||||
|
||||
function formatDTM($dtm){
|
||||
if(substr($dtm, 1,1) == "d" || substr($dtm, 2,1) == "d"){
|
||||
$day = explode("d",$dtm)[0]."d";
|
||||
$day = str_replace("d", "d ", str_replace("w", "w ", $day));
|
||||
$dtm = explode("d",$dtm)[1];
|
||||
}elseif(substr($dtm, 1,1) == "w" && substr($dtm, 3,1) == "d" || substr($dtm, 2,1) == "w" && substr($dtm, 4,1) == "d"){
|
||||
$day = explode("d",$dtm)[0]."d";
|
||||
$day = str_replace("d", "d ", str_replace("w", "w ", $day));
|
||||
$dtm = explode("d",$dtm)[1];
|
||||
}elseif (substr($dtm, 1,1) == "w" || substr($dtm, 2,1) == "w" ) {
|
||||
$day = explode("w",$dtm)[0]."w";
|
||||
$day = str_replace("d", "d ", str_replace("w", "w ", $day));
|
||||
$dtm = explode("w",$dtm)[1];
|
||||
}
|
||||
|
||||
// secs
|
||||
if(strlen($dtm) == "2" && substr($dtm, -1) == "s"){
|
||||
$format = $day." 00:00:0".substr($dtm, 0,-1);
|
||||
}elseif(strlen($dtm) == "3" && substr($dtm, -1) == "s"){
|
||||
$format = $day." 00:00:".substr($dtm, 0,-1);
|
||||
//minutes
|
||||
}elseif(strlen($dtm) == "2" && substr($dtm, -1) == "m"){
|
||||
$format = $day." 00:0".substr($dtm, 0,-1).":00";
|
||||
}elseif(strlen($dtm) == "3" && substr($dtm, -1) == "m"){
|
||||
$format = $day." 00:".substr($dtm, 0,-1).":00";
|
||||
//hours
|
||||
}elseif(strlen($dtm) == "2" && substr($dtm, -1) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,-1).":00:00";
|
||||
}elseif(strlen($dtm) == "3" && substr($dtm, -1) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,-1).":00:00";
|
||||
|
||||
//minutes -secs
|
||||
}elseif(strlen($dtm) == "4" && substr($dtm, -1) == "s" && substr($dtm,1,-2) == "m"){
|
||||
$format = $day." "."00:0".substr($dtm, 0,1).":0".substr($dtm, 2,-1);
|
||||
}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,1,-3) == "m"){
|
||||
$format = $day." "."00:0".substr($dtm, 0,1).":".substr($dtm, 2,-1);
|
||||
}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,2,-2) == "m"){
|
||||
$format = $day." "."00:".substr($dtm, 0,2).":0".substr($dtm, 3,-1);
|
||||
}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "s" && substr($dtm,2,-3) == "m"){
|
||||
$format = $day." "."00:".substr($dtm, 0,2).":".substr($dtm, 3,-1);
|
||||
|
||||
//hours -secs
|
||||
}elseif(strlen($dtm) == "4" && substr($dtm, -1) == "s" && substr($dtm,1,-2) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":00:0".substr($dtm, 2,-1);
|
||||
}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,1,-3) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":00:".substr($dtm, 2,-1);
|
||||
}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "s" && substr($dtm,2,-2) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":00:0".substr($dtm, 3,-1);
|
||||
}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "s" && substr($dtm,2,-3) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":00:".substr($dtm, 3,-1);
|
||||
|
||||
//hours -secs
|
||||
}elseif(strlen($dtm) == "4" && substr($dtm, -1) == "m" && substr($dtm,1,-2) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":0".substr($dtm, 2,-1).":00";
|
||||
}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "m" && substr($dtm,1,-3) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":".substr($dtm, 2,-1).":00";
|
||||
}elseif(strlen($dtm) == "5" && substr($dtm, -1) == "m" && substr($dtm,2,-2) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":0".substr($dtm, 3,-1).":00";
|
||||
}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "m" && substr($dtm,2,-3) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":".substr($dtm, 3,-1).":00";
|
||||
|
||||
//hours minutes secs
|
||||
}elseif(strlen($dtm) == "6" && substr($dtm, -1) == "s" && substr($dtm,3,-2) == "m" && substr($dtm,1,-4) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":0".substr($dtm, 2,-3).":0".substr($dtm, 4,-1);
|
||||
}elseif(strlen($dtm) == "7" && substr($dtm, -1) == "s" && substr($dtm,3,-3) == "m" && substr($dtm,1,-5) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":0".substr($dtm, 2,-4).":".substr($dtm, 4,-1);
|
||||
}elseif(strlen($dtm) == "7" && substr($dtm, -1) == "s" && substr($dtm,4,-2) == "m" && substr($dtm,1,-5) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":".substr($dtm, 2,-3).":0".substr($dtm, 5,-1);
|
||||
}elseif(strlen($dtm) == "8" && substr($dtm, -1) == "s" && substr($dtm,4,-3) == "m" && substr($dtm,1,-6) == "h"){
|
||||
$format = $day." 0".substr($dtm, 0,1).":".substr($dtm, 2,-4).":".substr($dtm, 5,-1);
|
||||
}elseif(strlen($dtm) == "7" && substr($dtm, -1) == "s" && substr($dtm,4,-2) == "m" && substr($dtm,2,-4) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":0".substr($dtm, 3,-3).":0".substr($dtm, 5,-1);
|
||||
}elseif(strlen($dtm) == "8" && substr($dtm, -1) == "s" && substr($dtm,4,-3) == "m" && substr($dtm,2,-5) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":0".substr($dtm, 3,-4).":".substr($dtm, 5,-1);
|
||||
}elseif(strlen($dtm) == "8" && substr($dtm, -1) == "s" && substr($dtm,5,-2) == "m" && substr($dtm,2,-5) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":".substr($dtm, 3,-3).":0".substr($dtm, 6,-1);
|
||||
}elseif(strlen($dtm) == "9" && substr($dtm, -1) == "s" && substr($dtm,5,-3) == "m" && substr($dtm,2,-6) == "h"){
|
||||
$format = $day." ".substr($dtm, 0,2).":".substr($dtm, 3,-4).":".substr($dtm, 6,-1);
|
||||
|
||||
}else{
|
||||
$format = $dtm;
|
||||
}
|
||||
return $format;
|
||||
}
|
||||
|
||||
|
||||
function randN($length) {
|
||||
$chars = "23456789";
|
||||
$charArray = str_split($chars);
|
||||
$charCount = strlen($chars);
|
||||
$result = "";
|
||||
for($i=1;$i<=$length;$i++)
|
||||
{
|
||||
$randChar = rand(0,$charCount-1);
|
||||
$result .= $charArray[$randChar];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function randUC($length) {
|
||||
$chars = "ABCDEFGHJKLMNPRSTUVWXYZ";
|
||||
$charArray = str_split($chars);
|
||||
$charCount = strlen($chars);
|
||||
$result = "";
|
||||
for($i=1;$i<=$length;$i++)
|
||||
{
|
||||
$randChar = rand(0,$charCount-1);
|
||||
$result .= $charArray[$randChar];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
function randLC($length) {
|
||||
$chars = "abcdefghijkmnprstuvwxyz";
|
||||
$charArray = str_split($chars);
|
||||
$charCount = strlen($chars);
|
||||
$result = "";
|
||||
for($i=1;$i<=$length;$i++)
|
||||
{
|
||||
$randChar = rand(0,$charCount-1);
|
||||
$result .= $charArray[$randChar];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function randULC($length) {
|
||||
$chars = "ABCDEFGHJKLMNPRSTUVWXYZabcdefghijkmnprstuvwxyz";
|
||||
$charArray = str_split($chars);
|
||||
$charCount = strlen($chars);
|
||||
$result = "";
|
||||
for($i=1;$i<=$length;$i++)
|
||||
{
|
||||
$randChar = rand(0,$charCount-1);
|
||||
$result .= $charArray[$randChar];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function randNLC($length) {
|
||||
$chars = "23456789abcdefghijkmnprstuvwxyz";
|
||||
$charArray = str_split($chars);
|
||||
$charCount = strlen($chars);
|
||||
$result = "";
|
||||
for($i=1;$i<=$length;$i++)
|
||||
{
|
||||
$randChar = rand(0,$charCount-1);
|
||||
$result .= $charArray[$randChar];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function randNUC($length) {
|
||||
$chars = "23456789ABCDEFGHJKLMNPRSTUVWXYZ";
|
||||
$charArray = str_split($chars);
|
||||
$charCount = strlen($chars);
|
||||
$result = "";
|
||||
for($i=1;$i<=$length;$i++)
|
||||
{
|
||||
$randChar = rand(0,$charCount-1);
|
||||
$result .= $charArray[$randChar];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
function randNULC($length) {
|
||||
$chars = "23456789ABCDEFGHJKLMNPRSTUVWXYZabcdefghijkmnprstuvwxyz";
|
||||
$charArray = str_split($chars);
|
||||
$charCount = strlen($chars);
|
||||
$result = "";
|
||||
for($i=1;$i<=$length;$i++)
|
||||
{
|
||||
$randChar = rand(0,$charCount-1);
|
||||
$result .= $charArray[$randChar];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Telegram Expiry Notifier — Setup ==="
|
||||
|
||||
DIR="/home/player/telegram-notif"
|
||||
PORT=8080
|
||||
SERVICE="telegram-notif"
|
||||
|
||||
# 1. Install PHP + curl
|
||||
echo "[1/4] Installing PHP + php-curl..."
|
||||
sudo apt update -qq
|
||||
sudo apt install -y -qq php-cli php-curl curl 2>/dev/null
|
||||
|
||||
# 2. Create directory
|
||||
echo "[2/4] Creating directory..."
|
||||
mkdir -p "$DIR"
|
||||
|
||||
# 3. Copy files (run this script from the same dir as the PHP files)
|
||||
echo "[3/4] Copying files..."
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
cp "$SCRIPT_DIR/notify-expire.php" "$DIR/"
|
||||
cp "$SCRIPT_DIR/telegram-cb.php" "$DIR/"
|
||||
|
||||
# 4. Create systemd service
|
||||
echo "[4/4] Creating systemd service..."
|
||||
sudo tee /etc/systemd/system/$SERVICE.service > /dev/null <<EOF
|
||||
[Unit]
|
||||
Description=Telegram Expiry Notifier
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/php -S 0.0.0.0:$PORT -t $DIR
|
||||
WorkingDirectory=$DIR
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
User=player
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable $SERVICE
|
||||
sudo systemctl restart $SERVICE
|
||||
|
||||
echo "=== Done ==="
|
||||
echo "Service: sudo systemctl status $SERVICE"
|
||||
echo "Test: curl http://localhost:$PORT/notify-expire.php?key=msr-2026-reset&name=test"
|
||||
echo "Logs: sudo journalctl -u $SERVICE -f"
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
error_reporting(0);
|
||||
|
||||
$secret = 'msr-2026-reset';
|
||||
if (!isset($_GET['key']) || $_GET['key'] !== $secret) { http_response_code(403); die('auth failed'); }
|
||||
|
||||
$bot = '5115036608:AAGeyXVq5eNvdAkaCQHU9NfH16uAAiSKiuk';
|
||||
|
||||
function tg_api($method, $params = array()) {
|
||||
global $bot;
|
||||
$url = "https://api.telegram.org/bot{$bot}/{$method}";
|
||||
if (!empty($params)) {
|
||||
$data = http_build_query($params);
|
||||
$ctx = stream_context_create(array('http' => array(
|
||||
'method' => 'POST',
|
||||
'header' => 'Content-Type: application/x-www-form-urlencoded',
|
||||
'content' => $data,
|
||||
'timeout' => 15,
|
||||
)));
|
||||
$r = @file_get_contents($url, false, $ctx);
|
||||
} else {
|
||||
$r = @file_get_contents($url, false, stream_context_create(array('http' => array('timeout' => 5))));
|
||||
}
|
||||
return $r ? json_decode($r, true) : null;
|
||||
}
|
||||
|
||||
if (isset($_GET['test'])) {
|
||||
$username = preg_replace('/[^a-zA-Z0-9_.@-]/', '', $_GET['test']);
|
||||
if (!$username) { die('invalid username'); }
|
||||
include_once(dirname(__DIR__) . '/lib/routeros_api.class.php');
|
||||
$api = new RouterosAPI();
|
||||
$api->debug = false;
|
||||
$api->port = 33206;
|
||||
if (!$api->connect('remote.vpnmurahjogja.my.id', 'mi4', 'm14')) { die('conn fail'); }
|
||||
$found = $api->comm('/ip/hotspot/user/print', array('?name' => $username));
|
||||
if (empty($found)) { $api->disconnect(); die('user not found'); }
|
||||
$uid = $found[0]['.id'];
|
||||
$api->comm('/ip/hotspot/user/set', array('.id' => $uid, 'limit-uptime' => '0', 'comment' => ''));
|
||||
$api->comm('/ip/hotspot/user/reset-counters', array('.id' => $uid));
|
||||
$sch = $api->comm('/system/scheduler/print', array('?name' => $username));
|
||||
if (!empty($sch) && isset($sch[0]['.id'])) {
|
||||
$api->comm('/system/scheduler/remove', array('.id' => $sch[0]['.id']));
|
||||
}
|
||||
$api->disconnect();
|
||||
tg_api('sendMessage', array(
|
||||
'chat_id' => '-1002554941429',
|
||||
'message_thread_id' => 42,
|
||||
'text' => "✅ *{$username}* has been reset\nReady to use again",
|
||||
'parse_mode' => 'Markdown',
|
||||
));
|
||||
echo "reset ok: {$username}";
|
||||
exit;
|
||||
}
|
||||
|
||||
$updates = tg_api('getUpdates', array('timeout' => 5, 'allowed_updates' => '["callback_query"]'));
|
||||
if (!$updates || !isset($updates['ok']) || !$updates['ok']) { echo "no updates"; exit; }
|
||||
|
||||
$processed = 0;
|
||||
$last_id = 0;
|
||||
|
||||
include_once(dirname(__DIR__) . '/lib/routeros_api.class.php');
|
||||
|
||||
foreach ($updates['result'] as $u) {
|
||||
$cb = isset($u['callback_query']) ? $u['callback_query'] : null;
|
||||
if (!$cb) continue;
|
||||
|
||||
$data = isset($cb['data']) ? $cb['data'] : '';
|
||||
$cb_id = isset($cb['id']) ? $cb['id'] : '';
|
||||
$last_id = max($last_id, $u['update_id']);
|
||||
|
||||
if (substr($data, 0, 6) !== 'reset_') continue;
|
||||
|
||||
$username = substr($data, 6);
|
||||
$username = preg_replace('/[^a-zA-Z0-9_.@-]/', '', $username);
|
||||
if (!$username) continue;
|
||||
|
||||
$api = new RouterosAPI();
|
||||
$api->debug = false;
|
||||
$api->port = 33206;
|
||||
if (!$api->connect('remote.vpnmurahjogja.my.id', 'mi4', 'm14')) {
|
||||
echo "conn fail for {$username} ";
|
||||
continue;
|
||||
}
|
||||
|
||||
$found = $api->comm('/ip/hotspot/user/print', array('?name' => $username));
|
||||
if (empty($found)) {
|
||||
$api->disconnect();
|
||||
tg_api('answerCallbackQuery', array(
|
||||
'callback_query_id' => $cb_id,
|
||||
'text' => "User {$username} not found",
|
||||
'show_alert' => true,
|
||||
));
|
||||
continue;
|
||||
}
|
||||
|
||||
$uid = $found[0]['.id'];
|
||||
$api->comm('/ip/hotspot/user/set', array('.id' => $uid, 'limit-uptime' => '0', 'comment' => ''));
|
||||
$api->comm('/ip/hotspot/user/reset-counters', array('.id' => $uid));
|
||||
|
||||
$sch = $api->comm('/system/scheduler/print', array('?name' => $username));
|
||||
if (!empty($sch) && isset($sch[0]['.id'])) {
|
||||
$api->comm('/system/scheduler/remove', array('.id' => $sch[0]['.id']));
|
||||
}
|
||||
|
||||
$api->disconnect();
|
||||
|
||||
tg_api('answerCallbackQuery', array(
|
||||
'callback_query_id' => $cb_id,
|
||||
'text' => "✅ {$username} reset!",
|
||||
'show_alert' => false,
|
||||
));
|
||||
|
||||
tg_api('sendMessage', array(
|
||||
'chat_id' => '-1002554941429',
|
||||
'message_thread_id' => 42,
|
||||
'text' => "✅ *{$username}* has been reset\nReady to use again",
|
||||
'parse_mode' => 'Markdown',
|
||||
));
|
||||
|
||||
$processed++;
|
||||
}
|
||||
|
||||
if ($last_id > 0) {
|
||||
tg_api('getUpdates', array('offset' => $last_id + 1, 'timeout' => 1));
|
||||
}
|
||||
|
||||
echo "processed: {$processed}";
|
||||
Reference in New Issue
Block a user