11 Commits
2.2.0 ... 2.2.4

20 changed files with 354 additions and 147 deletions

8
.gitignore vendored
View File

@ -8,3 +8,11 @@ db.sqlite
# Docs
docs/
# AppImage
AppDir/.DirIcon
AppDir/usr/
dist/python*
dist/packages/
dist/appimagetool*.AppImage
dist/ClanStat-*.AppImage

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "3rdparty/colors.sh"]
path = 3rdparty/colors.sh
url = https://git.a2s.su/iiiypuk/colors.sh.git

1
3rdparty/colors.sh vendored Submodule

Submodule 3rdparty/colors.sh added at b4c1b5e3ee

5
AppDir/AppRun Executable file
View File

@ -0,0 +1,5 @@
#!/bin/sh
export PYTHONPATH="${PYTHONPATH}:${APPDIR}/usr/lib/python3.13/site-packages"
${APPDIR}/usr/bin/python3.13 -m ClanStat $1

6
AppDir/app.desktop Normal file
View File

@ -0,0 +1,6 @@
[Desktop Entry]
Type=Application
Name=ClanStat
Icon=shield
Terminal=false
Categories=Utility;

BIN
AppDir/shield.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,6 +1,6 @@
# Информация о программе
__author__ = 'Alexander Popov'
__version__ = (2, 2, 0)
__version__ = (2, 2, 4)
# Импорт модулей стандартной библиотеки
from os import getenv

View File

@ -1,13 +1,17 @@
# Импорт модулей стандартной библиотеки
import time
# Импорт сторонних модулей
from pyrogram.errors import exceptions
from rich import print as rich_print
# Импорт модулей приложения
from . import client, db, logger, DELAY_TIME
from . import __version__, client, db, logger, DELAY_TIME
from .actions import get_top_wins, get_top_donates
async def main():
"""Запускак клиента Telegram"""
"""Запуск клиента Telegram"""
await client.start()
logger.info('Клиент Telegram запущен')
@ -33,6 +37,8 @@ async def main():
if __name__ == '__main__':
logger.warning('🎴 ClanStat 🆔 {}', '.'.join(map(str, __version__)))
logger.warning('🌐 URL: {}', 'https://git.a2s.su/AniCard/ClanStat/')
logger.info('Выполняется запуск приложения')
logger.info('Выполняется подключение к базе данных')
@ -40,6 +46,12 @@ if __name__ == '__main__':
try:
client.run(main())
except exceptions.unauthorized_401.AuthKeyUnregistered:
rich_print('Ошибка Telegram: [bold red][401 AUTH_KEY_UNREGISTERED][/bold red]')
rich_print(
'Удалите файлы авторизации [bold green]Pyrogram[/bold green]:'
' [italic yellow]*.session, *.session-journal[/italic yellow]'
)
except KeyboardInterrupt:
pass

View File

@ -1,3 +1,7 @@
"""
🩻 ...
"""
# Импорт модулей стандартной библиотеки
import time
@ -7,8 +11,8 @@ from .collect import parse_wins_top, parse_donates_top
async def get_top_wins() -> None:
"""Получает топ клана по победам"""
logger.info('Выполняется получения списка побед клана')
"""⚔️ Получает топ клана по победам"""
logger.info('⚔️ Выполняется получения списка побед клана')
await client.send_message(BOT_NAME, '🛡 Мой клан')
time.sleep(1)
@ -22,7 +26,7 @@ async def get_top_wins() -> None:
message_id=message_id,
callback_data='my_clan:tops:91:0:0:0:0:1',
)
time.sleep(1)
time.sleep(2)
async for message in client.get_chat_history(BOT_NAME, limit=1):
message_id = message.id
@ -32,22 +36,22 @@ async def get_top_wins() -> None:
message_id=message_id,
callback_data='my_clan:top-wins:91:0:0:0:0:1',
)
time.sleep(1)
time.sleep(2)
async for message in client.get_chat_history(BOT_NAME, limit=1):
await parse_wins_top(message)
await client.read_chat_history(BOT_NAME)
await client.read_chat_history(BOT_NAME) # Помечает сообщения в чате прочитанными
logger.info('Получение списка побед клана завершено')
async def get_top_donates() -> None:
"""Получает топ клана по пожертвованиям"""
logger.info('Выполняется получения списка пожертвований клана')
"""💠 Получает топ клана по пожертвованиям"""
logger.info('💠 Выполняется получения списка пожертвований клана')
await client.send_message(BOT_NAME, '🛡 Мой клан')
time.sleep(1)
time.sleep(2)
message_id = 0
async for message in client.get_chat_history(BOT_NAME, limit=1):
@ -58,7 +62,7 @@ async def get_top_donates() -> None:
message_id=message_id,
callback_data='my_clan:tops:91:0:0:0:0:1',
)
time.sleep(1)
time.sleep(2)
async for message in client.get_chat_history(BOT_NAME, limit=1):
message_id = message.id
@ -73,6 +77,6 @@ async def get_top_donates() -> None:
async for message in client.get_chat_history(BOT_NAME, limit=1):
await parse_donates_top(message)
await client.read_chat_history(BOT_NAME)
await client.read_chat_history(BOT_NAME) # Помечает сообщения в чате прочитанными
logger.info('Получение списка пожертвований клана завершено')

93
ClanStat/collect.py Normal file
View File

@ -0,0 +1,93 @@
"""
🩻 ...
"""
# Импорт модулей стандартной библиотеки
import re
import io
# Импорт сторонних модулей
from rich import print as rich_print
from rich.table import Table
from pyrogram.types import Message
from pyrogram.enums import MessageEntityType
# Импорт модулей приложения
from . import db
from .utils import get_telegram_id, get_telegram_data, clean_icons
async def parse_wins_top(message: Message) -> None:
"""⚔️ Выполняет парсинг данных топа побед членов клана"""
if not message.text.startswith('🏆 Топ по победам'):
return
WINS = await parse_tops(message, '')
# 📟 Отрисовывает таблицу побед
table = Table(show_header=True)
table.add_column('ID', style='dim', justify='right', width=12)
table.add_column('Username', style='magenta')
table.add_column('Count', style='green', width=7)
table.add_column('URL', style='yellow')
for i in WINS:
table.add_row(str(i['telegram_id']), i['username'], str(i['count']), i['url'])
rich_print(table)
db.add_data(WINS, True) # Добавляет данные в базу данных
async def parse_donates_top(message: Message) -> None:
"""💠 Выполняет парсинг данных топа пожертвований членов клана"""
if not message.text.startswith('🏆 Топ по пожертвованиям'):
return
DONATES = await parse_tops(message, '💠')
# 📟 Отрисовывает таблицу побед
table = Table(show_header=True)
table.add_column('ID', style='dim', justify='right', width=12)
table.add_column('Username', style='magenta')
table.add_column('Count', style='green', width=7)
table.add_column('URL', style='yellow')
for i in DONATES:
table.add_row(str(i['telegram_id']), i['username'], str(i['count']), i['url'])
rich_print(table)
db.add_data(DONATES, False) # Добавляет данные в базу данных
async def parse_tops(message: Message, emoji_replace: str) -> list:
"""..."""
top_data = list()
msg_buffer = io.StringIO(message.text)
msg_buffer.readline()
msg_buffer.readline()
for e in message.entities:
player_line = msg_buffer.readline()
player_name, battle_count = player_line.split(' - ')
player = clean_icons(message.text[e.offset : e.offset + e.length])
player_name = clean_icons(re.sub(r'^\d+. ', '', player_name))
battle_count = battle_count.replace(f' {emoji_replace}', '') # Удаляет эмодзи
battle_count = int(re.sub(r'[^\x00-\x7F]', '', battle_count))
if player not in player_name:
top_data.append({'telegram_id': None, 'username': player_name, 'count': battle_count, 'url': None})
msg_buffer.readline()
continue
if e.type is MessageEntityType.TEXT_LINK:
player_url = e.url
player_telegram_id = await get_telegram_id(player_url.replace('http://t.me/', ''))
elif e.type is MessageEntityType.TEXT_MENTION:
player_url = e.url
player_telegram_id = e.user.id
top_data.append({'telegram_id': player_telegram_id, 'username': player, 'count': battle_count, 'url': player_url})
return top_data

View File

@ -1,3 +1,7 @@
"""
🗃 Работа с базой данных
"""
# Импорт модулей стандартной библиотеки
import json
@ -11,25 +15,40 @@ class DataBase(object):
def __init__(self, host: str, port: int, name: str, user: str, password: str):
super(DataBase, self).__init__()
self.host = host
"""Хост PostgreSQL базы данных"""
"""🖥️ Хост PostgreSQL базы данных"""
self.port = port
"""Порт сервера базы данных"""
"""🔟 Порт сервера базы данных"""
self.name = name
"""Имя базы данных"""
"""🏷️ Имя базы данных"""
self.user = user
"""Имя пользователя"""
"""🥸 Имя пользователя"""
self.password = password
"""Пароль пользователя"""
"""🔐 Пароль пользователя"""
def connect(self) -> bool:
"""Выполняет подключение к базе данных"""
self.conn = psycopg2.connect(
dbname=self.name,
host=self.host,
user=self.user,
password=self.password,
port=self.port,
)
connection_kwargs = {
"host": self.host,
"port": self.port,
"user": self.user,
"password": self.password,
"database": self.name,
}
"""Настройки соединения"""
keepalive_kwargs = {
"keepalives": 1,
"keepalives_idle": 5,
"keepalives_interval": 5,
"keepalives_count": 2,
"tcp_user_timeout": 1000,
}
"""Настройки TCP"""
self.conn = psycopg2.connect(**connection_kwargs, **keepalive_kwargs) # Выполняет подключение к базе данных
self.conn.autocommit = True # Активирует автоматический коммит транзакций
return True
def close(self) -> bool:
@ -37,12 +56,23 @@ class DataBase(object):
self.conn.close()
return True
def check_connect(self) -> None:
"""Проверяет статус подключения"""
try:
cur = self.conn.cursor()
cur.execute('SELECT 1')
cur.close()
return True
except Exception as e:
return False
def commit(self) -> None:
"""Выполняет коммит"""
self.conn.commit()
return True
def add_data(self, data, wins: bool) -> bool: # wins изменит на Enum
def add_data(self, data, wins: bool) -> bool: # wins изменить на Enum
"""Добавляет данные в базу данных"""
if wins:
table = 'wins'
@ -59,7 +89,6 @@ class DataBase(object):
),
)
)
self.commit()
cur.close()
return True

49
ClanStat/utils.py Normal file
View File

@ -0,0 +1,49 @@
"""
🧰 Утилиты
"""
# Импорт сторонних модулей
from pyrogram.types import Message
from pyrogram.enums import MessageEntityType
from . import client
async def get_telegram_id(username: str) -> int:
"""🆔 Возвращает Telegram ID"""
try:
telegram_user = await client.get_users(username)
return telegram_user.id
except Exception as e:
return None
async def get_telegram_data(message: Message, idx: int) -> tuple():
"""..."""
if message.entities[idx].type == MessageEntityType.TEXT_LINK:
player_url = message.entities[idx].url
player_telegram_id = await get_telegram_id(player_url.replace('http://t.me/', ''))
elif message.entities[idx].type == MessageEntityType.TEXT_MENTION:
player_url = None
player_telegram_id = message.entities[idx].user.id
else:
player_url = None
player_telegram_id = None
return (player_url, player_telegram_id)
def clean_icons(text: str) -> str:
"""💎 Удаляет иконки подписок из ников"""
# оставляет ники игроков, Python не может в эмодзи в RegExp, по этому пришлось делать через str.replace()
# player = re.sub(r'\s[⚡⚜]$', '', player)
output = text
for icon in ['', '', '💎']:
output = output.replace(f' {icon}', '')
return output

View File

