- добавление скриптов для systemd
This commit is contained in:
Binary file not shown.
33
bot-news-linux-gaming.service
Normal file
33
bot-news-linux-gaming.service
Normal file
@@ -0,0 +1,33 @@
|
||||
[Unit]
|
||||
Description=Linux Gaming News Bot
|
||||
Documentation=https://github.com/xpamych/bot-news-linux-gaming
|
||||
After=network.target network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=xpamych
|
||||
Group=xpamych
|
||||
WorkingDirectory=/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming
|
||||
ExecStart=/usr/bin/python3 /home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming/news-bot-modular.py
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
# Переменные окружения
|
||||
Environment=PYTHONPATH=/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming
|
||||
Environment=PYTHONUNBUFFERED=1
|
||||
|
||||
# Ограничения безопасности
|
||||
NoNewPrivileges=yes
|
||||
PrivateTmp=yes
|
||||
ProtectSystem=strict
|
||||
ProtectHome=read-only
|
||||
ReadWritePaths=/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming
|
||||
|
||||
# Логирование
|
||||
StandardOutput=journal
|
||||
StandardError=journal
|
||||
SyslogIdentifier=news-bot
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@@ -80,14 +80,50 @@ class DiscordClient:
|
||||
return None
|
||||
|
||||
async def send_message(self, channel, content):
|
||||
"""Отправка сообщения в Discord канал"""
|
||||
"""Отправка сообщения в Discord канал с разбивкой длинных сообщений"""
|
||||
if not self.is_enabled():
|
||||
return
|
||||
|
||||
try:
|
||||
# Разбиваем содержимое на части по 2000 символов (лимит Discord)
|
||||
for i in range(0, len(content), 2000):
|
||||
await channel.send(content[i:i+2000])
|
||||
# Discord лимит: 2000 символов
|
||||
max_length = 2000
|
||||
|
||||
if len(content) <= max_length:
|
||||
# Короткое сообщение - отправляем как есть
|
||||
await channel.send(content)
|
||||
else:
|
||||
# Длинное сообщение - разбиваем умно по строкам
|
||||
self.logger.warning(f"Сообщение слишком длинное ({len(content)} символов), разбиваем на части")
|
||||
|
||||
parts = []
|
||||
current_part = ""
|
||||
|
||||
for line in content.split('\n'):
|
||||
# Если добавление этой строки превысит лимит
|
||||
if len(current_part + line + '\n') > max_length:
|
||||
if current_part:
|
||||
parts.append(current_part.rstrip())
|
||||
current_part = ""
|
||||
current_part += line + '\n'
|
||||
|
||||
# Добавляем последнюю часть
|
||||
if current_part:
|
||||
parts.append(current_part.rstrip())
|
||||
|
||||
self.logger.info(f"Сообщение разбито на {len(parts)} частей для Discord")
|
||||
|
||||
# Отправляем каждую часть
|
||||
for i, part in enumerate(parts, 1):
|
||||
if len(parts) > 1:
|
||||
part_content = f"[Часть {i}/{len(parts)}]\n\n{part}"
|
||||
else:
|
||||
part_content = part
|
||||
await channel.send(part_content)
|
||||
|
||||
# Небольшая задержка между частями
|
||||
if i < len(parts):
|
||||
await asyncio.sleep(1)
|
||||
|
||||
self.logger.info("Сообщение успешно отправлено в Discord")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки сообщения в Discord: {e}")
|
||||
@@ -142,7 +178,7 @@ class DiscordClient:
|
||||
await client.close()
|
||||
return
|
||||
|
||||
# Публикуем новости
|
||||
# Публикуем новости в обратном порядке, чтобы новые оказались сверху в ленте
|
||||
for topic_id, topic_title in reversed(list_for_public):
|
||||
from site_api import SiteAPI
|
||||
site_api = SiteAPI()
|
||||
|
127
install-service.sh
Executable file
127
install-service.sh
Executable file
@@ -0,0 +1,127 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Установочный скрипт для Linux Gaming News Bot
|
||||
# Создает systemd service и настраивает его для автозапуска
|
||||
|
||||
set -e # Выход при любой ошибке
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функции для цветного вывода
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Проверка запуска от root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "Скрипт должен быть запущен с правами root (sudo)"
|
||||
print_info "Используйте: sudo ./install-service.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Определение текущего пользователя (не root)
|
||||
REAL_USER=$(who am i | awk '{print $1}')
|
||||
if [ -z "$REAL_USER" ]; then
|
||||
REAL_USER=$(logname 2>/dev/null || echo $SUDO_USER)
|
||||
fi
|
||||
|
||||
if [ -z "$REAL_USER" ]; then
|
||||
print_error "Не удалось определить имя пользователя"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
print_info "Установка сервиса для пользователя: $REAL_USER"
|
||||
|
||||
# Определение директории проекта
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$SCRIPT_DIR"
|
||||
|
||||
print_info "Директория проекта: $PROJECT_DIR"
|
||||
|
||||
# Проверка существования файлов
|
||||
if [ ! -f "$PROJECT_DIR/news-bot-modular.py" ]; then
|
||||
print_error "Не найден файл news-bot-modular.py в $PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$PROJECT_DIR/bot-news-linux-gaming.service" ]; then
|
||||
print_error "Не найден файл bot-news-linux-gaming.service в $PROJECT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Проверка наличия keys.py
|
||||
if [ ! -f "$PROJECT_DIR/keys.py" ]; then
|
||||
print_warning "Не найден файл keys.py с настройками"
|
||||
print_info "Создайте keys.py на основе keys_example.py перед запуском сервиса:"
|
||||
print_info " cp keys_example.py keys.py"
|
||||
print_info " nano keys.py # заполните реальными ключами"
|
||||
fi
|
||||
|
||||
# Создание временного файла сервиса с правильными путями
|
||||
TEMP_SERVICE=$(mktemp)
|
||||
sed "s|/home/xpamych/Yandex.Disk/IdeaProjects/bot-news-linux-gaming|$PROJECT_DIR|g" "$PROJECT_DIR/bot-news-linux-gaming.service" > "$TEMP_SERVICE"
|
||||
sed -i "s|User=xpamych|User=$REAL_USER|g" "$TEMP_SERVICE"
|
||||
sed -i "s|Group=xpamych|Group=$REAL_USER|g" "$TEMP_SERVICE"
|
||||
|
||||
print_info "Копирование systemd unit файла..."
|
||||
cp "$TEMP_SERVICE" /etc/systemd/system/bot-news-linux-gaming.service
|
||||
rm "$TEMP_SERVICE"
|
||||
|
||||
print_info "Установка прав доступа..."
|
||||
chmod 644 /etc/systemd/system/bot-news-linux-gaming.service
|
||||
chown root:root /etc/systemd/system/bot-news-linux-gaming.service
|
||||
|
||||
print_info "Перезагрузка systemd..."
|
||||
systemctl daemon-reload
|
||||
|
||||
print_success "Сервис успешно установлен!"
|
||||
print_info ""
|
||||
print_info "Для управления сервисом используйте команды:"
|
||||
print_info " sudo systemctl enable bot-news-linux-gaming # Включить автозапуск"
|
||||
print_info " sudo systemctl start bot-news-linux-gaming # Запустить сервис"
|
||||
print_info " sudo systemctl status bot-news-linux-gaming # Посмотреть статус"
|
||||
print_info " sudo systemctl stop bot-news-linux-gaming # Остановить сервис"
|
||||
print_info " sudo systemctl disable bot-news-linux-gaming # Отключить автозапуск"
|
||||
print_info ""
|
||||
print_info "Логи сервиса:"
|
||||
print_info " sudo journalctl -u bot-news-linux-gaming -f # Следить за логами"
|
||||
print_info " sudo journalctl -u bot-news-linux-gaming -n 50 # Последние 50 строк"
|
||||
print_info ""
|
||||
|
||||
# Предложение сразу включить и запустить
|
||||
read -p "Включить автозапуск и запустить сервис сейчас? (y/N): " -n 1 -r
|
||||
echo
|
||||
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
||||
print_info "Включение автозапуска..."
|
||||
systemctl enable bot-news-linux-gaming
|
||||
|
||||
print_info "Запуск сервиса..."
|
||||
systemctl start bot-news-linux-gaming
|
||||
|
||||
sleep 2
|
||||
|
||||
print_info "Статус сервиса:"
|
||||
systemctl status bot-news-linux-gaming --no-pager
|
||||
|
||||
print_success "Сервис запущен и добавлен в автозагрузку!"
|
||||
else
|
||||
print_info "Сервис установлен, но не запущен. Запустите его командой:"
|
||||
print_info " sudo systemctl enable --now bot-news-linux-gaming"
|
||||
fi
|
155
service-control.sh
Executable file
155
service-control.sh
Executable file
@@ -0,0 +1,155 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Простой скрипт управления Linux Gaming News Bot
|
||||
|
||||
SERVICE_NAME="bot-news-linux-gaming"
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Функция показа статуса
|
||||
show_status() {
|
||||
print_info "Статус сервиса $SERVICE_NAME:"
|
||||
systemctl status $SERVICE_NAME --no-pager -l
|
||||
}
|
||||
|
||||
# Функция показа логов
|
||||
show_logs() {
|
||||
local lines=${1:-50}
|
||||
print_info "Последние $lines строк логов:"
|
||||
sudo journalctl -u $SERVICE_NAME -n $lines --no-pager
|
||||
}
|
||||
|
||||
# Функция следения за логами
|
||||
follow_logs() {
|
||||
print_info "Следование за логами (Ctrl+C для выхода):"
|
||||
sudo journalctl -u $SERVICE_NAME -f
|
||||
}
|
||||
|
||||
# Функция показа помощи
|
||||
show_help() {
|
||||
echo "Управление Linux Gaming News Bot"
|
||||
echo ""
|
||||
echo "Использование: $0 [команда]"
|
||||
echo ""
|
||||
echo "Команды:"
|
||||
echo " start - Запустить сервис"
|
||||
echo " stop - Остановить сервис"
|
||||
echo " restart - Перезапустить сервис"
|
||||
echo " status - Показать статус сервиса"
|
||||
echo " enable - Включить автозапуск"
|
||||
echo " disable - Отключить автозапуск"
|
||||
echo " logs - Показать последние логи"
|
||||
echo " logs N - Показать последние N строк логов"
|
||||
echo " follow - Следить за логами в реальном времени"
|
||||
echo " install - Установить сервис (требует sudo)"
|
||||
echo " uninstall - Удалить сервис (требует sudo)"
|
||||
echo " help - Показать эту справку"
|
||||
echo ""
|
||||
}
|
||||
|
||||
# Проверка существования сервиса
|
||||
check_service_exists() {
|
||||
if ! systemctl list-unit-files | grep -q "^$SERVICE_NAME.service"; then
|
||||
print_error "Сервис $SERVICE_NAME не установлен"
|
||||
print_info "Запустите: sudo ./install-service.sh"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Основная логика
|
||||
case "${1}" in
|
||||
"start")
|
||||
check_service_exists
|
||||
print_info "Запуск сервиса..."
|
||||
sudo systemctl start $SERVICE_NAME
|
||||
show_status
|
||||
;;
|
||||
"stop")
|
||||
check_service_exists
|
||||
print_info "Остановка сервиса..."
|
||||
sudo systemctl stop $SERVICE_NAME
|
||||
show_status
|
||||
;;
|
||||
"restart")
|
||||
check_service_exists
|
||||
print_info "Перезапуск сервиса..."
|
||||
sudo systemctl restart $SERVICE_NAME
|
||||
show_status
|
||||
;;
|
||||
"status")
|
||||
check_service_exists
|
||||
show_status
|
||||
;;
|
||||
"enable")
|
||||
check_service_exists
|
||||
print_info "Включение автозапуска..."
|
||||
sudo systemctl enable $SERVICE_NAME
|
||||
print_success "Автозапуск включен"
|
||||
;;
|
||||
"disable")
|
||||
check_service_exists
|
||||
print_info "Отключение автозапуска..."
|
||||
sudo systemctl disable $SERVICE_NAME
|
||||
print_success "Автозапуск отключен"
|
||||
;;
|
||||
"logs")
|
||||
check_service_exists
|
||||
if [ -n "$2" ] && [[ "$2" =~ ^[0-9]+$ ]]; then
|
||||
show_logs $2
|
||||
else
|
||||
show_logs
|
||||
fi
|
||||
;;
|
||||
"follow")
|
||||
check_service_exists
|
||||
follow_logs
|
||||
;;
|
||||
"install")
|
||||
if [ ! -f "./install-service.sh" ]; then
|
||||
print_error "Файл install-service.sh не найден"
|
||||
exit 1
|
||||
fi
|
||||
sudo ./install-service.sh
|
||||
;;
|
||||
"uninstall")
|
||||
if [ ! -f "./uninstall-service.sh" ]; then
|
||||
print_error "Файл uninstall-service.sh не найден"
|
||||
exit 1
|
||||
fi
|
||||
sudo ./uninstall-service.sh
|
||||
;;
|
||||
"help"|"--help"|"-h")
|
||||
show_help
|
||||
;;
|
||||
"")
|
||||
print_warning "Команда не указана"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
print_error "Неизвестная команда: $1"
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
@@ -82,37 +82,81 @@ class TelegramNewsClient:
|
||||
return None
|
||||
|
||||
async def send_message(self, client, channel_username, content):
|
||||
"""Отправка сообщения в Telegram канал/топик с обработкой flood wait"""
|
||||
"""Отправка сообщения в Telegram канал/топик с обработкой flood wait и длинных сообщений"""
|
||||
try:
|
||||
# Получаем entity канала
|
||||
entity = await client.get_entity(channel_username)
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Если указан topic_id, отправляем в топик
|
||||
if self.config['topic_id']:
|
||||
self.logger.debug(f"Отправка в топик {self.config['topic_id']}")
|
||||
await client.send_message(
|
||||
entity,
|
||||
content,
|
||||
reply_to=self.config['topic_id']
|
||||
)
|
||||
self.logger.info(f"Сообщение успешно отправлено в Telegram топик {self.config['topic_id']}")
|
||||
# Telegram лимит: 4096 символов
|
||||
max_length = 4096
|
||||
|
||||
# Если сообщение слишком длинное, разбиваем его
|
||||
if len(content) > max_length:
|
||||
self.logger.warning(f"Сообщение слишком длинное ({len(content)} символов), разбиваем на части")
|
||||
|
||||
# Разбиваем по параграфам, чтобы не резать посередине слов
|
||||
parts = []
|
||||
current_part = ""
|
||||
|
||||
for line in content.split('\n'):
|
||||
# Если добавление этой строки превысит лимит
|
||||
if len(current_part + line + '\n') > max_length:
|
||||
if current_part:
|
||||
parts.append(current_part.rstrip())
|
||||
current_part = ""
|
||||
current_part += line + '\n'
|
||||
|
||||
# Добавляем последнюю часть
|
||||
if current_part:
|
||||
parts.append(current_part.rstrip())
|
||||
|
||||
self.logger.info(f"Сообщение разбито на {len(parts)} частей")
|
||||
|
||||
# Отправляем каждую часть
|
||||
for i, part in enumerate(parts, 1):
|
||||
if len(parts) > 1:
|
||||
part_content = f"[Часть {i}/{len(parts)}]\n\n{part}"
|
||||
else:
|
||||
# Обычная отправка в канал
|
||||
await client.send_message(entity, content)
|
||||
self.logger.info("Сообщение успешно отправлено в Telegram канал")
|
||||
break
|
||||
except FloodWaitError as e:
|
||||
self.logger.warning(f"Flood wait error: нужно подождать {e.seconds} секунд")
|
||||
await asyncio.sleep(e.seconds)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки сообщения в Telegram: {e}")
|
||||
break
|
||||
part_content = part
|
||||
|
||||
await self._send_single_message(client, entity, part_content)
|
||||
|
||||
# Небольшая задержка между частями
|
||||
if i < len(parts):
|
||||
await asyncio.sleep(1)
|
||||
else:
|
||||
# Обычная отправка
|
||||
await self._send_single_message(client, entity, content)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка получения entity канала '{channel_username}': {e}")
|
||||
self.logger.info("Убедитесь, что имя канала указано правильно и бот имеет доступ")
|
||||
|
||||
async def _send_single_message(self, client, entity, content):
|
||||
"""Отправка одного сообщения с обработкой flood wait"""
|
||||
while True:
|
||||
try:
|
||||
# Если указан topic_id, отправляем в топик
|
||||
if self.config['topic_id']:
|
||||
self.logger.debug(f"Отправка в топик {self.config['topic_id']}")
|
||||
await client.send_message(
|
||||
entity,
|
||||
content,
|
||||
reply_to=self.config['topic_id']
|
||||
)
|
||||
self.logger.info(f"Сообщение успешно отправлено в Telegram топик {self.config['topic_id']}")
|
||||
else:
|
||||
# Обычная отправка в канал
|
||||
await client.send_message(entity, content)
|
||||
self.logger.info("Сообщение успешно отправлено в Telegram канал")
|
||||
break
|
||||
except FloodWaitError as e:
|
||||
self.logger.warning(f"Flood wait error: нужно подождать {e.seconds} секунд")
|
||||
await asyncio.sleep(e.seconds)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка отправки сообщения в Telegram: {e}")
|
||||
raise
|
||||
|
||||
async def check_and_publish_news(self, news_list):
|
||||
"""Проверка и публикация новостей в Telegram"""
|
||||
self.logger.info("Начинаем проверку новостей для Telegram")
|
||||
@@ -151,7 +195,7 @@ class TelegramNewsClient:
|
||||
|
||||
self.logger.info(f"Новости для публикации в Telegram: {list_for_public}")
|
||||
|
||||
# Публикуем новости
|
||||
# Публикуем новости в обратном порядке, чтобы новые оказались сверху в ленте
|
||||
for topic_id, topic_title in reversed(list_for_public):
|
||||
from site_api import SiteAPI
|
||||
site_api = SiteAPI()
|
||||
|
68
uninstall-service.sh
Executable file
68
uninstall-service.sh
Executable file
@@ -0,0 +1,68 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Скрипт удаления Linux Gaming News Bot systemd service
|
||||
|
||||
set -e # Выход при любой ошибке
|
||||
|
||||
# Цвета для вывода
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Функции для цветного вывода
|
||||
print_info() {
|
||||
echo -e "${BLUE}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
print_success() {
|
||||
echo -e "${GREEN}[SUCCESS]${NC} $1"
|
||||
}
|
||||
|
||||
print_warning() {
|
||||
echo -e "${YELLOW}[WARNING]${NC} $1"
|
||||
}
|
||||
|
||||
print_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# Проверка запуска от root
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
print_error "Скрипт должен быть запущен с правами root (sudo)"
|
||||
print_info "Используйте: sudo ./uninstall-service.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SERVICE_NAME="bot-news-linux-gaming"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
print_info "Удаление сервиса $SERVICE_NAME..."
|
||||
|
||||
# Остановка сервиса если он запущен
|
||||
if systemctl is-active --quiet $SERVICE_NAME; then
|
||||
print_info "Остановка сервиса..."
|
||||
systemctl stop $SERVICE_NAME
|
||||
fi
|
||||
|
||||
# Отключение автозапуска если включен
|
||||
if systemctl is-enabled --quiet $SERVICE_NAME; then
|
||||
print_info "Отключение автозапуска..."
|
||||
systemctl disable $SERVICE_NAME
|
||||
fi
|
||||
|
||||
# Удаление файла сервиса
|
||||
if [ -f "$SERVICE_FILE" ]; then
|
||||
print_info "Удаление файла сервиса..."
|
||||
rm "$SERVICE_FILE"
|
||||
else
|
||||
print_warning "Файл сервиса не найден: $SERVICE_FILE"
|
||||
fi
|
||||
|
||||
# Перезагрузка systemd
|
||||
print_info "Перезагрузка systemd..."
|
||||
systemctl daemon-reload
|
||||
|
||||
print_success "Сервис $SERVICE_NAME успешно удален!"
|
||||
print_info "Файлы проекта не затронуты и остались на месте."
|
@@ -155,7 +155,7 @@ class VKClient:
|
||||
|
||||
self.logger.info(f"Новости для публикации в VK: {list_for_public}")
|
||||
|
||||
# Публикуем новости
|
||||
# Публикуем новости в обратном порядке, чтобы новые оказались сверху в ленте
|
||||
for topic_id, topic_title in reversed(list_for_public):
|
||||
from site_api import SiteAPI
|
||||
site_api = SiteAPI()
|
||||
|
Reference in New Issue
Block a user