Compare commits

..

12 Commits
v1 ... master

25 changed files with 306 additions and 104 deletions

8
.gitignore vendored

@ -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

@ -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

@ -0,0 +1 @@
Subproject commit b4c1b5e3ee7372b93da0f82123df7363f1af1118

5
AppDir/AppRun Executable 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

@ -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

(image error) Size: 22 KiB

@ -1,4 +1,12 @@
### 1.1.0
## 2.1.0
- В БД сохраняется UTF-8 строки, вместо `\u`
## 2.0.0
- переход с SQLite3 на PostgreSQL
## 1.1.0
- Добавлен выхлом ошибок `try...catch` в логи
- У игроков с премиумом теперь отрезается символ 💎 из имени

@ -1,6 +1,6 @@
# Информация о программе
__author__ = 'Alexander Popov'
__version__ = (1, 1, 0)
__version__ = (2, 2, 1)
# Импорт модулей стандартной библиотеки
from os import getenv
@ -16,10 +16,16 @@ from .db import DataBase
load_dotenv() # Выполняет чтение .env
client = Client(getenv('ACCOUNT'), api_id=getenv('APP_ID'), api_hash=getenv('APP_HASH'))
"""Клиент Telegram"""
"""🤖 Клиент Telegram"""
db = DataBase(getenv('DB_PATH'))
"""База данных"""
db = DataBase(
getenv('DB_HOST'),
int(getenv('DB_PORT')),
getenv('DB_NAME'),
getenv('DB_USER'),
getenv('DB_PASSWORD'),
)
"""🗃️ База данных"""
logger.add(getenv('LOG_PATH'), compression='zip')
"""Логгер"""
@ -28,4 +34,4 @@ DELAY_TIME = 12 * 60 * 60 # 12 часов
"""Время приостановки выполнения цикла сбора статистики"""
BOT_NAME = '@anicardplaybot'
"""Имя бота"""
"""🏷️ Имя бота"""

@ -2,12 +2,12 @@
import time
# Импорт модулей приложения
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 +33,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('Выполняется подключение к базе данных')

@ -35,7 +35,7 @@ async def get_top_wins() -> None:
time.sleep(1)
async for message in client.get_chat_history(BOT_NAME, limit=1):
parse_wins_top(message)
await parse_wins_top(message)
await client.read_chat_history(BOT_NAME)
@ -71,7 +71,7 @@ async def get_top_donates() -> None:
time.sleep(1)
async for message in client.get_chat_history(BOT_NAME, limit=1):
parse_donates_top(message)
await parse_donates_top(message)
await client.read_chat_history(BOT_NAME)