@ -11,3 +11,10 @@ include_toc: true
* 🟢 Добавлено
* 🔴 Удалёно
* ♻️ Исправлено
## 2.2.4
* 🟢 Добавлено сообщение об ошибке **401** `AUTH_KEY_UNREGISTERED`
* 🟢 Добавлены emoji в приветственное сообщение
* 🟢 Результаты в терминале отображаются в таблице
* 🟢 Изменён алгоритм получения значений побед и пожертвований

View File

@ -15,12 +15,24 @@ include_toc: true
## 💽 Установка и использование
### ⬇️ Загрузка проекта
Первым делом необходимо скачать приложение.
```sh
# Скачиваем приложение и переходим в скачанную директорию
git clone https://git.a2s.su/AniCard/ClanStat.git
cd ClanStat
```
### 📄 Подготовка файла конфигурации
Настройки приложения хранится в файле `.env`.
Пример файла с настройками называется `env.example`.
```sh
# Копируем файл с примером конфигурации
cp env.example .env
```

View File

@ -1,91 +0,0 @@
# Импорт модулей стандартной библиотеки
import re
# Импорт сторонних модулей
from pyrogram.types import Message
from pyrogram.enums import MessageEntityType
# Импорт модулей приложения
from . import db
from .utils import get_telegram_id, get_player_data
async def parse_wins_top(message: Message) -> None:
"""Выполняет парсинг данных топа побед членов клана"""
if message.text.startswith('🏆 Топ по победам'):
players = message.text.split('\n')
players.pop(0)
players.pop(0)
WINS = list()
for idx, player in enumerate(players):
player = re.sub(r'^\d+. ', '', player) # удаляет нумерацию
player, battle_count = player.split(' - ') # разделяет ник и количество побед в клановых сражениях
# оставляет ники игроков, Python не может в эмодзи в RegExp, по этому пришлось делать через str.replace()
# player = re.sub(r'\s[⚡⚜]$', '', player)
player = player.replace('', '')
player = player.replace('', '').replace('\ufe0f', '')
player = player.replace(' 💎', '')
battle_count = battle_count.replace('', '') # удаляем эмодзи мечей
battle_count = int(re.sub(r'[^\x00-\x7F]', '', battle_count)) # преобразовывает строку в число
player_url, player_telegram_id = await get_player_data(message, idx)
WINS.append(
{
'telegram_id': player_telegram_id,
'username': player,
'count': battle_count,
'url': player_url,
}
)
print(WINS)
db.add_data(WINS, True)
else:
pass
async def parse_donates_top(message: Message) -> None:
"""Выполняет парсинг данных топа пожертвований членов клана"""
if message.text.startswith('🏆 Топ по пожертвованиям'):
players = message.text.split('\n')
# Удаляет из массива строку с заголовком и последующую пустую строку
players.pop(0)
players.pop(0)
DONATES = list()
for idx, player in enumerate(players):
player = re.sub(r'^\d+. ', '', player) # удаляет нумерацию
player, donates_count = player.split(' - ') # разделяет ник и количество пожертвований в клановую сокровищницу
# Оставляет только ники игроков.
# Python не может парсить эмодзи в RegExp, по этому пришлось делать через str.replace()
# player = re.sub(r'\s[⚡⚜]$', '', player)
player = player.replace('', '')
player = player.replace('', '').replace('\ufe0f', '')
player = player.replace(' 💎', '')
donates_count = donates_count.replace(' 💠', '') # удаляем эмодзи пожертвований
donates_count = int(re.sub(r'[^\x00-\x7F]', '', donates_count)) # преобразовывает строку в число
player_url, player_telegram_id = await get_player_data(message, idx)
DONATES.append(
{
'telegram_id': player_telegram_id,
'username': player,
'count': donates_count,
'url': player_url,
}
)
print(DONATES)
db.add_data(DONATES, False)
else:
pass

View File

