#PXI_NET
🙂 Всем привет! Надеюсь вы помните про программное средство анализа доступности веб-сайтов. Я обещал, что сделаю телеграмм бота, чтобы вы могли тоже проверять интересующие вас сайты. Особенно это актуально сейчас. Пару дней подписчики, которые успели, смогли поучаствовать в тестировании бота и остались довольны (за это я вас благодарю). Сегодня я расскажу про то, как я его сделал.
⚡️ Как делаются ТГ-Боты?
Начнём с того, что есть достаточно много способов создания своего тг бота, но почти все они так или иначе взаимодействуют с botfather (бот, созданный telegram специально для создания других ботов). Я использовал асинхронную библиотеку aiogram, которая предоставляет API для работы с telegram.
Сначала мы должны создать объект класса Bot, этот объект ничего сам не делает, он лишь предоставляет нам доступ к API telegram. Далее идём в botfather и создаём нового бота, даём ему имя и получаем свой токен. Этот токен передаём в качестве параметра для конструктора при создании объекта класса Bot.
Далее интересный момент, телеграмм сейчас у меня тормозят, а для общения с его серверами нужен прокси или впн. Но если просто включить впн на компьютере, где запущено программное средство, то и проверка будет уже не для меня, а как будто я нахожусь в том месте, где сервер VPN. Поэтому я использовал свой телефон и VPN на нём в качестве прокси сервера именно для обращения к серверам телеграмма. Это задаётся параметром session при создании объекта класса Bot.
Как я сказал сам объект Bot лишь предоставляет доступ к API, то нам нужен тот, кто умеет что-то делать и знает, что делать на конкретный запрос от пользователя. Это объект класса Dispatcher.
Теперь перейдём к первой функции с декоратором @db.message(Command("start")).
Эта функция срабатывает, когда пользователь отправляет /start боту. Объект types.Message автоматически отправляет бот нам, это объект сообщения пользователя в данном случае, но может быть и нашего. В нём есть информация о имени пользователя, тексте сообщения, с какого устройства отправлено и тд. Мы же в ответ на это вызываем метод answer, который отправляет сообщение пользователю, а также возвращает объект уже нашего сообщения.
💬 На этом пока что всё, завтра расскажу про то, как реализовал непосредственно функцию анализа доступности веб-сайтов.
Начнём с того, что есть достаточно много способов создания своего тг бота, но почти все они так или иначе взаимодействуют с botfather (бот, созданный telegram специально для создания других ботов). Я использовал асинхронную библиотеку aiogram, которая предоставляет API для работы с telegram.
Сначала мы должны создать объект класса Bot, этот объект ничего сам не делает, он лишь предоставляет нам доступ к API telegram. Далее идём в botfather и создаём нового бота, даём ему имя и получаем свой токен. Этот токен передаём в качестве параметра для конструктора при создании объекта класса Bot.
Далее интересный момент, телеграмм сейчас у меня тормозят, а для общения с его серверами нужен прокси или впн. Но если просто включить впн на компьютере, где запущено программное средство, то и проверка будет уже не для меня, а как будто я нахожусь в том месте, где сервер VPN. Поэтому я использовал свой телефон и VPN на нём в качестве прокси сервера именно для обращения к серверам телеграмма. Это задаётся параметром session при создании объекта класса Bot.
Как я сказал сам объект Bot лишь предоставляет доступ к API, то нам нужен тот, кто умеет что-то делать и знает, что делать на конкретный запрос от пользователя. Это объект класса Dispatcher.
import asyncio
import logging
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
from aiogram.client.session.aiohttp import AiohttpSession
from aiogram.utils.markdown import hbold, hcode
from app.checker import analyze_availability
import dotenv
import os
import httpx
dotenv.load_dotenv()
TOKEN=os.getenv("BOT_TOKEN")
proxy_url = os.getenv("PROXY_URL", "http://10.166.129.106:8080" )
session = AiohttpSession(proxy=proxy_url)
bot = Bot(token=TOKEN, session=session)
#bot = Bot(token=TOKEN)
dp = Dispatcher()
@dp.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer("Пришли мне URL (например, https://google.com), и я проверю его доступность.")
Теперь перейдём к первой функции с декоратором @db.message(Command("start")).
Эта функция срабатывает, когда пользователь отправляет /start боту. Объект types.Message автоматически отправляет бот нам, это объект сообщения пользователя в данном случае, но может быть и нашего. В нём есть информация о имени пользователя, тексте сообщения, с какого устройства отправлено и тд. Мы же в ответ на это вызываем метод answer, который отправляет сообщение пользователю, а также возвращает объект уже нашего сообщения.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2🔥2👍1
#PXI_NET
🙂 Привет! Продолжаю прошлый пост про ТГ-бота своего программного средства анализа доступности веб-сайтов.
🌐Функция с анализом доступности
Создаём функцию с декоратором @dp.message(), то есть она будет срабатывать на любое сообщение от пользователя (если не среагировала какая-то другая функция выше).
Получаем от пользователя текст его сообщения через message.text, и передаём в нашу функцию анализа. Получаем результат в виде объекта pydantic модели.
Получив результат анализа мы начинаем формировать красивый ответ. Телеграм имеет встроенные теги для шрифтов (hbold — жирный, hcode — моно), воспользуемся ими.
Далее мы вместо того, чтобы отправить новое сообщение, изменим уже отправленное нами сообщение через функцию status_msg.edit_text. Первым параметром передадим наш сформированый ответ, вторым — метод парсинга (чтобы он понял, что текст имеет теги и их надо правильно отобразить, то есть перевести в HTML теги, а не просто текстом)
В случае ошибки уведомляем пользователя.
❓ Зачем нужна функция main?
Как обычно в асинхронном коде мы создаём цикл событий и вызываем в нём функцию main при помощи asyncio.run(main()).
Функция main() включает логи с уровнем INFO. Метод dp.start_polling(bot) запускает режим опроса, объект bot постоянно опрашивает сервера telegram на наличие новых сообщений. Если они есть, то Dispatcher отправляет их в свои обработчики (check_url_handler или cmd_start у меня).
💬 На этом хочу закончить. Обязательно скажу вам, когда снова можно будет пользоваться ботом.
В следующий раз думаю подробнее рассказать про то, как я добавил схожего бота в свой мессенджер, а позже про асинхронность, многопоточность или же про декораторы.
🌐Функция с анализом доступности
Создаём функцию с декоратором @dp.message(), то есть она будет срабатывать на любое сообщение от пользователя (если не среагировала какая-то другая функция выше).
Получаем от пользователя текст его сообщения через message.text, и передаём в нашу функцию анализа. Получаем результат в виде объекта pydantic модели.
@dp.message()
async def check_url_handler(message: types.Message):
url = message.text.strip()
status_msg = await message.answer(f"🔍 Проверяю {url}...")
try:
result = await analyze_availability(url)
color_emoji = "✅" if result.status_label == "Доступен" else "❌"
if result.status_label == "Нас заблокировали": color_emoji = "⚠️"
response = (
f"{color_emoji} {hbold('Результат для:')} {result.url}\n"
f"🌐 {hbold('IP:')} {hcode(result.ip or 'N/A')}\n"
f"📊 {hbold('Статус:')} {result.status_label}\n"
f"⏱ {hbold('Задержка:')} {result.latency_ms:.1f}ms\n"
f"📝 {hbold('Детали:')} {result.detail or '—'}"
)
await status_msg.edit_text(response, parse_mode="HTML")
except Exception as e:
await status_msg.edit_text(f"❗ Ошибка при проверке: {str(e)}")
Получив результат анализа мы начинаем формировать красивый ответ. Телеграм имеет встроенные теги для шрифтов (hbold — жирный, hcode — моно), воспользуемся ими.
Далее мы вместо того, чтобы отправить новое сообщение, изменим уже отправленное нами сообщение через функцию status_msg.edit_text. Первым параметром передадим наш сформированый ответ, вторым — метод парсинга (чтобы он понял, что текст имеет теги и их надо правильно отобразить, то есть перевести в HTML теги, а не просто текстом)
В случае ошибки уведомляем пользователя.
Как обычно в асинхронном коде мы создаём цикл событий и вызываем в нём функцию main при помощи asyncio.run(main()).
Функция main() включает логи с уровнем INFO. Метод dp.start_polling(bot) запускает режим опроса, объект bot постоянно опрашивает сервера telegram на наличие новых сообщений. Если они есть, то Dispatcher отправляет их в свои обработчики (check_url_handler или cmd_start у меня).
async def main():
logging.basicConfig(level=logging.INFO)
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
В следующий раз думаю подробнее рассказать про то, как я добавил схожего бота в свой мессенджер, а позже про асинхронность, многопоточность или же про декораторы.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1🔥1
#PXI_NET
🙂 Всем привет! Да-да-да, сегодня я снова хочу рассказать про то, что я добавил в средство анализа доступности веб-сайтов. Кстати, совсем скоро я его запущу на постоянку вместе с мессенджером. Расскажу я про то, как я реализовал периодическую проверку веб-сайтов, при этом не заблокировал возможность пользователя делать другие запросы на проверку.
📎 Начнём с обработчика для команды /periodtest
Тут мы получаем вместе с командой аргументы (/periodtest 1 pxi.net), первый аргумент уйдёт в переменную time (время периода в минутах), а все остальные в список urls.
Далее мы проверяем не запустили ли уже проверку для этого пользователя, если нет, то создаём и запускаем задачу в фоне с выполнением нашей функции (покажу её ниже) и заносим эту задачу в словарь, чтоб потом проверять не запущена ли такая задача и суметь остановить её.
⚠️ Остановка задачи
Здесь реализуем функцию остановки задачи (/stop). При помощи метода cancel() останавливаем выполнение задачи в фоне, которую мы получили из словаря по chat_id.
В конце также удаляем объект задачи из словаря.
⚡️ Функция переодической проверки
Здесь мы в цикле вызываем наши асинхронные функции через gather(), формируем общий ответ и уже не изменяем прошлое сообщение, а отправляем новое, так как в тг через некоторое время нельзя будет изменить сообщение.
При помощи asyncio.sleep() заставили ждать только именно эту задачу, другие пользователи и даже тот, который эту задачу вызвал могут пользоваться ботом дальше.
💬 На этом пока что всё, пишите вопросы в комментариях. Буду рад помочь :)
Тут мы получаем вместе с командой аргументы (/periodtest 1 pxi.net), первый аргумент уйдёт в переменную time (время периода в минутах), а все остальные в список urls.
active_checks = {}
@dp.message(Command("periodtest"))
async def period_test_handler(message: types.Message):
chat_id = message.chat.id
try:
time, *urls = message.text.replace("/periodtest","").strip().split()
except ValueError:
await message.answer("Неверная команда, вот пример: /periodtest 10 https://google.com")
return
if float(time) < 0.5:
return await message.answer("Слишком часто! Минимум — раз в 0.5 мин.")
if not urls:
return await message.answer("Неверная команда, вот пример: /periodtest 10 https://google.com")
if chat_id in active_checks:
return await message.answer("У вас уже запущена проверка! Сначала остановите её (команда /stop).")
await message.answer(f"🚀 Запускаю периодическую проверку каждые {time} минут для {len(urls)} URL...")
task = asyncio.create_task(period_check(chat_id,time,urls))
active_checks[chat_id] = taskДалее мы проверяем не запустили ли уже проверку для этого пользователя, если нет, то создаём и запускаем задачу в фоне с выполнением нашей функции (покажу её ниже) и заносим эту задачу в словарь, чтоб потом проверять не запущена ли такая задача и суметь остановить её.
Здесь реализуем функцию остановки задачи (/stop). При помощи метода cancel() останавливаем выполнение задачи в фоне, которую мы получили из словаря по chat_id.
@dp.message(Command("stop"))
async def stop(message: types.Message):
chat_id = message.chat.id
if chat_id in active_checks:
active_checks[chat_id].cancel()
del active_checks[chat_id]
await message.answer("⏹ Мониторинг остановлен.")
else:
await message.answer("У вас нет активных проверок.")В конце также удаляем объект задачи из словаря.
Здесь мы в цикле вызываем наши асинхронные функции через gather(), формируем общий ответ и уже не изменяем прошлое сообщение, а отправляем новое, так как в тг через некоторое время нельзя будет изменить сообщение.
async def period_check(chat_id:int, time:float, urls):
try:
while True:
analyze = [analyze_availability(url) for url in urls]
results = await asyncio.gather(*analyze)
response = ""
for result in results:
color_emoji = "✅" if result.status_label == "Доступен" else "❌"
if result.status_label == "Нас заблокировали": color_emoji = "⚠️"
response += (
f"{color_emoji} {hbold('Результат для:')} {result.url}\n"
f"🌐 {hbold('IP:')} {hcode(result.ip or 'N/A')}\n"
f"📊 {hbold('Статус:')} {result.status_label}\n"
f"⏱ {hbold('Задержка:')} {result.latency_ms:.1f}ms\n"
f"📝 {hbold('Детали:')} {result.detail or '—'}\n\n"
)
await bot.send_message(chat_id, response, parse_mode="HTML")
await asyncio.sleep(float(time)*60)
except asyncio.CancelledError:
logging.info(f"Мониторинг для чата {chat_id} остановлен.")
При помощи asyncio.sleep() заставили ждать только именно эту задачу, другие пользователи и даже тот, который эту задачу вызвал могут пользоваться ботом дальше.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1🔥1
#PXI_PROJECT #PXI_NET
🙂 Всем привет! Наконец-то вы сможете использовать мои проекты каждый день, и каждый сможет поучаствовать в разработке. Сегодня хочу рассказать про бота для анализа сетевой доступности веб-сайтов. Пишите обо всех найденных багах или о падении бота мне в личку моего мессенджера (PXI, мой ник именно большими буквами), в личку в Telegram или в комментариях канала.
❓ Как найти и пользоваться ботом?
Просто нажмите сюда:
@URL_checker_PXI_bot
После того, как перешли в бота, напишите /start, и бот вам всё пояснит.
⚡️ Что с ним сейчас не так
На данный момент я купил VPS в Европе ещё не настроил прокси или ВПН для России, поэтому бот показывает данные для Европы. Но это я в скором времени исправлю (исправил).
💬 В следующем посте расскажу про мессенджер, а ещё чуть позже про VPS, и как вам его можно использовать.
Просто нажмите сюда:
@URL_checker_PXI_bot
После того, как перешли в бота, напишите /start, и бот вам всё пояснит.
На данный момент я купил VPS в Европе ещё не настроил прокси или ВПН для России, поэтому бот показывает данные для Европы. Но это я в скором времени исправлю (исправил).
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1🔥1
#PXI_PROJECT #PXI_WEB
🙂 Всем привет! Сегодня мой мессенджер стал доступен всем для тестирования. Пишите обо всех найденных багах или о падении мессенджера мне в личку или группу моего мессенджера (PXI, мой ник и имя группы именно большими буквами и без смайликов и тд), в личку в Telegram или в комментариях канала.
❓ Как найти и пользоваться мессенджером?
Просто нажмите сюда:
http://2.26.118.52
Зарегистрируйте свой аккаунт и можете искать других пользователей для общения или создавать группы и каналы.
⚡️ Что с ним сейчас не так
Во-первых, если у вас какая-то проблема, попробуйте обновить сайт. Например, сейчас если вы впервые пишете человеку или в группу, то он у вас в списке чатов появится только после обновления страницы, это скоро исправлю (уже исправил). С удалением чатов такой проблемы нет.
Также исправлю проблему с тем, что наши сообщения и нашего собеседника прижимаются все влево (тоже исправил).
В любом случае пишите обо всём, что вам не нравится и кажется сломанным.
⚠️ На данный момент мессенджер нацелен удобством на компьютеры и ноутбуки. Позже добавлю поддержку красивого фронта для смартфонов.
💬 В следующем посте расскажу про VPS, и как вам его можно использовать.
Просто нажмите сюда:
http://2.26.118.52
Зарегистрируйте свой аккаунт и можете искать других пользователей для общения или создавать группы и каналы.
Во-первых, если у вас какая-то проблема, попробуйте обновить сайт. Например, сейчас если вы впервые пишете человеку или в группу, то он у вас в списке чатов появится только после обновления страницы, это скоро исправлю (уже исправил). С удалением чатов такой проблемы нет.
Также исправлю проблему с тем, что наши сообщения и нашего собеседника прижимаются все влево (тоже исправил).
В любом случае пишите обо всём, что вам не нравится и кажется сломанным.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1🔥1
#PXI_PROJECT — готовые и развивающиеся проекты
Я развиваю экосистему полезных сервисов и на данном этапе готовы следующие проекты:
1️⃣ Мой собственный сайт, который является хабом всех проектов и социальных сетей.
http://132.243.17.128/
2️⃣ Бот для проверки сетевой доступности сайтов. Удобно мониторить свои ресурсы по URL или IP прямо в Telegram.
@URL_checker_PXI_bot
3️⃣ PXI Messenger — мой собственный мессенджер.
Проект находится в стадии активной разработки, и сейчас он открыт для всех желающих! Заходите, создавайте аккаунты и общайтесь:
http://2.26.118.52
📎 Полный исходный код можете посмотреть здесь:
https://github.com/geses-off
💬 Буду рад любой обратной связи — ваши отзывы помогают развивать проекты дальше!
Я развиваю экосистему полезных сервисов и на данном этапе готовы следующие проекты:
http://132.243.17.128/
@URL_checker_PXI_bot
Проект находится в стадии активной разработки, и сейчас он открыт для всех желающих! Заходите, создавайте аккаунты и общайтесь:
http://2.26.118.52
https://github.com/geses-off
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
#PXI_NET
🙂 Всем привет! Сегодня будет обещанный пост про VPS/VDS. Кстати, я докупил VPS в России и теперь мой бот проводит анализ из двух точек присутствия (@URL_checker_PXI_bot).
❓ Что такое VPS и VDS?
VPS/VDS — это сервер, который развернут на платформе виртуализации и сдается провайдером в аренду. Можно сказать, что это аналог виртуальной машины.
На дата-центрах есть физические сервера, обычно все их ресурсы объединяют в пул и уже из этого пула продают ресурсы нам. Это позволяет эффективно использовать оборудование и не простаивать ему.
Часто используется динамическое выделение ресурсов под нужны пользователя, например, вы купили VPS с диском на 50 Гб, но используете только 30 тогда ваши 20 можно кому-то отдать, пока вы их не запросите.
⚡️ В чём разница между VPS и VDS?
VPS (Virtual Private Server) — виртуальный приватный сервер.
VDS (Virtual Dedicated Server) — виртуальный выделенный сервер.
Считается, что VPS использует программную виртуализацию (разделение ресурсов происходит на уровне ОС, OpenVZ), а VDS — аппаратную (KVM).
Также существует Shared Hosting — это такой вид хостинга, когда на одном физическом сервере находится множество сайтов различных владельцев. При этом данные этих сайтов изолированы друг от друга. Это наиболее бюджетный вид хостинга и подходит для простых веб-проектов, которые не требуют больших ресурсов.
🖥 Виды VPS/VDS
VPS/VDS разделяют на управляемые, неуправляемые и полууправляемые.
Управляемые VPS/VDS — это когда провайдер берет на себя все задачи по обслуживанию и обновлению серверов, например, установка актуального ПО (с услугой администрирования).
Полууправляемые VPS/VDS — это когда провайдер отвечает только за часть приложений или обновление ядра. Например, он может поддерживать безопасность сервера и связанных с этим приложений.
Неуправляемые или самоуправляемые VPS/VDS — это когда провайдер отвечает только за выделение необходимых ресурсов, а также за доступность.
🌐 Я купил VPS/VDS, что дальше?
В большинстве случаев вам на почту придёт белый IP адрес, логин и пароль от пользователя на сервере. Подключаемся к нему через SSH и используем для своих целей.
Желательно поменять пароль сразу и прописать простенький iptables. Если как я планируете поднимать какой-то сервис с базой данных (например, мессенджер), то не надо этот порт открывать для всех. Вместо 5432:5432 в докере используйте 127.0.0.1:5432:5432.
К сожалению, я решил это оставить на потом и это привело к тому, что бот, сканирующий сеть, успел поставить майнер в мой докер контейнер, благо он изолирован от остального сервера и я легко от него избавился и залатал дыры безопасности.
💬 На этом пока что всё! Буду рад, если вы будете пользоваться моими проектами.
VPS/VDS — это сервер, который развернут на платформе виртуализации и сдается провайдером в аренду. Можно сказать, что это аналог виртуальной машины.
На дата-центрах есть физические сервера, обычно все их ресурсы объединяют в пул и уже из этого пула продают ресурсы нам. Это позволяет эффективно использовать оборудование и не простаивать ему.
Часто используется динамическое выделение ресурсов под нужны пользователя, например, вы купили VPS с диском на 50 Гб, но используете только 30 тогда ваши 20 можно кому-то отдать, пока вы их не запросите.
VPS (Virtual Private Server) — виртуальный приватный сервер.
VDS (Virtual Dedicated Server) — виртуальный выделенный сервер.
Считается, что VPS использует программную виртуализацию (разделение ресурсов происходит на уровне ОС, OpenVZ), а VDS — аппаратную (KVM).
Также существует Shared Hosting — это такой вид хостинга, когда на одном физическом сервере находится множество сайтов различных владельцев. При этом данные этих сайтов изолированы друг от друга. Это наиболее бюджетный вид хостинга и подходит для простых веб-проектов, которые не требуют больших ресурсов.
VPS/VDS разделяют на управляемые, неуправляемые и полууправляемые.
Управляемые VPS/VDS — это когда провайдер берет на себя все задачи по обслуживанию и обновлению серверов, например, установка актуального ПО (с услугой администрирования).
Полууправляемые VPS/VDS — это когда провайдер отвечает только за часть приложений или обновление ядра. Например, он может поддерживать безопасность сервера и связанных с этим приложений.
Неуправляемые или самоуправляемые VPS/VDS — это когда провайдер отвечает только за выделение необходимых ресурсов, а также за доступность.
В большинстве случаев вам на почту придёт белый IP адрес, логин и пароль от пользователя на сервере. Подключаемся к нему через SSH и используем для своих целей.
Желательно поменять пароль сразу и прописать простенький iptables. Если как я планируете поднимать какой-то сервис с базой данных (например, мессенджер), то не надо этот порт открывать для всех. Вместо 5432:5432 в докере используйте 127.0.0.1:5432:5432.
К сожалению, я решил это оставить на потом и это привело к тому, что бот, сканирующий сеть, успел поставить майнер в мой докер контейнер, благо он изолирован от остального сервера и я легко от него избавился и залатал дыры безопасности.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2🔥1
#PXI_WEB
🙂 Всем привет! Что-то давненько я не показывал код нововведений в нашем мессенджере (http://2.26.118.52). Сегодня я покажу как реализовал группы и каналы.
⚡️ Начнём с обновления нашей базы данных, добавим новые таблицы и привяжем все чаты к chat_id
До этого у меня было всего две таблицы Messages и Users. Все сообщения находились через имена пользователей, но раз мы хотим добавлять группы и каналы это уже не прокатит, так как писать может любой человек, поэтому добавим chat_id, таблицу Chats для хранения информации о чатах и ChatMembers для хранения информации о участниках чата.
Также добавил pydantic-модель CreateGroup для валидации данных при создании новых групп и каналов.
🖥 Теперь добавим метод создания групп и каналов
Начать я решил с реализации вкладок и кнопки создания канала на фронтенде, а потом перешёл к более интересной бекенд части.
Здесь мы получаем название и тип создаваемого чата (канал или группа) и валидируем с помощью нашей pydantic-модели.
Далее проверяем нет ли уже с таким названием группы или канала и в случае нахождения отправляем 400 ошибку. Если всё хорошо, то создаём и комитим в базу данных этот чат.
Сначала делаем flush, чтобы данные не зафиксировались в базе данных, но при этом мы смогли получить chat_id и добавить создателя в участники. В конце возвращаем статус код 201 и JSON с chat_id и названием группы или канала.
💬 Накопилось очень много нового функционала и в один пост я всё не смог вместить, поэтому обязательно расскажу всё в следующих.
До этого у меня было всего две таблицы Messages и Users. Все сообщения находились через имена пользователей, но раз мы хотим добавлять группы и каналы это уже не прокатит, так как писать может любой человек, поэтому добавим chat_id, таблицу Chats для хранения информации о чатах и ChatMembers для хранения информации о участниках чата.
class Messages(Base):
__tablename__ = "messages"
id = Column(Integer, primary_key=True, autoincrement=True)
chat_id = Column(Integer, ForeignKey("chats.id"), index=True)
sender_name = Column(Text, ForeignKey("users.username"))
receiver_name = Column(Text, nullable=True) # Для лички
content = Column(Text)
is_read = Column(Boolean, default=False)
timestamp = Column(DateTime, default=get_moscow_now)
class ChatType(enum.Enum):
DIRECT = "direct"
GROUP = "group"
CHANNEL = "channel"
class Chats(Base):
__tablename__ = "chats"
id = Column(Integer, primary_key=True, autoincrement=True)
type = Column(Enum(ChatType), default=ChatType.DIRECT)
name = Column(Text, nullable=True)
created_at = Column(DateTime, default=get_moscow_now)
class ChatMembers(Base):
__tablename__ = "chat_members"
chat_id = Column(Integer, ForeignKey("chats.id"), primary_key=True)
username = Column(Text, ForeignKey("users.username"), primary_key=True)
joined_at = Column(DateTime, default=get_moscow_now)
#pydantic-models
class CreateGroup(BaseModel):
name: str
type: str
Также добавил pydantic-модель CreateGroup для валидации данных при создании новых групп и каналов.
Начать я решил с реализации вкладок и кнопки создания канала на фронтенде, а потом перешёл к более интересной бекенд части.
@router.post("/chat")
async def create_group(request:Request, data: models.CreateGroup, db: SessionDep, username = Depends(auth.get_current_user)):
existing_chat = await db.execute(
select(models.Chats).where(
and_(
models.Chats.name == data.name,
models.Chats.type == (models.ChatType.GROUP if data.type == 'group' else models.ChatType.CHANNEL)
)
)
)
if existing_chat.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Группа или канал с таким названием уже существует"
)
new_chat = models.Chats(name=data.name, type=models.ChatType.GROUP if data.type == 'group' else models.ChatType.CHANNEL)
db.add(new_chat)
await db.flush()
new_member = models.ChatMembers(chat_id=new_chat.id, username=username)
db.add(new_member)
await db.commit()
chat_data = {"chat_id": new_chat.id, "name": new_chat.name}
return JSONResponse(status_code=status.HTTP_201_CREATED,content=chat_data)Здесь мы получаем название и тип создаваемого чата (канал или группа) и валидируем с помощью нашей pydantic-модели.
Далее проверяем нет ли уже с таким названием группы или канала и в случае нахождения отправляем 400 ошибку. Если всё хорошо, то создаём и комитим в базу данных этот чат.
Сначала делаем flush, чтобы данные не зафиксировались в базе данных, но при этом мы смогли получить chat_id и добавить создателя в участники. В конце возвращаем статус код 201 и JSON с chat_id и названием группы или канала.
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2🔥1🥰1
#PXI_WEB
🙂 Всем привет! Продолжаю рассказывать про то, как я добавил группы и каналы в наш мессенджер (http://2.26.118.52). Сегодня расскажу про изменения в работе сокетов и отправки сообщений.
☄️ Обновим отправку сообщений для отправки нескольким людям сразу
Ранее наш класс ConnectionManager хранил текущие соединения с пользователями и имел только метод отправки персональных сообщений для личек. Добавим метод для рассылки всем участникам группы или канала.
Я добавил метод send_broadcast, в нём мы получаем всех участников для чата по chat_id и отправляем им данные, если они сейчас активны через сокет.
☄️ Мы добавили chat_id и множественную рассылку, теперь надо обновить функцию, которая занималась созданием и хранением соединений, а также отправкой сообщений.
Здесь стоит начать с фронтенда, для облегчения работы я использую websocket, а не чистые сокеты.
Первым делом определяем протокол и домен сайта (в моём случае сразу IP), если защищённый https то и переходим (обновляем) на защищённый wss, иначе ws. Важно, что websocket это не сырой сокет 4 уровня OSI, а надстройка над HTTP/HTTPS и отправляет данные с такими же заголовками.
Далее мы создаём объект websocket передавая ему протокол ws или wss и домен нашего сервера, он его резолвит и отправляет HTTP запрос на переход с HTTP/HTTPS на ws/wss. Также передаем, какую ручку нужно дёрнуть (/ws/myName), чтобы наш сервер смог ответить на этот запрос корректно (в случае согласия отправит код 101).
Важно, что обновляется не то соединение, которое было создано при открытии сайта. При создании websocket сначала создаётся новое HTTP/HTTPS соединение и в нем уже GET запрос с заголовком на переход.
💬 На этом пока что всё, ручку опишу в следующем посте!
Ранее наш класс ConnectionManager хранил текущие соединения с пользователями и имел только метод отправки персональных сообщений для личек. Добавим метод для рассылки всем участникам группы или канала.
class ConnectionManager:
def __init__(self):
self.active_connections: dict[str, WebSocket] = {}
async def connect(self, username: str, websocket: WebSocket):
await websocket.accept()
self.active_connections[username] = websocket
def disconnect(self, username: str):
if username in self.active_connections:
del self.active_connections[username]
async def send_personal_message(self, message: dict, receiver: str):
if receiver in self.active_connections:
await self.active_connections[receiver].send_json(message)
async def send_broadcast(self, message:dict, sender: str, chat_id: int):
async with SESSIONLOCAL() as db:
stmt = (select(models.ChatMembers.username).where(models.ChatMembers.chat_id == chat_id).where(models.ChatMembers.username != sender))
results = await db.execute(stmt)
results = results.scalars().all()
for user in results:
if user in self.active_connections:
await self.active_connections[user].send_json(message)
Я добавил метод send_broadcast, в нём мы получаем всех участников для чата по chat_id и отправляем им данные, если они сейчас активны через сокет.
Здесь стоит начать с фронтенда, для облегчения работы я использую websocket, а не чистые сокеты.
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const socket = new WebSocket(`${wsProtocol}//${window.location.host}/ws/${myName}`);Первым делом определяем протокол и домен сайта (в моём случае сразу IP), если защищённый https то и переходим (обновляем) на защищённый wss, иначе ws. Важно, что websocket это не сырой сокет 4 уровня OSI, а надстройка над HTTP/HTTPS и отправляет данные с такими же заголовками.
Далее мы создаём объект websocket передавая ему протокол ws или wss и домен нашего сервера, он его резолвит и отправляет HTTP запрос на переход с HTTP/HTTPS на ws/wss. Также передаем, какую ручку нужно дёрнуть (/ws/myName), чтобы наш сервер смог ответить на этот запрос корректно (в случае согласия отправит код 101).
Важно, что обновляется не то соединение, которое было создано при открытии сайта. При создании websocket сначала создаётся новое HTTP/HTTPS соединение и в нем уже GET запрос с заголовком на переход.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3❤1
#PXI_WEB
🙂 Всем привет! Во-первых, начну с того, что я обновил наш мессенджер, добавив в него профили и отслеживание активности (онлайн или нет и когда).
Сегодня покажу обещанную ручку, которая соглашается на переход с HTTP/HTTPS на ws/wss, а также именно она занимается хранением текущих соединений, сохранением сообщений в базу данных и передачей их другим пользователям через сокет.
⚡️ Ручка /ws/{username}
Здесь если фронт передал chat_id, то считаю группой/каналом и шлю сообщение всем, иначе личкой.
💬 Отвечу на ваши любые вопросы
Сегодня покажу обещанную ручку, которая соглашается на переход с HTTP/HTTPS на ws/wss, а также именно она занимается хранением текущих соединений, сохранением сообщений в базу данных и передачей их другим пользователям через сокет.
@router.websocket("/{username}")
async def websocket_endpoint(websocket: WebSocket, username: str):
await manager.connect(username, websocket)
try:
while True:
data = await websocket.receive_json()
receiver = data.get("receiver")
text = data.get("text")
chat_id = data.get("chat_id")
if(chat_id):
async with SESSIONLOCAL() as db:
new_msg = models.Messages(
chat_id=chat_id,
sender_name=username,
content=text
)
db.add(new_msg)
member_stmt = select(models.ChatMembers).where(
and_(
models.ChatMembers.chat_id == chat_id,
models.ChatMembers.username == username
)
)
member_check = await db.execute(member_stmt)
if not member_check.scalar_one_or_none():
new_member = models.ChatMembers(
chat_id=chat_id,
username=username
)
db.add(new_member)
await db.commit()
await manager.send_broadcast({
"chat_id": chat_id,
"sender": username,
"text": text,
"time": datetime.now().strftime("%H:%M")
},username,chat_id)
else:
async with SESSIONLOCAL() as db:
stmt = (
select(models.ChatMembers.chat_id)
.join(models.Chats, models.Chats.id == models.ChatMembers.chat_id)
.where(models.Chats.type == models.ChatType.DIRECT)
.where(models.ChatMembers.username.in_([username, receiver]))
.group_by(models.ChatMembers.chat_id)
.having(func.count(models.ChatMembers.username) == 2)
)
result = await db.execute(stmt)
chat_id = result.scalar()
if not chat_id:
new_chat = models.Chats(type=models.ChatType.DIRECT)
db.add(new_chat)
await db.flush()
db.add_all([
models.ChatMembers(chat_id=new_chat.id, username=username),
models.ChatMembers(chat_id=new_chat.id, username=receiver)
])
chat_id = new_chat.id
new_msg = models.Messages(
chat_id=chat_id,
sender_name=username,
receiver_name=receiver,
content=text
)
db.add(new_msg)
await db.commit()
if receiver == "URL_checker🌐":
asyncio.create_task(handle_bot_logic(username, text,chat_id))
await manager.send_personal_message({
"chat_id": chat_id,
"sender": username,
"text": text,
"time": datetime.now().strftime("%H:%M")
}, receiver)
except WebSocketDisconnect:
manager.disconnect(username)Здесь если фронт передал chat_id, то считаю группой/каналом и шлю сообщение всем, иначе личкой.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3❤1
#PXI_WEB
🙂 Всем привет! Продолжаю рассказывать про нововведения в нашем мессенджере. В пошлых постах мы добавили новые таблицы Chats, ChatMembers и другие, теперь наши сообщения в директах тоже имеют chat_id. Мы добавили также создание групп и каналов, а также обновили отправку сообщений, добавив функцию, которая отправляет всем участникам сразу. Но есть проблемы, которые надо решить...
⚡️ Как найти группы и каналы?
Однако, хоть во фронтенде мы добавили вкладки для групп и каналов, сейчас там ничего нет, мы не можем даже найти каналы, которые мы создали. Исправим это добавив к поиску пользователей, поиск групп или каналов в зависимости от открытой вкладки.
Функция аналогична поиску пользователей, но теперь используем ChatType.
☄️ Как получить сообщения относящиеся к группе/каналу?
Мы можем теперь находить группы и каналы, даже отправить сообщение, но как нам получить историю сообщений? Надо переписать функцию для получения сообщений. Я решил для директов тянуть получение через имена (как и было), чтобы не менять фронтенд, а для групп и каналов будем использовать chat_id.
Получение по именам для директов:
Получение по chat_id:
В обоих случаях мы делаем запрос в базу данных, вытягиваем нужные сообщения и отправляем список словарей. FastAPI сам преобразует его в JSON, а на фронте мы преобразуем JSON в массив объектов JavaScript .
💬 На этом пока что всё, в следующий раз расскажу, как я реализовал профили, удаление чатов и загрузку групп и каналов, в которых мы состоим, при переходе в нужную вкладку.
Однако, хоть во фронтенде мы добавили вкладки для групп и каналов, сейчас там ничего нет, мы не можем даже найти каналы, которые мы создали. Исправим это добавив к поиску пользователей, поиск групп или каналов в зависимости от открытой вкладки.
@router.get("/search_chats")
async def search_chats(query:str, type: str, db:SessionDep):
stmt = select(models.Chats).where(models.Chats.type == models.ChatType(type)).where(models.Chats.name.ilike(f"%{query}%"))
result = await db.execute(stmt)
return [{"id": c.id, "name": c.name} for c in result.scalars().all()]Функция аналогична поиску пользователей, но теперь используем ChatType.
Мы можем теперь находить группы и каналы, даже отправить сообщение, но как нам получить историю сообщений? Надо переписать функцию для получения сообщений. Я решил для директов тянуть получение через имена (как и было), чтобы не менять фронтенд, а для групп и каналов будем использовать chat_id.
Получение по именам для директов:
@router.get("/messages/by_name/{chat_with}")
async def get_messages_by_name(
chat_with: str,
database: SessionDep,
current_user: str = Depends(auth.get_current_user)
):
stmt = (
select(models.Messages)
.join(models.Chats, models.Chats.id == models.Messages.chat_id)
.where(models.Chats.type == models.ChatType.DIRECT)
.where(
or_(
and_(models.Messages.sender_name == current_user, models.Messages.receiver_name == chat_with),
and_(models.Messages.sender_name == chat_with, models.Messages.receiver_name == current_user)
)
)
.order_by(models.Messages.timestamp.asc())
)
result = await database.execute(stmt)
messages = result.scalars().all()
return [
{
"sender": m.sender_name,
"text": m.content,
"time": m.timestamp.strftime("%H:%M"),
"type": "outgoing" if m.sender_name == current_user else "incoming"
} for m in messages
]Получение по chat_id:
@router.get("/messages/{chat_id}")
async def get_messages(chat_id: int, database: SessionDep, current_user: str = Depends(auth.get_current_user)):
check_stmt = select(models.ChatMembers).where(
and_(
models.ChatMembers.chat_id == chat_id,
models.ChatMembers.username == current_user
)
)
is_member = await database.execute(check_stmt)
if not is_member.scalar_one_or_none():
raise HTTPException(status_code=403, detail="Вы не состоите в этом чате")
stmt = (
select(models.Messages)
.where(models.Messages.chat_id == chat_id)
.order_by(models.Messages.timestamp.asc())
)
result = await database.execute(stmt)
messages = result.scalars().all()
return [
{
"sender": m.sender_name,
"text": m.content,
"time": m.timestamp.strftime("%H:%M"),
"type": "outgoing" if m.sender_name == current_user else "incoming"
} for m in messages
]В обоих случаях мы делаем запрос в базу данных, вытягиваем нужные сообщения и отправляем список словарей. FastAPI сам преобразует его в JSON, а на фронте мы преобразуем JSON в массив объектов JavaScript .
Please open Telegram to view this post
VIEW IN TELEGRAM
❤2🔥2
#PXI_WEB #PXI_PROJECT
🙂 Всем привет! Сегодня произошло пополнение в проектах. Я сделал первую версию сайта, в котором содержаться все ссылки на другие проекты, социальные сети и репозиторий с кодом (скоро добавлю все исходники в репозитории на Хабре). В дальнейшем планирую дублировать туда посты и статьи, добавить комментарии для общения между друг другом.
🖥 Попробуйте сейчас, думаю вам понравится! Также пишите свои идеи, что вы хотите там видеть
http://132.243.17.128/
http://132.243.17.128/
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2❤1
#PXI_WEB #PXI_PROJECT
🙂 Всем привет! Сегодня у меня для вас две новости. Во-первых, я наконец-то выкатил обновление мессенджера, в котором адаптировал фронтенд под мобильные устройства. Во-вторых, несколько людей просило, и я наконец-то создал репозитории в github. Подробнее про обновление или репозитории можете посмотреть на сайте!
Please open Telegram to view this post
VIEW IN TELEGRAM
GitHub
geses-off - Overview
Создатель проекта PXI. geses-off has 2 repositories available. Follow their code on GitHub.
🔥3
#PXI_WEB
🙂 Всем привет! Продолжаю рассказывать про обновления в мессенджере. На этот раз расскажу про то, как я реализовал удаление чатов и получение групп и каналов, для отображения во вкладках.
⚡️ Отображаем группы и каналы, в которых мы состоим
В прошлый раз мы с вами реализовали отправку сообщений в группах и каналах, получение сообщений, создание и поиск групп и каналов. В текущей реализации мы становимся участником после того, как отправим первое сообщение. Но до сих пор не реализовали отображение групп и каналов, в которых мы являемся участниками.
Здесь мы в зависимости от вкладки получаем разное значение в строке type от фронтенда, потом используем нашу функцию get_current_user и Depends (чтобы даже если нам отошлют имя в quary параметрах вызвалась наша функция и взялось наше значение), которая возвращает имя пользователя из токена (чтобы никто не мог представиться другим).
Приводим строку type к значению enum (models.ChatType(type), если type равен direct, то получим ChatType.DIRECT), иначе база данных не поймет, ведь в ней лежит именно объект enum.
Далее делаем select, выбирая все chat_id и name чатов нужного типа, в которых мы состоим. Вызываем метод .all() так как результат простые объекты кортежи, а не словари или списки. Возвращаем список словарей, FastAPI сам переведёт его в JSON.
💥 Удаление личных чатов
Мы уже реализовали основу для групп и каналов (они сейчас правда работает как группы), перейдём теперь к другим важным функциям. Реализуем удаление личных чатов, сейчас я решил реализовать так, что удаляется сразу у обоих участников.
Вначале мы ищем chat_id по именам, присоединяя таблицу самих чатов (приписываем поля из Chats), чтобы потом отсечь группы, каналы и тд. Метод scalar_one_or_none() возвращает либо один объект, либо None.
Если чата нет возвращаем 404 ошибку, если есть, то удаляем сначала сообщения этого чата, потом участников, потом сам чат (как в зависимостях в базе данных). Здесь execute заставляет базу выполнить запрос, но пока не завершать транзакцию, после всех удалений мы завершим общую транзакцию.
💬 На этом пока что всё, задавайте интересующие вас вопросы!
В прошлый раз мы с вами реализовали отправку сообщений в группах и каналах, получение сообщений, создание и поиск групп и каналов. В текущей реализации мы становимся участником после того, как отправим первое сообщение. Но до сих пор не реализовали отображение групп и каналов, в которых мы являемся участниками.
@router.get("/get_chats")
async def get_chats(type: str, db: SessionDep, username: str = Depends(auth.get_current_user)):
try:
chat_type_enum = models.ChatType(type)
except ValueError:
return []
query = (
select(models.Chats.id, models.Chats.name)
.join(models.ChatMembers, models.ChatMembers.chat_id == models.Chats.id)
.where(and_(
models.Chats.type == chat_type_enum,
models.ChatMembers.username == username
))
)
result = await db.execute(query)
rows = result.all()
return [{"id": row[0], "name": row[1]} for row in rows]Здесь мы в зависимости от вкладки получаем разное значение в строке type от фронтенда, потом используем нашу функцию get_current_user и Depends (чтобы даже если нам отошлют имя в quary параметрах вызвалась наша функция и взялось наше значение), которая возвращает имя пользователя из токена (чтобы никто не мог представиться другим).
Приводим строку type к значению enum (models.ChatType(type), если type равен direct, то получим ChatType.DIRECT), иначе база данных не поймет, ведь в ней лежит именно объект enum.
Далее делаем select, выбирая все chat_id и name чатов нужного типа, в которых мы состоим. Вызываем метод .all() так как результат простые объекты кортежи, а не словари или списки. Возвращаем список словарей, FastAPI сам переведёт его в JSON.
Мы уже реализовали основу для групп и каналов (они сейчас правда работает как группы), перейдём теперь к другим важным функциям. Реализуем удаление личных чатов, сейчас я решил реализовать так, что удаляется сразу у обоих участников.
@router.delete("/chat/direct/{interlocutor_name}", status_code=204)
async def delete_direct_chat(interlocutor_name: str, db: SessionDep, current_user: str = Depends(auth.get_current_user)):
stmt = (
select(models.ChatMembers.chat_id)
.join(models.Chats, models.Chats.id == models.ChatMembers.chat_id)
.where(models.Chats.type == models.ChatType.DIRECT)
.where(models.ChatMembers.username.in_([current_user, interlocutor_name]))
.group_by(models.ChatMembers.chat_id)
.having(func.count(models.ChatMembers.chat_id) == 2)
)
result = await db.execute(stmt)
chat_id = result.scalar_one_or_none()
if not chat_id:
raise HTTPException(status_code=404, detail="Переписка не найдена")
await db.execute(delete(models.Messages).where(models.Messages.chat_id == chat_id))
await db.execute(delete(models.ChatMembers).where(models.ChatMembers.chat_id == chat_id))
await db.execute(delete(models.Chats).where(models.Chats.id == chat_id))
await db.commit()
return NoneВначале мы ищем chat_id по именам, присоединяя таблицу самих чатов (приписываем поля из Chats), чтобы потом отсечь группы, каналы и тд. Метод scalar_one_or_none() возвращает либо один объект, либо None.
Если чата нет возвращаем 404 ошибку, если есть, то удаляем сначала сообщения этого чата, потом участников, потом сам чат (как в зависимостях в базе данных). Здесь execute заставляет базу выполнить запрос, но пока не завершать транзакцию, после всех удалений мы завершим общую транзакцию.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2
#PXI_WEB #PXI_PROJECT
🙂 Всем привет! Хочу сказать вам, что вышла новая версия мессенджера v2.4.0. В ней добавлены новые смайлики, свайпы (не везде работает плавно, исправлю позже), возможность редактирования профиля, исправлены некоторые баги и много чего ещё. Так что заходите, пользуйтесь.
⚠️ В этом и других постах я рассказываю о нововведениях, когда обновляется минорная или мажорная версия, а если вы хотите узнавать о нововведениях даже в патч версиях, то смотрите наш сайт.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥3
#PXI_WEB
🙂 Вновь всех приветствую! И вновь пост про нововведения в нашем мессенджере. Не беспокойтесь, следующий пост будет уже о кое-чём более интересном. А сегодня я расскажу, как добавил профили пользователей и возможность их редактирования.
⚡️ Ручка получения данных о профиле пользователя
На фронтенде я привязал клик на аватарку или имя пользователя к функции, которая дёргает эту ручку через fetch.
Здесь мы делаем select из таблицы Profiles, получая нужный нам профиль по имени, в не нахождения кидаем 404. Далее формируем словарь с нужными нам данными, last_seen это объект datatime, поэтому нужно самому превратить его в строку. Я использовал isoformat() и последующую ручную правку (хотя надо было просто использовать strftime).
В конце возвращаем наш словарь, а FastAPI сам его упакует в JSON или можем сами прописать JSONResponse(content = result).
☄️ Добавим создание профилей новым и старым пользователям
В нашу ручку аутентификации и авторизации добавим создание профилей.
✏️ Ручка редактирования профиля
Тут просто обновляем запись в таблице. Возвращаю 204, так как запрос выполнен, но данных не отправляю.
На фронтенде я привязал клик на аватарку или имя пользователя к функции, которая дёргает эту ручку через fetch.
@router.get("/get_profile/{username}",status_code=200)
async def get_profile(username:str, db: SessionDep):
stmt = select(models.Profiles).where(models.Profiles.username == username)
result = await db.execute(stmt)
result = result.scalar_one_or_none()
if not result : raise HTTPException(status_code=404, detail="Нет такого пользователя")
result = {
"display_name": result.display_name,
"username": result.username,
"avatar_url": "🤬",
"bio": result.bio,
"is_online": result.is_online,
"last_seen": result.last_seen.isoformat().replace("T"," ").split(".")[0] if result.last_seen else None
}
return resultЗдесь мы делаем select из таблицы Profiles, получая нужный нам профиль по имени, в не нахождения кидаем 404. Далее формируем словарь с нужными нам данными, last_seen это объект datatime, поэтому нужно самому превратить его в строку. Я использовал isoformat() и последующую ручную правку (хотя надо было просто использовать strftime).
В конце возвращаем наш словарь, а FastAPI сам его упакует в JSON или можем сами прописать JSONResponse(content = result).
В нашу ручку аутентификации и авторизации добавим создание профилей.
@router.post("")
async def auth(
response: Response,
database: SessionDep,
user_data: UserAuth
):
if not user_data.is_login:
query = select(models.Users).where(models.Users.username == user_data.username)
result = await database.execute(query)
existing_user = result.scalar_one_or_none()
if existing_user:
return {"error": "пользователь уже существует"}
user = models.Users(
username=user_data.username,
password=pwd_context.hash(user_data.password)
)
profile = models.Profiles(username=user_data.username,display_name=user_data.username, avatar_url="⭐", bio="Я новый пользователь!", is_online=True)
database.add(user)
await database.flush()
database.add(profile)
await database.commit()
return {"status": "успешно зарегистрированы"}
query = select(models.Users).where(models.Users.username == user_data.username)
result = await database.execute(query)
user = result.scalar_one_or_none()
if not user or not pwd_context.verify(user_data.password, user.password):
return {"error": "Неверный логин или пароль"}
prof_query = select(models.Profiles).where(models.Profiles.username == user_data.username)
prof_result = await database.execute(prof_query)
if not prof_result.scalar_one_or_none():
new_profile = models.Profiles(
username=user_data.username,
display_name=user_data.username,
avatar_url="⭐",
bio="Старый добрый пользователь",
is_online=True
)
database.add(new_profile)
await database.commit()
token = jwt.encode({"sub": user_data.username}, SECRET_KEY, algorithm=ALGORITHM)
response.set_cookie(key="access_token", value=token, httponly=True)
return {"status": "ok"}Тут просто обновляем запись в таблице. Возвращаю 204, так как запрос выполнен, но данных не отправляю.
@router.post("/edit_profile", status_code=204)
async def edit_profile(profile: models.EditProfile, db: SessionDep, username: str = Depends(auth.get_current_user)):
stmt = update(models.Profiles).where(models.Profiles.username == username).values(display_name=profile.display_name, bio=profile.bio)
await db.execute(stmt)
await db.commit()
return NonePlease open Telegram to view this post
VIEW IN TELEGRAM
❤1🔥1
#PXI_NET
🙂 Всем привет! В дополнение к прошлому посту решил сделать для вас шпаргалку с основными статус-кодами протокола HTTP/HTTPS. Для более подробного погружения в тему рекомендую прочитать RFC 9110, который регламентирует статус коды и весь протокол. В этот пост вместятся основные понятие и первые две группы статус кодов.
❓ Что такое status code или коды состояния ответа ?
Код состояния ответа — это трёхзначный целочисленный код, описывающий результат запроса и семантику ответа, включая информацию об успешности запроса и его содержимом (если таковое имеется). Все допустимые коды состояния находятся в диапазоне от 100 до 599 включительно.
Первая цифра кода состояния определяет класс ответа. Последние две цифры не имеют никакого отношения к классификации.
Первая цифра может принимать пять значений:
Важное уточнение, что коды состояния HTTP являются расширяемыми. Клиент не обязан понимать значение всех зарегистрированных кодов состояния, хотя такое понимание желательно.
Однако клиент ОБЯЗАН понимать класс любого кода состояния, определяемый первой цифрой, и рассматривать нераспознанный код состояния как эквивалентный коду состояния x00 этого класса.
Иногда используют статус коды вне диапазона 100-599 (например, 600-999), но они используются для внутренней связи не связанной с HTTP/HTTPS, например, ошибка конкретной библиотеки. Клиент такую ошибку может приравнивать к 5xx (Ошибка сервера).
Некоторые статус коды являются эвристически кэшируемыми (например, 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414 и 501), то есть браузер кэширует ответ с ними, даже если это не указано явно.
📎 Что такое URI, URL и URN?
В документации встречается аббревиатура URI, кратко разберём что это такое сразу.
⚡️ Наиболее используемые статус коды
1xx (Информационные)
2xx (Успешные)
Код состояния ответа — это трёхзначный целочисленный код, описывающий результат запроса и семантику ответа, включая информацию об успешности запроса и его содержимом (если таковое имеется). Все допустимые коды состояния находятся в диапазоне от 100 до 599 включительно.
Первая цифра кода состояния определяет класс ответа. Последние две цифры не имеют никакого отношения к классификации.
Первая цифра может принимать пять значений:
1️⃣ 1xx (Информационное сообщение) : Запрос получен, процесс продолжается2️⃣ 2xx (Успешно) : Запрос был успешно получен, понят и принят3️⃣ 3xx (Перенаправление) : Для завершения запроса необходимо предпринять дополнительные действия4️⃣ 4xx (Ошибка клиента) : Запрос содержит некорректный синтаксис или не может быть выполнен5️⃣ 5xx (Ошибка сервера) : Сервер не смог выполнить, по-видимому, действительный запрос
Важное уточнение, что коды состояния HTTP являются расширяемыми. Клиент не обязан понимать значение всех зарегистрированных кодов состояния, хотя такое понимание желательно.
Однако клиент ОБЯЗАН понимать класс любого кода состояния, определяемый первой цифрой, и рассматривать нераспознанный код состояния как эквивалентный коду состояния x00 этого класса.
Иногда используют статус коды вне диапазона 100-599 (например, 600-999), но они используются для внутренней связи не связанной с HTTP/HTTPS, например, ошибка конкретной библиотеки. Клиент такую ошибку может приравнивать к 5xx (Ошибка сервера).
Некоторые статус коды являются эвристически кэшируемыми (например, 200, 203, 204, 206, 300, 301, 308, 404, 405, 410, 414 и 501), то есть браузер кэширует ответ с ними, даже если это не указано явно.
В документации встречается аббревиатура URI, кратко разберём что это такое сразу.
URI (Uniform Resource Identifier) — это компактная последовательность символов, идентифицирующая абстрактный или физический ресурс. Он включает в себя URN и URL.
URL (Uniform Resource Locator) — это тип URI, который идентифицирует ресурс через описание способа его нахождения (протокол и тд).
URN (Uniform Resource Name) — это тип URI, который должен оставаться глобально уникальным и постоянным (имя ресурса, ISBN книг).
1xx (Информационные)
100 (Continue, продолжить) — сервер получил начальную часть запроса и ждёт недостающих данных, чтобы дать окончательный ответ клиенту.
101 (Switching Protocols, переключение протокола) — сервер понимает и готов выполнить запрос клиента, поступающий через поле заголовка Upgrade, об изменении используемого протокола приложения в данном соединении (так происходит переход с HTTP на websocket).
2xx (Успешные)
200 (Ok, успех) — запрос выполнен успешно, обычно есть содержимое в ответе.
201 (Created, создано) — запрос выполнен и в результате создан один или несколько новых ресурсов, содержимое ответа обычно содержит этот ресурс.
202 (Accepted, принято) — запрос принят к обработке, но обработка еще не завершена.
203 (Non-authoritative information, неавторитетная информация) — запрос был успешным, но содержимое было изменено по сравнению с ответом исходного сервера 200 (OK) преобразующим прокси-сервером.
204 (No content, нет содержимого) — сервер успешно выполнил запрос и в ответе нет дополнительного содержимого для отправки.
205 (Reset content, сброс содержимого) — сервер выполнил запрос и желает, чтобы пользовательский агент сбросил «представление документа», которое вызвало отправку запроса, до его исходного состояния, полученного от исходного сервера.
206 (Partial content, частичное содержание) — сервер успешно выполняет запрос диапазона для целевого ресурса, передавая одну или несколько частей выбранного представления.
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2
#PXI_NET
🙂 Всем привет! Этот пост является продолжением предыдущего, снова направляю интересующихся почитать RFC 9110. А для тех, кто хочет краткую выжимку, читайте этот пост.
⚡️ Наиболее используемые статус коды
3xx (Перенаправление)
4xx (Ошибка клиента)
5xx (Ошибка сервера)
3xx (Перенаправление)
300 (Multiple choices, множественный выбор) — указывает на то, что целевой ресурс имеет более одного представления, каждое со своим более конкретным идентификатором, и предоставляется информация об альтернативах, чтобы пользователь (или пользовательский агент) мог выбрать предпочтительное представление, перенаправив свой запрос на один или несколько из этих идентификаторов.
301 (Moved permanently, перемещено навсегда) — указывает на то, что целевому ресурсу присвоен новый постоянный URI, и любые будущие ссылки на этот ресурс должны использовать один из включенных в него URI.
302 (Found, найдено) — указывает на то, что целевой ресурс временно находится по другому URI. Поскольку перенаправление может периодически изменяться, клиенту следует продолжать использовать целевой URI для будущих запросов.
303 (See Other, смотри другое) — указывает на то, что сервер перенаправляет пользовательский агент на другой ресурс, как указано в URI в поле заголовка Location , который предназначен для предоставления косвенного ответа на исходный запрос. Пользовательский агент может выполнить запрос на получение данных, нацеленный на этот URI (запрос GET или HEAD при использовании HTTP), который также может быть перенаправлен, и представить конечный результат в качестве ответа на исходный запрос. Обратите внимание, что новый URI в поле заголовка Location не считается эквивалентным целевому URI.
304 (Not Modified, не изменено) — указывает на то, что у нас и так актуальная версия ресурса и нет надобности сервера отправлять его снова.
307 (Temporary redirect, временное перенаправление) — указывает на то, что целевой ресурс временно находится под другим URI, и пользовательский агент НЕ ДОЛЖЕН изменять метод запроса, если он выполняет автоматическое перенаправление на этот URI.
308 (Permanent redirect, постоянное перенаправление) — указывает на то, что целевому ресурсу присвоен новый постоянный URI, и любые будущие ссылки на этот ресурс должны использовать один из включенных в него URI. Аналог 301, но с гарантией неизменности методов.
4xx (Ошибка клиента)
400 (Bad Request, неверный запрос) — указывает на то, что сервер не может или не будет обрабатывать запрос из-за ошибки, которая воспринимается как ошибка клиента. Например, если не подходит к pydantic схеме, то выбрасываем это.
401 (Unauthorized, не авторизован или несанкционированный доступ) — указывает на то, что запрос не был выполнен, поскольку отсутствуют действительные учётные данные для аутентификации целевого ресурса. Я кидаю её, если пользователь дёргает ручку без токена или с невалидным токеном.
403 (Forbidden, запрещено) — указывает на то, что сервер понял запрос, но отказался его выполнить. Иногда его используют для неявных блокировках пользователей, а я его кидаю, когда пользователь пытается редактировать не свой профиль и тд.
404 (Not Found, не найдено) — указывает на то, что исходный сервер не обнаружил актуального представления целевого ресурса или не желает сообщать о его существовании.
405 (Method not allowed, метод не разрешен) — указывает на то, что метод, полученный в строке запроса, известен исходному серверу, но не применим к этому ресурсу.
408 (Request Time-out, тайм-аут запроса) — указывает на то, что сервер не получил полный запрос в течение времени, отведенного на ожидание
409 (Conflict, конфликт) — указывает на то, что запрос не может быть выполнен из-за конфликта с текущим состоянием целевого ресурса. Этот код используется в ситуациях, когда пользователь может разрешить конфликт и повторно отправить запрос. Я его кидаю, когда пользователь с таким именем уже есть.
5xx (Ошибка сервера)
500 (Внутренняя ошибка сервера) — исключительная ситуация на сервере.
503 (Сервис недоступен) — указывает на то, что сервер в данный момент не может обработать запрос из-за временной перегрузки или планового технического обслуживания
Please open Telegram to view this post
VIEW IN TELEGRAM
❤1🔥1
#PXI_LEARN #PXI_WEB
🙂 Всем привет! Сегодня будет совсем небольшой пост. Я хочу поделиться с вами тем, как удобно можно перенести все посты из телеграмм канала. Мне это нужно было для того, чтобы перенести все мои посты на мой сайт.
❓ Как получить все посты из канала?
Существует множество различных способов. Можно использовать официальный способ (который я опишу ниже), можно использовать различных ТГ ботов (например, @ToCsvBot даст вам все посты в csv формате) и можно использовать виджеты.
Виджеты в моём случае сразу нет, хоть они и предоставляет красивое оформление как в тг, сохранение реакций и тд, но главная проблема их в том, что они подтягиваются через фронтенд (следовательно браузер пользователя). А телеграм сейчас работает в РФ не очень, следовательно я потеряю этих пользователей или обязую из использовать VPN для сайта.
Я остановил свой выбор на официальном способе:
⚡️ Бэкенд сайта
Вот весь бэк моего сайта. Тут как обычно FastAPI, шаблонизатор Jinja2.
load_tg_posts парсит и сохраняет посты вместе с тегами для красивого отображения.
Существует множество различных способов. Можно использовать официальный способ (который я опишу ниже), можно использовать различных ТГ ботов (например, @ToCsvBot даст вам все посты в csv формате) и можно использовать виджеты.
Виджеты в моём случае сразу нет, хоть они и предоставляет красивое оформление как в тг, сохранение реакций и тд, но главная проблема их в том, что они подтягиваются через фронтенд (следовательно браузер пользователя). А телеграм сейчас работает в РФ не очень, следовательно я потеряю этих пользователей или обязую из использовать VPN для сайта.
Я остановил свой выбор на официальном способе:
Вуаля, мы получили все наши посты (а также то, что вы выбрали дополнительно), если вы выбрали JSON, то мы получили собственно JSON. Ну или словарь, в нем нам нужно значение под ключом messages, это список словарей (наших постов).1️⃣ Зайдите в интересующий вас открытый канал2️⃣ Нажмите на три точки в углу3️⃣ Нажмите на экспорт чата4️⃣ Выберете в каком формате и что именно хотите экспортировать, доступные форматы: HTML или JSON (JSON будет удобнее в моём случае)
Вот весь бэк моего сайта. Тут как обычно FastAPI, шаблонизатор Jinja2.
load_tg_posts парсит и сохраняет посты вместе с тегами для красивого отображения.
from fastapi import FastAPI, Request, Depends, Form, status, HTTPException, Cookie, Response, UploadFile, File
from fastapi.templating import Jinja2Templates
from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse
import models
import json
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent
DATA_DIR = BASE_DIR.parent / "data"
app = FastAPI()
templates = Jinja2Templates(directory="templates")
app.mount("/static", StaticFiles(directory="static"), name="static")
posts = []
@app.on_event("startup")
async def on_startup():
global posts
posts = load_tg_posts("./posts/result.json")
@app.get("/")
async def home(request: Request):
return templates.TemplateResponse("site.html",{"request":request, "posts": posts})
def load_tg_posts(path_to_post):
with open(path_to_post, "r", encoding="utf-8") as f:
data = json.load(f)
messages = data.get('messages', [])
processed_posts = []
for msg in messages:
if msg.get('type') == 'message' and msg.get('text'):
full_html = ""
raw_text = msg.get('text')
if isinstance(raw_text, list):
for part in raw_text:
if isinstance(part, dict):
p_type = part.get('type')
p_text = part.get('text', '')
if p_type == 'hashtag':
full_html += f'<span class="hashtag" style="color: #3390ec; font-weight: bold;">{p_text}</span>'
elif p_type == 'bold':
full_html += f'<b>{p_text}</b>'
elif p_type == 'pre':
full_html += f'<pre><code>{p_text}</code></pre>'
elif p_type == 'code':
full_html += f'<code>{p_text}</code>'
elif p_type == 'text_link':
full_html += f'<a href="{part.get("href")}" target="_blank" style="color: #64b5f6;">{p_text}</a>'
else:
full_html += p_text
else:
full_html += str(part)
else:
full_html = str(raw_text)
full_html = full_html.replace('\n', '<br>')
raw_date = msg.get('date').replace('T', ' ')
clean_date = raw_date[:16]
processed_posts.append({"date": clean_date, "text": full_html})
return processed_posts[::-1]
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥2