@@ -1,6 +1,6 @@
#!/usr/bin/env python3
"""
G oTelegram v2.5.0 Bot - MTProxy Management for Linux
g oTelegram Pro v2.5.0 Bot - MTProxy Management for Linux
Manages telemt engine via Telegram interface with full CLI feature parity
Uses python-telegram-bot v21+
Supports EN/RU UI with per-user language preferences.
@@ -103,6 +103,7 @@ logger = logging.getLogger(__name__)
GOTELEGRAM_VERSION = " 2.5.0 "
GOTELEGRAM_CONFIG = " /opt/gotelegram/config.json "
DISABLED_USERS_FILE = " /opt/gotelegram/disabled_users.json "
TELEMT_CONFIG = " /etc/telemt/config.toml "
TELEMT_SERVICE = " telemt "
WEBSITE_ROOT = " /var/www/gotelegram-site "
@@ -113,6 +114,7 @@ INSTALL_SH = "/opt/gotelegram/install.sh"
PROMO_LINK_1 = " https://vk.cc/ct29NQ "
PROMO_LINK_2 = " https://vk.cc/cUxAhj "
TIP_LINK = " https://pay.cloudtips.ru/p/7410814f "
YOUTUBE_LINK = os . getenv ( " GOTELEGRAM_YOUTUBE_LINK " , " " ) . strip ( )
PROMO_STAMP_FILE = " /opt/gotelegram/.promo_bot_last_shown "
BOT_TOKEN = os . getenv ( " BOT_TOKEN " )
@@ -1412,6 +1414,48 @@ def load_telemt_users() -> Dict[str, str]:
}
def load_disabled_users ( ) - > Dict [ str , str ] :
raw = load_json ( DISABLED_USERS_FILE ) or { }
if not isinstance ( raw , dict ) :
return { }
users = raw . get ( " users " ) if isinstance ( raw . get ( " users " ) , dict ) else raw
if not isinstance ( users , dict ) :
return { }
clean : Dict [ str , str ] = { }
for name , secret in users . items ( ) :
if name in { " version " , " updated_at " } :
continue
name_s = str ( name ) . strip ( )
secret_s = str ( secret or " " ) . strip ( )
if _USER_NAME_RE . match ( name_s ) and secret_s :
clean [ name_s ] = secret_s
return clean
def save_disabled_users ( users : Dict [ str , str ] ) - > bool :
payload = {
" version " : 1 ,
" updated_at " : datetime . utcnow ( ) . isoformat ( ) + " Z " ,
" users " : { name : users [ name ] for name in sorted ( users ) } ,
}
ok = save_json ( DISABLED_USERS_FILE , payload )
if ok :
try :
os . chmod ( DISABLED_USERS_FILE , 0o600 )
except OSError :
pass
return ok
def load_user_records ( ) - > Dict [ str , Dict [ str , Any ] ] :
records : Dict [ str , Dict [ str , Any ] ] = { }
for name , secret in load_disabled_users ( ) . items ( ) :
records [ name ] = { " secret " : secret , " enabled " : False }
for name , secret in load_telemt_users ( ) . items ( ) :
records [ name ] = { " secret " : secret , " enabled " : True }
return records
def save_telemt_users ( users : Dict [ str , str ] ) - > bool :
""" Persist [access.users] while keeping the rest of the TOML structure. """
telemt_cfg = load_toml ( TELEMT_CONFIG ) or { }
@@ -1589,10 +1633,12 @@ async def cb_menu_share(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
# ============================================================================
def _users_keyboard ( users : Dict [ str , str ] , user_id : Optional [ int ] ) - > InlineKeyboardMarkup :
def _users_keyboard ( users : Dict [ str , Dict [ str , Any ] ] , user_id : Optional [ int ] ) - > InlineKeyboardMarkup :
rows = [ ]
for name in sorted ( users ) :
rows . append ( [ InlineKeyboardButton ( f " 👤 { name } " , callback_data = f " user_view_ { name } " ) ] )
for name in sorted ( users , key = lambda item : ( item != " main " , item ) ):
enabled = bool ( users [ name ] . get ( " enabled " ) )
icon = " 🟢 " if enabled else " ⏸ "
rows . append ( [ InlineKeyboardButton ( f " { icon } { name } " , callback_data = f " user_view_ { name } " ) ] )
rows . append ( [ InlineKeyboardButton ( " ➕ Добавить ключ" , callback_data = " user_add " ) ] )
rows . append ( [
InlineKeyboardButton ( _t ( user_id , " btn_refresh " ) , callback_data = " menu_users " ) ,
@@ -1605,10 +1651,13 @@ async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
query = update . callback_query
await query . answer ( )
user_id = _uid ( update )
users = load_telemt_user s ( )
users = load_user_record s ( )
if users :
user_lines = " \n " . join ( f " • <code> { html . escape ( name ) } </code> " for name in sorted ( users ) )
user_lines = " \n " . join (
f " { ' 🟢 ' if users [ name ] . get ( ' enabled ' ) else ' ⏸ ' } <code> { html . escape ( name ) } </code> "
for name in sorted ( users , key = lambda item : ( item != " main " , item ) )
)
else :
user_lines = " <i>Ключей пока нет</i> "
@@ -1635,9 +1684,9 @@ async def cb_menu_users(update: Update, context: ContextTypes.DEFAULT_TYPE) -> N
await safe_edit_message ( query , text , reply_markup = _users_keyboard ( users , user_id ) , parse_mode = " HTML " )
async def _user_detail_text ( name : str , secret : str ) - > str :
async def _user_detail_text ( name : str , secret : str , enabled : bool = True ) - > str :
link = await get_proxy_link_for_secret ( secret )
api = await telemt_api_get ( f " /v1/users/ { quote ( name , safe = ' ' ) } " )
api = await telemt_api_get ( f " /v1/users/ { quote ( name , safe = ' ' ) } " ) if enabled else None
details = " "
if api :
data = api . get ( " data " , api )
@@ -1656,12 +1705,16 @@ async def _user_detail_text(name: str, secret: str) -> str:
else :
compact = json . dumps ( data , ensure_ascii = False ) [ : 600 ]
details = f " \n <pre> { html . escape ( compact ) } </pre> "
elif enabled :
details = " \n <i>Runtime API недоступен. Новые установки goTelegram Pro включают е г о автоматически.</i> "
else :
details = " \n <i>Runtime API недоступен. Новые установки GoTelegram включают е г о автоматически .</i> "
details = " \n <i>Ключ отключён и сейчас не принимается telemt .</i> "
link_line = html . escape ( link ) if link else " link unavailable "
status_line = " 🟢 enabled " if enabled else " ⏸ disabled "
return (
f " <b>👤 { html . escape ( name ) } </b> \n \n "
f " Status: <b> { status_line } </b> \n "
f " Secret: <code> { html . escape ( secret ) } </code> \n \n "
f " <b>Ссылка:</b> \n <code> { link_line } </code> \n "
f " { details } "
@@ -1673,23 +1726,31 @@ async def cb_user_view(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
await query . answer ( )
user_id = _uid ( update )
name = query . data . removeprefix ( " user_view_ " )
users = load_telemt_user s ( )
s ecret = users . get ( name )
if not s ecret :
users = load_user_record s ( )
r ecord = users . get ( name )
if not r ecord :
await safe_edit_message (
query ,
" ❌ Пользователь не найден. " ,
reply_markup = InlineKeyboardMarkup ( [ [ InlineKeyboardButton ( _t ( user_id , " btn_back " ) , callback_data = " menu_users " ) ] ] ) ,
)
return
enabled = bool ( record . get ( " enabled " ) )
secret = str ( record . get ( " secret " , " " ) )
buttons = [
[ InlineKeyboardButton ( " ⏸ Отключить " if enabled else " ▶️ Включить " , callback_data = f " user_toggle_ { name } " ) ] ,
[ InlineKeyboardButton ( " 🗑 Удалить " , callback_data = f " user_del_ { name } " ) ] ,
[ InlineKeyboardButton ( _t ( user_id , " btn_back " ) , callback_data = " menu_users " ) ] ,
]
if name == " main " :
buttons = [
[ InlineKeyboardButton ( " 🔒 Main key " , callback_data = f " user_view_ { name } " ) ] ,
[ InlineKeyboardButton ( _t ( user_id , " btn_back " ) , callback_data = " menu_users " ) ] ,
]
await safe_edit_message (
query ,
await _user_detail_text ( name , secret ) ,
await _user_detail_text ( name , secret , enabled ),
reply_markup = InlineKeyboardMarkup ( buttons ) ,
parse_mode = " HTML " ,
disable_web_page_preview = True ,
@@ -1714,6 +1775,45 @@ async def cb_user_add(update: Update, context: ContextTypes.DEFAULT_TYPE) -> Non
)
async def cb_user_toggle ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
query = update . callback_query
await query . answer ( )
user_id = _uid ( update )
name = query . data . removeprefix ( " user_toggle_ " )
if name == " main " :
await query . answer ( " main нельзя отключить " , show_alert = True )
return
active = load_telemt_users ( )
disabled = load_disabled_users ( )
records = load_user_records ( )
record = records . get ( name )
if not record :
await query . answer ( " Ключ не найден " , show_alert = True )
return
enabled = not bool ( record . get ( " enabled " ) )
secret = str ( record . get ( " secret " , " " ) )
if enabled :
disabled . pop ( name , None )
active [ name ] = secret
else :
active . pop ( name , None )
disabled [ name ] = secret
if enabled :
saved = save_telemt_users ( active ) and save_disabled_users ( disabled )
else :
saved = save_disabled_users ( disabled ) and save_telemt_users ( active )
if not saved :
await safe_edit_message ( query , " ❌ Н е удалось сохранить состояние ключа " )
return
await refresh_telemt_after_user_change ( )
await safe_edit_message (
query ,
f " { ' ✅ Ключ включён ' if enabled else ' ⏸ Ключ отключён ' } : <code> { html . escape ( name ) } </code> " ,
reply_markup = InlineKeyboardMarkup ( [ [ InlineKeyboardButton ( _t ( user_id , " btn_back " ) , callback_data = f " user_view_ { name } " ) ] ] ) ,
parse_mode = " HTML " ,
)
async def cb_user_delete ( update : Update , context : ContextTypes . DEFAULT_TYPE ) - > None :
query = update . callback_query
await query . answer ( )
@@ -1735,12 +1835,15 @@ async def cb_user_delete_confirm(update: Update, context: ContextTypes.DEFAULT_T
await query . answer ( )
user_id = _uid ( update )
name = query . data . removeprefix ( " user_del_yes_ " )
users = load_telemt_users ( )
if name == " main " or name not in users:
active = load_telemt_users ( )
disabled = load_disabled_ users( )
records = load_user_records ( )
if name == " main " or name not in records :
await query . answer ( " Нельзя удалить этот ключ " , show_alert = True )
return
users . pop ( name , None )
if not save_telemt_users ( users ) :
active . pop ( name , None )
disabled . pop ( name , None )
if not save_telemt_users ( active ) or not save_disabled_users ( disabled ) :
await safe_edit_message ( query , " ❌ Н е удалось сохранить config.toml " )
return
await refresh_telemt_after_user_change ( )
@@ -1757,10 +1860,11 @@ async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TY
if not _USER_NAME_RE . match ( name ) :
await update . message . reply_text ( " ❌ Некорректное имя. Используйте латиницу, цифры, _ . - и до 48 символов. " )
return
user s = load_telemt_user s ( )
if name in user s:
record s = load_user_record s ( )
if name in record s:
await update . message . reply_text ( " ❌ Такой пользователь уже есть. " )
return
users = load_telemt_users ( )
secret = hashlib . sha256 ( f " { name } : { time . time ( ) } : { os . urandom ( 16 ) . hex ( ) } " . encode ( ) ) . hexdigest ( ) [ : 32 ]
users [ name ] = secret
if not save_telemt_users ( users ) :
@@ -1773,7 +1877,7 @@ async def create_user_from_text(update: Update, context: ContextTypes.DEFAULT_TY
f " Пользователь: <code> { html . escape ( name ) } </code> \n "
f " Secret: <code> { secret } </code> \n \n "
f " <code> { html . escape ( link or ' ' ) } </code> " ,
reply_markup = _users_keyboard ( load_telemt_user s ( ) , user_id ) ,
reply_markup = _users_keyboard ( load_user_record s ( ) , user_id ) ,
parse_mode = " HTML " ,
disable_web_page_preview = True ,
)
@@ -2332,8 +2436,8 @@ async def cmd_deladmin(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
def get_promo_text ( ) - > str :
""" Return promo text with 2 hosters + donate. """
return (
""" Return promo text with 2 hosters, optional YouTube link and donate. """
text = (
" <b>💰 Хостинг #1 — скидка до 60 % </b> \n "
f " <a href= ' { PROMO_LINK_1 } ' > { PROMO_LINK_1 } </a> \n \n "
" <b>Промокоды:</b> \n "
@@ -2349,6 +2453,13 @@ def get_promo_text() -> str:
" <b>☕ Донат / Чаевые</b> \n "
f " <a href= ' { TIP_LINK } ' > { TIP_LINK } </a> "
)
if YOUTUBE_LINK :
text + = (
" \n \n ━━━━━━━━━━━━━━━━━━━━━━━━━ \n \n "
" <b>▶ YouTube-канал</b> \n "
f " <a href= ' { html . escape ( YOUTUBE_LINK ) } ' > { html . escape ( YOUTUBE_LINK ) } </a> "
)
return text
def should_show_promo_bot ( ) - > bool :
@@ -2393,7 +2504,7 @@ async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
text = (
f " <b>ℹ ️ Credits & Acknowledgements</b> \n \n "
f " <b>G oTelegram v { GOTELEGRAM_VERSION } </b> \n \n "
f " <b>g oTelegram Pro v { GOTELEGRAM_VERSION } </b> \n \n "
f " Built with love for the Telegram community \n \n "
f " <b>Special thanks to:</b> \n \n "
f " 🙏 <b>telemt</b> - MTProxy engine \n "
@@ -2405,7 +2516,7 @@ async def cb_menu_credits(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
f " 🚀 <b>Start Bootstrap</b> - Bootstrap templates \n "
f " Professional design framework \n \n "
f " 💬 <b>Community</b> - Your feedback & support \n \n "
f " <i>G oTelegram is open-source and community-driven</i> "
f " <i>g oTelegram Pro is open-source and community-driven</i> "
)
keyboard = InlineKeyboardMarkup (
@@ -2425,7 +2536,7 @@ async def cb_menu_remove(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await query . answer ( )
text = (
" <b>⚠️ Remove G oTelegram</b> \n \n "
" <b>⚠️ Remove g oTelegram Pro </b> \n \n "
" This will completely remove the installation. \n "
" Are you sure? "
)
@@ -2443,7 +2554,7 @@ async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
query = update . callback_query
await query . answer ( )
await safe_edit_message ( query , " ⏳ Removing G oTelegram... " )
await safe_edit_message ( query , " ⏳ Removing g oTelegram Pro ... " )
# Stop service
await sh ( " systemctl " , " stop " , TELEMT_SERVICE )
@@ -2452,7 +2563,7 @@ async def cb_remove_confirm(update: Update, context: ContextTypes.DEFAULT_TYPE)
for path in [ " /opt/gotelegram " , WEBSITE_ROOT ] :
await sh ( " rm " , " -rf " , path )
text = " ✅ G oTelegram removed successfully "
text = " ✅ g oTelegram Pro removed successfully "
keyboard = InlineKeyboardMarkup (
[ [ InlineKeyboardButton ( _t ( _uid ( update ) , " btn_back " ) , callback_data = " menu_main " ) ] ]
)
@@ -2617,6 +2728,8 @@ async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE) ->
await cb_user_add ( update , context )
elif data . startswith ( " user_view_ " ) :
await cb_user_view ( update , context )
elif data . startswith ( " user_toggle_ " ) :
await cb_user_toggle ( update , context )
elif data . startswith ( " user_del_yes_ " ) :
await cb_user_delete_confirm ( update , context )
elif data . startswith ( " user_del_ " ) :
@@ -2663,7 +2776,7 @@ async def handle_text_message(update: Update, context: ContextTypes.DEFAULT_TYPE
if not ok :
await update . message . reply_text ( _t ( user_id , info ) , parse_mode = " HTML " )
return
# Success — record in G oTelegram config. Use "template_id" (canonical
# Success — record in g oTelegram Pro config. Use "template_id" (canonical
# field name written by install.sh/save_gotelegram_config).
config = load_json ( GOTELEGRAM_CONFIG ) or { }
config [ " template_id " ] = tpl_id
@@ -2734,7 +2847,7 @@ def main() -> None:
application . add_error_handler ( error_handler )
# Run the bot
logger . info ( f " G oTelegram v{ GOTELEGRAM_VERSION } bot starting... " )
logger . info ( f " g oTelegram Pro v{ GOTELEGRAM_VERSION } bot starting... " )
application . run_polling ( allowed_updates = Update . ALL_TYPES )