comletely delete old lws, rewrite vk_sms_checker on python

This commit is contained in:
Evgeny Zinoviev 2024-02-20 00:56:00 +03:00
parent 952e41d594
commit 95ac1f0d67
16 changed files with 151 additions and 1143 deletions

75
bin/vk_sms_checker.py Executable file
View File

@ -0,0 +1,75 @@
#!/usr/bin/env python3
import __py_include
import re
from html import escape
from typing import Optional
from homekit.config import AppConfigUnit, config
from homekit.modem import ModemsConfig, E3372
from homekit.database import MySQLHomeDatabase
from homekit.telegram import send_message
db: Optional[MySQLHomeDatabase] = None
class VkSmsCheckerConfig(AppConfigUnit):
NAME = 'vk_sms_checker'
@classmethod
def schema(cls) -> Optional[dict]:
return {
'modem': {'type': 'string', 'required': True}
}
@staticmethod
def custom_validator(data):
if data['modem'] not in ModemsConfig():
raise ValueError('invalid modem')
def get_last_time() -> int:
cur = db.cursor()
cur.execute("SELECT last_message_time FROM vk_sms LIMIT 1")
return int(cur.fetchone()[0])
def set_last_time(timestamp: int) -> None:
cur = db.cursor()
cur.execute("UPDATE vk_sms SET last_message_time=%s", (timestamp,))
db.commit()
def check_sms():
modem = ModemsConfig()[config.app_config['modem']]
cl = E3372(modem['ip'], legacy_token_auth=modem['legacy_auth'])
messages = cl.sms_list()
messages.reverse()
last_time = get_last_time()
new_last_time = None
results = []
if not messages:
return
for m in messages:
if m['UnixTime'] <= last_time:
continue
new_last_time = m['UnixTime']
if re.match(r'^vk', m['Phone'], flags=re.IGNORECASE) or re.match(r'vk', m['Content'], flags=re.IGNORECASE):
results.append(m)
if results:
for m in results:
text = '<b>'+escape(m['Phone'])+'</b> ('+m['Date']+')'
text += "\n"+escape(m['Content'])
send_message(text=text, chat='vk_sms_checker')
if new_last_time:
set_last_time(new_last_time)
if __name__ == '__main__':
db = MySQLHomeDatabase()
config.load_app(VkSmsCheckerConfig)
check_sms()

View File