@ -3,50 +3,48 @@ import re
# Импорт сторонних модулей
from pyrogram.types import Message
from pyrogram.enums import MessageEntityType
# Импорт модулей приложения
from . import db
from .utils import get_telegram_id, get_telegram_data, clean_icons
def parse_wins_top(message: Message) -> None:
async def parse_wins_top(message: Message) -> None:
"""Выполняет парсинг данных топа побед членов клана"""
if message.text.startswith('🏆 Топ по победам'):
gamers = message.text.split('\n')
gamers.pop(0)
gamers.pop(0)
players = message.text.split('\n')
players.pop(0)
players.pop(0)
WINS = list()
for idx, gamer in enumerate(gamers):
gamer = re.sub(r'^\d+. ', '', gamer) # удаляет нумерацию
gamer, battle_count = gamer.split(' - ') # разделяет ник и количество побед в клановых сражениях
# оставляет ники игроков, Python не может в эмодзи в RegExp, по этому пришлось делать через str.replace()
# gamer = re.sub(r'\s[⚡⚜]$', '', gamer)
gamer = gamer.replace('', '')
gamer = gamer.replace('', '').replace('\ufe0f', '')
gamer = gamer.replace(' 💎', '')
for idx, player in enumerate(players):
player = re.sub(r'^\d+. ', '', player) # удаляет нумерацию
player, battle_count = player.split(' - ') # разделяет ник и количество побед в клановых сражениях
player = clean_icons(player)
battle_count = battle_count.replace('', '') # удаляем эмодзи мечей
battle_count = int(re.sub(r'[^\x00-\x7F]', '', battle_count)) # преобразовывает строку в число
player_url, player_telegram_id = await get_telegram_data(message, idx)
WINS.append(
{
# 'telegram_id': message.entities[idx].url.strip('http://t.me/'),
'username': gamer,
'telegram_id': player_telegram_id,
'username': player,
'count': battle_count,
'url': player_url,
}
)
# print('{0} - {1} (@{2})'.format(gamer, battle_count, message.entities[idx].url.strip('http://t.me/')))
print(WINS)
db.add_data(WINS, True)
else:
pass
def parse_donates_top(message: Message) -> None:
async def parse_donates_top(message: Message) -> None:
"""Выполняет парсинг данных топа пожертвований членов клана"""
if message.text.startswith('🏆 Топ по пожертвованиям'):
players = message.text.split('\n')
@ -60,27 +58,22 @@ def parse_donates_top(message: Message) -> None:
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(' 💎', '')
player = clean_icons(player)
donates_count = donates_count.replace(' 💠', '') # удаляем эмодзи пожертвований
donates_count = int(re.sub(r'[^\x00-\x7F]', '', donates_count)) # преобразовывает строку в число
player_url, player_telegram_id = await get_telegram_data(message, idx)
DONATES.append(
{
# 'telegram_id': message.entities[idx].url.strip('http://t.me/'),
'telegram_id': player_telegram_id,
'username': player,
'count': donates_count,
'url': player_url,
}
)
# print('{0} - {1} (@{2})'.format(player, donates_count, message.entities[idx].url.strip('http://t.me/')))
print(DONATES)
db.add_data(DONATES, False)
else:

66
ClanStat/db.py Normal file