@ -1,27 +0,0 @@
# Импорт сторонних модулей
from pyrogram.types import Message
from pyrogram.enums import MessageEntityType
from . import client
async def get_telegram_id(username: str) -> int:
"""Возвращает Telegram ID"""
telegram_user = await client.get_users(username)
return telegram_user.id
async def get_player_data(message: Message, idx: int) -> tuple():
"""..."""
if message.entities[idx].type == MessageEntityType.TEXT_LINK:
player_url = message.entities[idx].url
player_telegram_id = await get_telegram_id(player_url.replace('http://t.me/', ''))
elif message.entities[idx].type == MessageEntityType.TEXT_MENTION:
player_url = None
player_telegram_id = message.entities[idx].user.id
else:
player_url = None
player_telegram_id = None
return (player_url, player_telegram_id)

0
dist/.gitkeep vendored Normal file
View File

View File

@ -1,5 +1,7 @@
loguru==0.7.2
pdoc==15.0.1
psycopg2==2.9.10
Pyrogram==2.0.106
python-dotenv==1.0.1
rich==14.0.0
TgCrypto==1.2.5
zstandard==0.23.0

82
scripts/make-appimage.sh Executable file
View File

@ -0,0 +1,82 @@
#!/bin/sh
# author: Alexander Popov
# mail: iiiypuk {at} fastmail.fm
# license: Public Domain
# desc: Собирает AppImage
# Импортирует скрипт с определениями цветов
source ./3rdparty/colors.sh/colors.sh
APPDIR="../AppDir"
GIT_HASH=$(git rev-parse --short HEAD)
# Скачивает x86_64 AppImageTool
download_appimage_tool() {
echo -e "${CYAN}Загрузка AppImageTool...${NC}"
wget -q --show-progress https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
}
# Скачивает March Any Linux Python
download_python() {
wget -q --show-progress https://github.com/niess/python-appimage/releases/download/python3.13/python3.13.3-cp313-cp313-manylinux2014_x86_64.AppImage
chmod +x python3.13.3-cp313-cp313-manylinux2014_x86_64.AppImage
mv python3.13.3-cp313-cp313-manylinux2014_x86_64.AppImage python3.13
mkdir -p ${APPDIR}/usr/bin/
cp ./python3.13 ${APPDIR}/usr/bin/python3.13
}
# Загружает Python зависимости проекта
get_requirements() {
./python3.13 -m pip install --target=$(pwd)/packages -r ../requirements.txt > /dev/null 2>&1
}
# Копирует записимости в AppDir
copy_requirements() {
mkdir -p ${APPDIR}/usr/lib/python3.13/site-packages/
cp -r packages/* ${APPDIR}/usr/lib/python3.13/site-packages/
}
# Копирует приложение в AppDir
copy_app_files() {
echo -e "${GREEN}Копирование файлов ...${NC}"
rm -rf ${APPDIR}/usr/lib/python3.13/site-packages/ClanStat
cp -r ../ClanStat ${APPDIR}/usr/lib/python3.13/site-packages/ClanStat
rm -rf ${APPDIR}/usr/lib/python3.13/site-packages/ClanStat/__pycache__
}
# Выполянет сборку AppImage
make_appimage() {
echo -e "${GREEN}Сборка AppImage...${NC}"
./appimagetool-x86_64.AppImage ${APPDIR} > /dev/null 2>&1
mv ./ClanStat-x86_64.AppImage ./ClanStat-${GIT_HASH}-x86_64.AppImage
echo -e "${RED}Сборка AppImage заверщена${NC}"
}
# Основная функция
main() {
clear
echo -e "${YELLOW}Подготовка к сборке AppImage...${NC}"
if [ ! -f "appimagetool-x86_64.AppImage" ]; then
download_appimage_tool
fi
if [ ! -f "python3.13" ]; then
download_python
get_requirements
copy_requirements
fi
copy_app_files
make_appimage
}
# Запускает основную функцию
main

View File

@ -1,3 +1,15 @@
#!/bin/sh
pdoc -o docs/ app
# author: Alexander Popov
# mail: iiiypuk {at} fastmail.fm
# license: Public Domain
# desc: Генерирует документацию Python кода
# Импортирует скрипт с определениями цветов
source ./3rdparty/colors.sh/colors.sh
echo -e "${CYAN}Генерация документации...${NC}"
pdoc -o docs/ ClanStat
echo -e "${GREEN}Генерация завершена. Смотрите директорию 'docs'.${NC}"