@ -6,6 +6,7 @@ __all__ = [
'get_clickhouse',
'SimpleState',
'MySQLHomeDatabase',
'SensorsDatabase',
'InverterDatabase',
'BotsDatabase'
@ -14,12 +15,13 @@ __all__ = [
def __getattr__(name: str):
if name in __all__:
if name.endswith('Database'):
file = name[:-8].lower()
elif 'mysql' in name:
ln = name.lower()
if 'mysql' in ln:
file = 'mysql'
elif 'clickhouse' in name:
elif 'clickhouse' in ln:
file = 'clickhouse'
elif name.endswith('Database'):
file = name[:-8].lower()
else:
file = 'simple_state'

View File

@ -1,6 +1,6 @@
from .mysql import (
get_mysql as get_mysql,
mysql_now as mysql_now
mysql_now as mysql_now,
MySQLHomeDatabase as MySQLHomeDatabase
)
from .clickhouse import get_clickhouse as get_clickhouse

View File

@ -1,6 +1,6 @@
import pytz
from .mysql import mysql_now, MySQLDatabase, datetime_fmt
from .mysql import mysql_now, MySQLDatabase, datetime_fmt, MySQLCredsConfig, MySQLCloudCredsConfig
from ..api.types import (
SoundSensorLocation
)
@ -26,6 +26,9 @@ class OpenwrtLogRecord:
class BotsDatabase(MySQLDatabase):
def creds(self) -> MySQLCredsConfig:
return MySQLCloudCredsConfig()
def add_openwrt_logs(self,
lines: List[Tuple[datetime, str]],
access_point: int):

View File

@ -1,47 +1,74 @@
import time
import logging
from abc import ABC, abstractmethod
from mysql.connector import connect, MySQLConnection, Error
from typing import Optional
from ..config import config
from ..config import ConfigUnit
link: Optional[MySQLConnection] = None
logger = logging.getLogger(__name__)
datetime_fmt = '%Y-%m-%d %H:%M:%S'
def get_mysql() -> MySQLConnection:
global link
class MySQLCredsConfig(ConfigUnit, ABC):
@classmethod
def schema(cls) -> Optional[dict]:
schema = {}
for k in ('host', 'database', 'user', 'password'):
schema[k] = dict(type='string', required=True)
return schema
if link is not None:
return link
link = connect(
host=config['mysql']['host'],
user=config['mysql']['user'],
password=config['mysql']['password'],
database=config['mysql']['database'],
)
link.time_zone = '+01:00'
return link
class MySQLHomeCredsConfig(MySQLCredsConfig):
NAME = 'mysql_home_creds'
class MySQLCloudCredsConfig(MySQLCredsConfig):
NAME = 'mysql_cloud_creds'
def mysql_now() -> str:
return time.strftime('%Y-%m-%d %H:%M:%S')
class MySQLDatabase:
def __init__(self):
self.db = get_mysql()
class MySQLDatabase(ABC):
_enable_pings: bool
_link: MySQLConnection
_time_zone: Optional[str]
@abstractmethod
def creds(self) -> MySQLCredsConfig:
pass
def __init__(self, enable_pings=False, time_zone='+01:00'):
self._enable_pings = enable_pings
self._time_zone = time_zone
self._connect()
def _connect(self):
c = self.creds()
self._link = connect(
host=c['host'],
user=c['user'],
password=c['password'],
database=c['database'],
)
if self._time_zone:
self._link.time_zone = self._time_zone
def cursor(self, **kwargs):
try:
self.db.ping(reconnect=True, attempts=2)
except Error as e:
logger.exception(e)
self.db = get_mysql()
return self.db.cursor(**kwargs)
if self._enable_pings:
try:
self._link.ping(reconnect=True, attempts=2)
except Error as e:
logger.exception(e)
self._connect()
return self._link.cursor(**kwargs)
def commit(self):
self.db.commit()
self._link.commit()
class MySQLHomeDatabase(MySQLDatabase):
def creds(self) -> MySQLCredsConfig:
return MySQLHomeCredsConfig()

View File

@ -1,6 +1,7 @@
import requests
import xml.etree.ElementTree as ElementTree
from datetime import datetime
from ..util import Addr
from enum import Enum
from ..http import HTTPMethod
@ -21,14 +22,14 @@ class Error(Enum):
ERROR_WRONG_TOKEN = 125001
ERROR_WRONG_SESSION = 125002
ERROR_WRONG_SESSION_TOKEN = 125003
class WifiStatus(Enum):
WIFI_CONNECTING = '900'
WIFI_CONNECTED = '901'
WIFI_DISCONNECTED = '902'
WIFI_DISCONNECTING = '903'
class Cradle(Enum):
CRADLE_CONNECTING = '900'
@ -38,8 +39,8 @@ class Cradle(Enum):
CRADLE_CONNECTFAILED = '904'
CRADLE_CONNECTSTATUSNULL = '905'
CRANDLE_CONNECTSTATUSERRO = '906'
class MacroEVDOLevel(Enum):
MACRO_EVDO_LEVEL_ZERO = '0'
MACRO_EVDO_LEVEL_ONE = '1'
@ -47,8 +48,8 @@ class MacroEVDOLevel(Enum):
MACRO_EVDO_LEVEL_THREE = '3'
MACRO_EVDO_LEVEL_FOUR = '4'
MACRO_EVDO_LEVEL_FIVE = '5'
class MacroNetWorkType(Enum):
MACRO_NET_WORK_TYPE_NOSERVICE = 0
MACRO_NET_WORK_TYPE_GSM = 1
@ -127,7 +128,7 @@ class E3372:
_get_raw_data: bool
_headers: dict[str, str]
_authorized: bool
def __init__(self,
addr: Addr,
need_auth: bool = True,
@ -174,7 +175,7 @@ class E3372:
def sms_count(self):
self.auth()
return self.request('sms/sms-count')
def sms_send(self, phone: str, text: str):
self.auth()
return self.request('sms/send-sms', HTTPMethod.POST, {
@ -204,6 +205,7 @@ class E3372:
messages = []
for message_elem in root.find('Messages').findall('Message'):
message_dict = {child.tag: child.text for child in message_elem}
message_dict['UnixTime'] = int(datetime.strptime(message_dict['Date'], '%Y-%m-%d %H:%M:%S').timestamp())
messages.append(message_dict)
return messages

View File

@ -11,7 +11,7 @@ _logger = logging.getLogger(__name__)
def send_message(text: str,
chat: str,
parse_mode: str = 'HTML',
disable_web_page_preview: bool = False,):
disable_web_page_preview: bool = False):
data, token = _send_telegram_data(text, chat, parse_mode, disable_web_page_preview)
req = requests.post('https://api.telegram.org/bot%s/sendMessage' % token, data=data)
return req.json()

View File

@ -1,37 +0,0 @@
<?php
class TelegramBotClient {
protected string $token;
public function __construct(string $token) {
$this->token = $token;
}
public function sendMessage(int $chat_id, string $text): bool {
$ch = curl_init();
$url = 'https://api.telegram.org/bot'.$this->token.'/sendMessage';
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
curl_setopt($ch, CURLOPT_POSTFIELDS, [
'chat_id' => $chat_id,
'text' => $text,
'parse_mode' => 'html',
'disable_web_page_preview' => 1
]);
$body = curl_exec($ch);
curl_close($ch);
$resp = jsonDecode($body);
if (!$resp['ok']) {
debugError(__METHOD__ . ': ' . $body);
return false;
}
return true;
}
}

View File

@ -1,16 +0,0 @@
{
"name": "ch1p/localwebsite.homekit",
"type": "project",
"require": {
"twig/twig": "^3.3",
"ext-mbstring": "*",
"ext-sockets": "*",
"ext-simplexml": "*",
"ext-curl": "*",
"ext-json": "*",
"ext-gmp": "*",
"ext-sqlite3": "*",
"giggsey/libphonenumber-for-php": "^8.12"
},
"license": "MIT"
}

View File

@ -1,341 +0,0 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "aad58d1c2f9900517de6f62599845b12",
"packages": [
{
"name": "giggsey/libphonenumber-for-php",
"version": "8.12.51",
"source": {
"type": "git",
"url": "https://github.com/giggsey/libphonenumber-for-php.git",
"reference": "a42d89a46797083a95aa48393485fdac22fcac94"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/libphonenumber-for-php/zipball/a42d89a46797083a95aa48393485fdac22fcac94",
"reference": "a42d89a46797083a95aa48393485fdac22fcac94",
"shasum": ""
},
"require": {
"giggsey/locale": "^1.7|^2.0",
"php": ">=5.3.2",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
"pear/pear-core-minimal": "^1.9",
"pear/pear_exception": "^1.0",
"pear/versioncontrol_git": "^0.5",
"phing/phing": "^2.7",
"php-coveralls/php-coveralls": "^1.0|^2.0",
"symfony/console": "^2.8|^3.0|^v4.4|^v5.2",
"symfony/phpunit-bridge": "^4.2 || ^5"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "8.x-dev"
}
},
"autoload": {
"psr-4": {
"libphonenumber\\": "src/"
},
"exclude-from-classmap": [
"/src/data/",
"/src/carrier/data/",
"/src/geocoding/data/",
"/src/timezone/data/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Joshua Gigg",
"email": "giggsey@gmail.com",
"homepage": "https://giggsey.com/"
}
],
"description": "PHP Port of Google's libphonenumber",
"homepage": "https://github.com/giggsey/libphonenumber-for-php",
"keywords": [
"geocoding",
"geolocation",
"libphonenumber",
"mobile",
"phonenumber",
"validation"
],
"support": {
"irc": "irc://irc.appliedirc.com/lobby",
"issues": "https://github.com/giggsey/libphonenumber-for-php/issues",
"source": "https://github.com/giggsey/libphonenumber-for-php"
},
"time": "2022-07-11T08:12:34+00:00"
},
{
"name": "giggsey/locale",
"version": "2.2",
"source": {
"type": "git",
"url": "https://github.com/giggsey/Locale.git",
"reference": "9c1dca769253f6a3e81f9a5c167f53b6a54ab635"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/giggsey/Locale/zipball/9c1dca769253f6a3e81f9a5c167f53b6a54ab635",
"reference": "9c1dca769253f6a3e81f9a5c167f53b6a54ab635",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"require-dev": {
"ext-json": "*",
"pear/pear-core-minimal": "^1.9",
"pear/pear_exception": "^1.0",
"pear/versioncontrol_git": "^0.5",
"phing/phing": "^2.7",
"php-coveralls/php-coveralls": "^2.0",
"phpunit/phpunit": "^8.5|^9.5",
"symfony/console": "^5.0",
"symfony/filesystem": "^5.0",
"symfony/finder": "^5.0",
"symfony/process": "^5.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Giggsey\\Locale\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Joshua Gigg",
"email": "giggsey@gmail.com",
"homepage": "https://giggsey.com/"
}
],
"description": "Locale functions required by libphonenumber-for-php",
"support": {
"issues": "https://github.com/giggsey/Locale/issues",
"source": "https://github.com/giggsey/Locale/tree/2.2"
},
"time": "2022-04-06T07:33:59+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/46cd95797e9df938fdd2b03693b5fca5e64b01ce",
"reference": "46cd95797e9df938fdd2b03693b5fca5e64b01ce",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"time": "2021-02-19T12:13:01+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.23.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
"reference": "2df51500adbaebdc4c38dea4c89a2e131c45c8a1",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.23-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"time": "2021-05-27T09:27:20+00:00"
},
{
"name": "twig/twig",
"version": "v3.3.2",
"source": {
"type": "git",
"url": "https://github.com/twigphp/Twig.git",
"reference": "21578f00e83d4a82ecfa3d50752b609f13de6790"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/twigphp/Twig/zipball/21578f00e83d4a82ecfa3d50752b609f13de6790",
"reference": "21578f00e83d4a82ecfa3d50752b609f13de6790",
"shasum": ""
},
"require": {
"php": ">=7.2.5",
"symfony/polyfill-ctype": "^1.8",
"symfony/polyfill-mbstring": "^1.3"
},
"require-dev": {
"psr/container": "^1.0",
"symfony/phpunit-bridge": "^4.4.9|^5.0.9"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.3-dev"
}
},
"autoload": {
"psr-4": {
"Twig\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": "http://fabien.potencier.org",
"role": "Lead Developer"
},
{
"name": "Twig Team",
"role": "Contributors"
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"role": "Project Founder"
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "https://twig.symfony.com",
"keywords": [
"templating"
],
"time": "2021-05-16T12:14:13+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"ext-mbstring": "*",
"ext-sockets": "*",
"ext-simplexml": "*",
"ext-curl": "*",
"ext-json": "*",
"ext-gmp": "*",
"ext-sqlite3": "*"
},
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View File

@ -1,95 +0,0 @@
<?php
return [
'group' => 'www-data',
'files_mode' => 0664,
'dirs_mode' => 0775,
'is_dev' => true,
'static_public_path' => '/assets',
'openwrt_ip' => '192.168.1.1',
'inverterd_host' => '192.168.1.2',
'inverterd_port' => 8305,
'pump_host' => '192.168.1.2',
'pump_port' => 8307,
'temphumd_servers' => [
// fill here, example:
'hall' => ['192.168.1.3', 8306, 'Big Hall'/*, optional: config::TEMPHUMD_NO_HUM */],
],
// modem names (array keys) must match ipset names and
// routing table names on the openwrt router
//
// the order of the keys in the array must be the same
// as the order in which fwmark iptables rules are applied
'modems' => [
'modem-example' => [
'ip' => '1.2.3.4',
'label' => 'Modem Name',
'short_label' => 'Mname',
'legacy_token_auth' => false,
],
],
// 'routing_smallhome_ip' => 'fill_me',
// 'routing_default' => 'fill_me',
'debug_backtrace' => true,
'debug_file' => '.debug.log',
'twig_cache' => true,
'templates' => [
'web' => [
'root' => 'templates-web',
'cache' => 'cache/templates-web',
],
],
'static' => [
'app.css' => 12,
'app.js' => 7,
'polyfills.js' => 1,
'modem.js' => 2,
'inverter.js' => 2,
'h265webjs-dist/h265webjs-v20221106.js' => 3,
'h265webjs-dist/h265webjs-v20221106-reminified.js' => 1,
'h265webjs-dist/missile.js' => 1,
],
'cam_hls_access_key' => '',
'cam_hls_proto' => 'http', // bool|callable
'cam_hls_host' => '192.168.1.1', // bool|callable
'cam_list' => [
'low' => [
// fill me with names
],
'high' => [
// fill me with names
],
'labels' => [
// assoc array
],
],
'vk_sms_checker' => [
'telegram_token' => '',
'telegram_chat_id' => '',
'modem_name' => '', // reference to the 'modems' array
],
'database_path' => getenv('HOME').'/.config/homekit.localwebsite.sqlite3',
'auth_cookie_host' => '',
'auth_need' => false, // bool|callable
'auth_pw_salt' => '',
'grafana_sensors_url' => '',
'grafana_inverter_url' => '',
'ipcam_server_api_addr' => '',
'dhcp_hostname_overrides' => [],
];

View File

@ -1,44 +0,0 @@
#!/usr/bin/env php
<?php
// this scripts pulls recent inbox from e3372 modem,
// looks for new messages from vk and re-sends them
// to the telegram group
require_once __DIR__.'/../init.php';
global $config;
$cfg = $config['modems'][$config['vk_sms_checker']['modem_name']];
$e3372 = new E3372($cfg['ip'], $cfg['legacy_token_auth']);
$db = getDB();
$last_processed = $db->querySingle("SELECT last_message_time FROM vk_processed");
$new_last_processed = 0;
$messages = $e3372->getSMSList();
$messages = array_reverse($messages);
$results = [];
if (!empty($messages)) {
foreach ($messages as $m) {
if ($m['timestamp'] <= $last_processed)
continue;
$new_last_processed = $m['timestamp'];
if (preg_match('/^vk/i', $m['phone']) || preg_match('/vk/i', $m['content']))
$results[] = $m;
}
}
if (!empty($results)) {
$t = new TelegramBotClient($config['vk_sms_checker']['telegram_token']);
foreach ($results as $m) {
$text = '<b>'.htmlescape($m['phone']).'</b> ('.$m['date'].')';
$text .= "\n".htmlescape($m['content']);
$t->sendMessage($config['vk_sms_checker']['telegram_chat_id'], $text);
}
}
if ($new_last_processed != 0)
$db->exec("UPDATE vk_processed SET last_message_time=?", $new_last_processed);

View File

@ -1,131 +0,0 @@
<?php
class database {
const SCHEMA_VERSION = 2;
protected SQLite3 $link;
public function __construct(string $db_path) {
$will_create = !file_exists($db_path);
$this->link = new SQLite3($db_path);
if ($will_create)
setperm($db_path);
$this->link->enableExceptions(true);
$this->upgradeSchema();
}
protected function upgradeSchema() {
$cur = $this->getSchemaVersion();
if ($cur == self::SCHEMA_VERSION)
return;
if ($cur < 1) {
$this->link->exec("CREATE TABLE users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT,
password TEXT
)");
}
if ($cur < 2) {
$this->link->exec("CREATE TABLE vk_processed (
last_message_time INTEGER
)");
$this->link->exec("INSERT INTO vk_processed (last_message_time) VALUES (0)");
}
$this->syncSchemaVersion();
}
protected function getSchemaVersion() {
return $this->link->query("PRAGMA user_version")->fetchArray()[0];
}
protected function syncSchemaVersion() {
$this->link->exec("PRAGMA user_version=".self::SCHEMA_VERSION);
}
protected function prepareQuery(string $sql): string {
if (func_num_args() > 1) {
$mark_count = substr_count($sql, '?');
$positions = array();
$last_pos = -1;
for ($i = 0; $i < $mark_count; $i++) {
$last_pos = strpos($sql, '?', $last_pos + 1);
$positions[] = $last_pos;
}
for ($i = $mark_count - 1; $i >= 0; $i--) {
$arg_val = func_get_arg($i + 1);
if (is_null($arg_val)) {
$v = 'NULL';
} else {
$v = '\''.$this->link->escapeString($arg_val) . '\'';
}
$sql = substr_replace($sql, $v, $positions[$i], 1);
}
}
return $sql;
}
public function query(string $sql, ...$params): SQLite3Result {
return $this->link->query($this->prepareQuery($sql, ...$params));
}
public function exec(string $sql, ...$params) {
return $this->link->exec($this->prepareQuery($sql, ...$params));
}
public function querySingle(string $sql, ...$params) {
return $this->link->querySingle($this->prepareQuery($sql, ...$params));
}
public function querySingleRow(string $sql, ...$params) {
return $this->link->querySingle($this->prepareQuery($sql, ...$params), true);
}
protected function performInsert(string $command, string $table, array $fields): SQLite3Result {
$names = [];
$values = [];
$count = 0;
foreach ($fields as $k => $v) {
$names[] = $k;
$values[] = $v;
$count++;
}
$sql = "{$command} INTO `{$table}` (`" . implode('`, `', $names) . "`) VALUES (" . implode(', ', array_fill(0, $count, '?')) . ")";
array_unshift($values, $sql);
return call_user_func_array([$this, 'query'], $values);
}
public function insert(string $table, array $fields): SQLite3Result {
return $this->performInsert('INSERT', $table, $fields);
}
public function replace(string $table, array $fields): SQLite3Result {
return $this->performInsert('REPLACE', $table, $fields);
}
public function insertId(): int {
return $this->link->lastInsertRowID();
}
public function update($table, $rows, ...$cond): SQLite3Result {
$fields = [];
$args = [];
foreach ($rows as $row_name => $row_value) {
$fields[] = "`{$row_name}`=?";
$args[] = $row_value;
}
$sql = "UPDATE `$table` SET " . implode(', ', $fields);
if (!empty($cond)) {
$sql .= " WHERE " . $cond[0];
if (count($cond) > 1)
$args = array_merge($args, array_slice($cond, 1));
}
return $this->query($sql, ...$args);
}
}

View File

@ -1,300 +0,0 @@
<?php
function param($key) {
global $RouterInput;
$val = null;
if (isset($RouterInput[$key])) {
$val = $RouterInput[$key];
} else if (isset($_POST[$key])) {
$val = $_POST[$key];
} else if (isset($_GET[$key])) {
$val = $_GET[$key];
}
if (is_array($val)) {
$val = implode($val);
}
return $val;
}
function str_replace_once(string $needle, string $replace, string $haystack): string {
$pos = strpos($haystack, $needle);
if ($pos !== false) {
$haystack = substr_replace($haystack, $replace, $pos, strlen($needle));
}
return $haystack;
}
function htmlescape($s) {
if (is_array($s)) {
foreach ($s as $k => $v) {
$s[$k] = htmlescape($v);
}
return $s;
}
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
}
function jsonEncode($obj) {
return json_encode($obj, JSON_UNESCAPED_UNICODE);
}
function jsonDecode($json) {
return json_decode($json, true);
}
function startsWith(string $haystack, string $needle): bool {
return $needle === "" || strpos($haystack, $needle) === 0;
}
function endsWith(string $haystack, string $needle): bool {
return $needle === "" || substr($haystack, -strlen($needle)) === $needle;
}
function exectime($format = null) {
$time = round(microtime(true) - START_TIME, 4);
if (!is_null($format)) {
$time = sprintf($format, $time);
}
return $time;
}
function stransi($s) {
static $colors = [
'black' => 0,
'red' => 1,
'green' => 2,
'yellow' => 3,
'blue' => 4,
'magenta' => 5,
'cyan' => 6,
'white' => 7
];
static $valid_styles = ['bold', 'fgbright', 'bgbright'];
$s = preg_replace_callback('/<(?:e ([a-z, =]+)|\/e)>/', function($match) use ($colors, $valid_styles) {
if (empty($match[1])) {
return "\033[0m";
} else {
$codes = [];
$args = preg_split('/ +/', $match[1]);
$fg = null;
$bg = null;
$styles = [];
foreach ($args as $arg) {
list($argname, $argvalue) = explode('=', $arg);
$err = false;
if ($argname == 'fg' || $argname == 'bg') {
if (isset($colors[$argvalue])) {
$$argname = $colors[$argvalue];
} else {
$err = true;
}
} else if ($argname == 'style') {
$argstyles = array_filter(explode(',', $argvalue));
foreach ($argstyles as $style) {
if (!in_array($style, $valid_styles)) {
$err = true;
break;
}
}
if (!$err) {
foreach ($argstyles as $style) {
$styles[$style] = true;
}
}
} else {
$err = true;
}
if ($err) {
trigger_error(__FUNCTION__.": unrecognized argument {$arg}", E_USER_WARNING);
}
}
if (!is_null($fg)) {
$codes[] = $fg + (isset($styles['fgbright']) ? 90 : 30);
}
if (!is_null($bg)) {
$codes[] = $bg + (isset($styles['bgbright']) ? 100 : 40);
}
if (isset($styles['bold'])) {
$codes[] = 1;
}
return !empty($codes) ? "\033[".implode(';', $codes)."m" : '';
}
}, $s);
return $s;
}
function strgen($len = 10): string {
$buf = '';
for ($i = 0; $i < $len; $i++) {
$j = mt_rand(0, 61);
if ($j >= 36) {
$j += 13;
} else if ($j >= 10) {
$j += 7;
}
$buf .= chr(48 + $j);
}
return $buf;
}
function setperm($file, $is_dir = null) {
global $config;
// chgrp
$gid = filegroup($file);
$gname = posix_getgrgid($gid);
if (!is_array($gname)) {
debugError(__FUNCTION__.": posix_getgrgid() failed on $gid", $gname);
} else {
$gname = $gname['name'];
}
if ($gname != $config['group']) {
if (!chgrp($file, $config['group'])) {
debugError(__FUNCTION__.": chgrp() failed on $file");
}
}
// chmod
$perms = fileperms($file);
$need_perms = is_dir($file) ? $config['dirs_mode'] : $config['files_mode'];
if (($perms & $need_perms) !== $need_perms) {
if (!chmod($file, $need_perms)) {
debugError(__FUNCTION__.": chmod() failed on $file");
}
}
}
function redirect($url, $preserve_utm = true, $no_ajax = false) {
if (PHP_SAPI != 'cli' && $_SERVER['REQUEST_METHOD'] == 'GET' && $preserve_utm) {
$proxy_params = ['utm_source', 'utm_medium', 'utm_content', 'utm_campaign'];
$params = [];
foreach ($proxy_params as $p) {
if (!empty($_GET[$p])) {
$params[$p] = (string)$_GET[$p];
}
}
if (!empty($params)) {
if (($anchor_pos = strpos($url, '#')) !== false) {
$anchor = substr($url, $anchor_pos+1);
$url = substr($url, 0, $anchor_pos);
}
$url .= (strpos($url, '?') === false ? '?' : '&').http_build_query($params);
if ($anchor_pos !== false) {
$url .= '#'.$anchor;
}
}
}
header('Location: ' . $url);
exit;
}
function is_xhr_request(): bool {
return isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest';
}
function secondsToTime(int $n): string {
$parts = [];
if ($n >= 86400) {
$days = floor($n / 86400);
$n %= 86400;
$parts[] = "{$days}д";
}
if ($n >= 3600) {
$hours = floor($n / 3600);
$n %= 3600;
$parts[] = "{$hours}ч";
}
if ($n >= 60) {
$minutes = floor($n / 60);
$n %= 60;
$parts[] = "{$minutes}мин";
}
if ($n)
$parts[] = "{$n}сек";
return implode(' ', $parts);
}
function bytesToUnitsLabel(GMP $b): string {
$ks = array('B', 'Kb', 'Mb', 'Gb', 'Tb');
foreach ($ks as $i => $k) {
if (gmp_cmp($b, gmp_pow(1024, $i + 1)) < 0) {
if ($i == 0)
return gmp_strval($b) . ' ' . $k;
$n = gmp_intval(gmp_div_q($b, gmp_pow(1024, $i)));
return round($n, 2).' '.$k;
}
}
return gmp_strval($b);
}
function pwhash(string $s): string {
return hash('sha256', config::get('auth_pw_salt').'|'.$s);
}
$ShutdownFunctions = [];
function append_shutdown_function(callable $f) {
global $ShutdownFunctions;
$ShutdownFunctions[] = $f;
}
function prepend_shutdown_function(callable $f) {
global $ShutdownFunctions;
array_unshift($ShutdownFunctions, $f);
}
function getDB(): database {
static $link = null;
if (is_null($link))
$link = new database(config::get('database_path'));
return $link;
}
function to_camel_case(string $input, string $separator = '_'): string {
return lcfirst(str_replace($separator, '', ucwords($input, $separator)));
}
function from_camel_case(string $s): string {
$buf = '';
$len = strlen($s);
for ($i = 0; $i < $len; $i++) {
if (!ctype_upper($s[$i])) {
$buf .= $s[$i];
} else {
$buf .= '_'.strtolower($s[$i]);
}
}
return $buf;
}
function unsetcookie(string $name) {
global $config;
setcookie($name, null, -1, '/', $config['auth_cookie_host']);
}
function setcookie_safe(...$args) {
global $config;
if (!headers_sent()) {
if (count($args) == 2)
setcookie($args[0], $args[1], time()+86400*365, '/', $config['auth_cookie_host']);
else
setcookie(...$args);
}
}

View File

@ -1,71 +0,0 @@
<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);
mb_internal_encoding('UTF-8');
mb_regex_encoding('UTF-8');
register_shutdown_function(function() {
global $ShutdownFunctions;
if (!empty($ShutdownFunctions)) {
foreach ($ShutdownFunctions as $f)
$f();
}
});
spl_autoload_register(function($class) {
if (endsWith($class, 'Handler'))
$path = ROOT.'/handlers/'.$class.'.php';
// engine classes
else if (in_array($class, ['request_handler', 'router', 'model', 'debug', 'database']))
$path = ROOT.'/engine/'.$class.'.php';
else if ($class == 'Lang')
$path = ROOT.'/engine/lang.php';
else if (endsWith($class, '_tpl'))
$path = ROOT.'/engine/tpl.php';
// other classes
else
$path = ROOT.'/classes/'.$class.'.php';
if (strpos($path, '\\') !== false)
$path = str_replace('\\', '/', $path);
if (is_file($path))
require_once $path;
});
define('ROOT', __DIR__);
define('START_TIME', microtime(true));
set_include_path(get_include_path().PATH_SEPARATOR.ROOT);
require_once ROOT.'/functions.php';
$config = require ROOT.'/config.php';
if (!is_file(ROOT.'/config.local.php'))
die('config.local.php not found');
$config = array_merge($config, require_once ROOT.'/config.local.php');
// it's better to start logging as early as possible
$debug = debug::getInstance(
function($errno, $errfile, $errlne, $errstr) {
// it's not our fault that some vendor package uses something that's deprecated
// so let's not spam our logs
if ($errno == E_USER_DEPRECATED && startsWith($errfile, ROOT.'/vendor/'))
return false;
return true;
}
);
$debug->setMessagesStoreType(debug::STORE_FILE);
$debug->setErrorsStoreType(debug::STORE_FILE);
$debug->enable();
unset($debug);
// composer
require_once ROOT.'/vendor/autoload.php';

View File

@ -1,66 +0,0 @@
#!/usr/bin/env php
<?php
require_once __DIR__.'/init.php';
function read_stdin(?string $prompt = null, bool $multiline = true) {
if (!is_null($prompt))
echo $prompt;
if (!$multiline)
return trim(fgets(STDIN));
$fp = fopen('php://stdin', 'r');
$data = stream_get_contents($fp);
fclose($fp);
return $data;
}
function usage() {
global $argv;
echo <<<EOF
usage: {$argv[0]} COMMAND
Supported commands:
add-user
change-password
EOF;
exit(1);
}
if (empty($argv[1]))
usage();
switch ($argv[1]) {
case 'add-user':
$username = read_stdin('enter username: ', false);
$password = read_stdin('enter password: ', false);
if (users::exists($username)) {
fwrite(STDERR, "user already exists\n");
exit(1);
}
$id = users::add($username, $password);
echo "added user, id = $id\n";
break;
case 'change-password':
$id = (int)read_stdin('enter ID: ', false);
if (!$id)
die("invalid id\n");
$password = read_stdin('enter new password: ', false);
if (!$password)
die("invalid password\n");
users::setPassword($id, $password);
break;
default:
fwrite(STDERR, "invalid command\n");
exit(1);
}