@ -0,0 +1,66 @@
# Импорт модулей стандартной библиотеки
import json
# Импорт сторонних модулей
import psycopg2
class DataBase(object):
"""Класс для работы с базой данных"""
def __init__(self, host: str, port: int, name: str, user: str, password: str):
super(DataBase, self).__init__()
self.host = host
"""🖥️ Хост 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,
)
self.conn.autocommit = True # Активирует автоматический коммит транзакций
return True
def close(self) -> bool:
"""Закрывает подключение к базе данных"""
self.conn.close()
return True
def commit(self) -> None:
"""Выполняет коммит"""
self.conn.commit()
return True
def add_data(self, data, wins: bool) -> bool: # wins изменит на Enum
"""Добавляет данные в базу данных"""
if wins:
table = 'wins'
else:
table = 'donates'
cur = self.conn.cursor()
cur.execute(
'INSERT INTO {table} ("data") VALUES (\'{data}\')'.format(
table=table,
data=json.dumps(
data,
ensure_ascii=False,
),
)
)
cur.close()
return True

41
ClanStat/utils.py Normal file

@ -0,0 +1,41 @@
# Импорт сторонних модулей
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_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

13
HISTORY.md Normal file

@ -0,0 +1,13 @@
---
gitea: none
include_toc: true
---
# 📄 История изменений
## 🪧 Легенда
* 🟢 Добавлено
* 🔴 Удалёно
* ♻️ Исправлено

@ -3,24 +3,36 @@ gitea: none
include_toc: true
---
# 🎴 Бот для ведения статистики клана в AniCard
# 🎴 Клиент для ведения статистики клана в AniCard
<img src="https://git.a2s.su/AniCard/ClanStat/raw/branch/master/assets/icon.png" alt="Иконка" height="512">
## 📦 Возможности
- [ ] Сохранение статистики побед в КВ членов клана
- [ ] Сохранение статистики пожертвований в сокровищницу членов клана
- [x] Сохранение статистики побед в КВ членов клана
- [x] Сохранение статистики пожертвований в сокровищницу членов клана
## 💽 Установка и использование
### ⬇️ Загрузка проекта
Первым делом необходимо скачать приложение.
```sh
# Скачиваем приложение и переходим в скачанную директорию
git clone https://git.a2s.su/AniCard/ClanStat.git
cd ClanStat
```
### 📄 Подготовка файла конфигурации
Настройки приложения хранится в файле `.env`.
Пример файла с настройками называется `env.example`.
```sh
# Копируем файл с примером конфигурации
cp env.example .env
```
@ -30,10 +42,14 @@ cp env.example .env
- `APP_HASH` — Хэш приложения Telegram.
- `ACCOUNT` — Имя аккаунта пользователя Telegram (_например `db_o_qp`_).
- `LOG_PATH` — Путь в файлу логов (_например `/tmp/test.log`_).
- `DB_PATH` — Путь к файлу базы данных SQLite3 (_например `./db.sqlite`_).
- `DB_HOST` — Хост базы данных PostgreSQL
- `DB_PORT` — Порт сервера базы данных
- `DB_NAME` — Имя базы данных
- `DB_USER` — Имя пользователя
- `DB_PASSWORD` — Пароль пользователя
### 🐦‍🔥 Настройка виртуального окружения и загрузка бибилотек
### 🐦‍🔥 Настройка виртуального окружения и загрузка библиотек
Создание и активация виртуального окружения.
@ -80,4 +96,6 @@ Enter confirmation code: 14628
- **Pyrogram** — Telegram клиент
- **python-dotenv** — Работа с `.env` файлами
- **loguru** — Логирование
- **psycopg2** — Работа с PostgreSQL
- **black** — Форматирование кода
- **pdoc** — Генератор документации Python кода

@ -1,41 +0,0 @@
# Импорт модулей стандартной библиотеки
import sqlite3
import json
class DataBase(object):
"""Класс для работы с базой данных"""
def __init__(self, path):
super(DataBase, self).__init__()
self.path = path
"""Путь к файлу базы данных SQLite3"""
def connect(self) -> bool:
"""Выполняет подключение к базе данных"""
self.conn = sqlite3.connect(self.path)
return True
def close(self) -> bool:
"""Закрывает подключение к базе данных"""
self.conn.close()
return True
def commit(self) -> None:
"""Выполняет коммит"""
self.conn.commit()
return True
def add_data(self, data, wins: bool) -> bool: # wins изменит на Enum
"""Добавляет данные в базу данных"""
if wins:
table = 'wins'
else:
table = 'donates'
cur = self.conn.cursor()
cur.execute('INSERT INTO {table} (\'data\') VALUES (\'{data}\')'.format(table=table, data=json.dumps(data)))
self.commit()
cur.close()
return True

@ -1,8 +0,0 @@
from . import client
async def get_telegram_id(username: str) -> int:
"""Возвращает Telegram ID по имени пользователя"""
telegram_user = await client.get_users(username)
return telegram_user.id

0
dist/.gitkeep vendored Normal file

@ -2,4 +2,8 @@ APP_ID=""
APP_HASH=""
ACCOUNT=""
LOG_PATH="/tmp/test.log"
DB_PATH="./db.sqlite"
DB_HOST=""
DB_PORT=""
DB_NAME=""
DB_USER=""
DB_PASSWORD=""

@ -1,7 +1,7 @@
[project]
dynamic = [ "version" ]
name = "..."
description = "..."
name = "ClanStat"
description = "AniCard Clan Statistics"
authors = [ {name = "Alexander Popov", email = "iiiypuk@fastmail.fm"}, ]
readme = "README.md"
license = { text = "MIT-0 License" }

@ -1 +1,2 @@
black==24.10.0
pdoc==15.0.1

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

77
scripts/make-appimage.sh Executable file

@ -0,0 +1,77 @@
#!/bin/sh
# Импортирует скрипт с определениями цветов
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

3
scripts/mkdocs.sh Executable file

@ -0,0 +1,3 @@
#!/bin/sh
pdoc -o docs/ app

@ -1,12 +1,8 @@
BEGIN;
CREATE TABLE IF NOT EXISTS 'wins'
(
'id' Integer PRIMARY KEY AUTOINCREMENT,
'timestamp' DateTime NOT NULL DEFAULT (datetime(CURRENT_TIMESTAMP, 'localtime')),
'data' Text NOT NULL
);
CREATE INDEX IF NOT EXISTS 'index_timestamp' ON 'wins' ('timestamp');
CREATE TABLE "public"."wins" (
"data" JSON NOT NULL,
"timestamp" Timestamp Without Time Zone DEFAULT CURRENT_TIMESTAMP );
;
COMMIT;