├── __init__.py
├── data
├── __init__.py
└── config.py
├── keyboards
├── __init__.py
├── reply
│ └── __init__.py
└── inline
│ ├── __init__.py
│ └── buttons.py
├── utils
├── db
│ ├── __init__.py
│ └── postgres.py
├── misc
│ ├── __init__.py
│ └── logging.py
├── api
│ ├── __init__.py
│ ├── backend.py
│ └── base.py
├── __init__.py
├── shortcuts.py
├── set_bot_commands.py
├── notify_admins.py
└── pgtoexcel.py
├── handlers
├── errors
│ ├── __init__.py
│ └── error_handler.py
├── groups
│ └── __init__.py
├── users
│ ├── __init__.py
│ ├── echo.py
│ ├── help.py
│ ├── start.py
│ └── admin.py
├── channels
│ └── __init__.py
└── __init__.py
├── states
├── __init__.py
└── test.py
├── middlewares
├── __init__.py
└── throttling.py
├── schemas
├── __init__.py
├── language.py
└── user.py
├── requirements.txt
├── filters
├── __init__.py
├── chat_type.py
└── admin.py
├── .env.example
├── loader.py
├── locales
├── uz
│ └── LC_MESSAGES
│ │ └── messages.po
├── en
│ └── LC_MESSAGES
│ │ └── messages.po
└── ru
│ └── LC_MESSAGES
│ └── messages.po
├── README.md
├── app.py
└── .gitignore
/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/data/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/keyboards/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/utils/db/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/handlers/errors/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/handlers/groups/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/handlers/users/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/keyboards/reply/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/handlers/channels/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/utils/misc/__init__.py:
--------------------------------------------------------------------------------
1 | from . import logging
2 |
--------------------------------------------------------------------------------
/keyboards/inline/__init__.py:
--------------------------------------------------------------------------------
1 | from . import buttons
--------------------------------------------------------------------------------
/states/__init__.py:
--------------------------------------------------------------------------------
1 | from .test import Test, AdminState
2 |
--------------------------------------------------------------------------------
/middlewares/__init__.py:
--------------------------------------------------------------------------------
1 | from .throttling import ThrottlingMiddleware
2 |
--------------------------------------------------------------------------------
/utils/api/__init__.py:
--------------------------------------------------------------------------------
1 | from .backend import bot_api_client # noqa
2 |
--------------------------------------------------------------------------------
/schemas/__init__.py:
--------------------------------------------------------------------------------
1 | from . import language # noqa
2 | from . import user # noqa
3 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xalq-mazza-qilsin/Aiogram3-Template/HEAD/requirements.txt
--------------------------------------------------------------------------------
/filters/__init__.py:
--------------------------------------------------------------------------------
1 | from .chat_type import ChatTypeFilter # noqa
2 | from .admin import IsBotAdminFilter # noqa
3 |
--------------------------------------------------------------------------------
/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from .notify_admins import on_startup_notify
2 | from .set_bot_commands import set_default_commands
3 | from .misc import logging
4 |
5 |
--------------------------------------------------------------------------------
/schemas/language.py:
--------------------------------------------------------------------------------
1 | from enum import StrEnum
2 |
3 |
4 | class LanguageEnum(StrEnum):
5 | EN = "en"
6 | UZ = "uz"
7 | RU = "ru"
8 | UNKNOWN = "unknown"
9 |
--------------------------------------------------------------------------------
/handlers/users/echo.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router, types
2 |
3 | router = Router()
4 |
5 |
6 | @router.message()
7 | async def start_user(message: types.Message):
8 | await message.answer(message.text)
9 |
--------------------------------------------------------------------------------
/states/test.py:
--------------------------------------------------------------------------------
1 | from aiogram.filters.state import StatesGroup, State
2 |
3 |
4 | class Test(StatesGroup):
5 | Q1 = State()
6 | Q2 = State()
7 |
8 |
9 | class AdminState(StatesGroup):
10 | are_you_sure = State()
11 | ask_ad_content = State()
12 |
--------------------------------------------------------------------------------
/utils/misc/logging.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | logging.basicConfig(format=u'%(filename)s [LINE:%(lineno)d] #%(levelname)-8s [%(asctime)s] %(message)s',
4 | level=logging.INFO,
5 | # level=logging.DEBUG,
6 | )
7 |
--------------------------------------------------------------------------------
/schemas/user.py:
--------------------------------------------------------------------------------
1 | from pydantic import BaseModel
2 |
3 | from .language import LanguageEnum
4 |
5 |
6 | class User(BaseModel):
7 | telegram_id: int
8 | username: str | None = None
9 | full_name: str | None = None
10 | language: LanguageEnum = LanguageEnum.EN
11 |
--------------------------------------------------------------------------------
/keyboards/inline/buttons.py:
--------------------------------------------------------------------------------
1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
2 |
3 |
4 | inline_keyboard = [[
5 | InlineKeyboardButton(text="✅ Yes", callback_data='yes'),
6 | InlineKeyboardButton(text="❌ No", callback_data='no')
7 | ]]
8 | are_you_sure_markup = InlineKeyboardMarkup(inline_keyboard=inline_keyboard)
9 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # YANGI .env FAYL YARATING VA
2 | # QUYIDAGI MA'LUMOTLARNI YOZING:
3 |
4 | ADMINS=12345678,12345677,12345676
5 | BOT_TOKEN=123452345243:ABshDlsFIjklL
6 |
7 |
8 | # PostgreSQL
9 | DB_USER=postgres
10 | DB_PASS=1234567
11 | DB_NAME=telegram_bot
12 | DB_HOST=localhost
13 | DB_PORT=5432
14 |
15 | BACKEND_HOST=http://127.0.0.1:8000
16 |
--------------------------------------------------------------------------------
/utils/api/backend.py:
--------------------------------------------------------------------------------
1 | from .base import BaseAPIClient
2 | from data.config import BACKEND_HOST
3 |
4 |
5 | class BotAPIClient(BaseAPIClient):
6 | def __init__(self):
7 | super().__init__()
8 | self.api_base_url = f"{BACKEND_HOST.rstrip('/')}/api/v1"
9 | self.bot_base_url = self.api_base_url + "/bot"
10 |
11 |
12 | bot_api_client = BotAPIClient()
13 |
--------------------------------------------------------------------------------
/handlers/users/help.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router, types
2 | from aiogram.filters.command import Command
3 |
4 | router = Router()
5 |
6 |
7 | @router.message(Command('help'))
8 | async def bot_help(message: types.Message):
9 | text = ("Buyruqlar: ",
10 | "/start - Botni ishga tushirish",
11 | "/help - Yordam")
12 | await message.answer(text="\n".join(text))
13 |
--------------------------------------------------------------------------------
/filters/chat_type.py:
--------------------------------------------------------------------------------
1 | from aiogram.enums import ChatType
2 | from aiogram.filters import BaseFilter
3 | from aiogram.types import Message
4 |
5 |
6 | class ChatTypeFilter(BaseFilter):
7 | def __init__(self, chat_types: list[ChatType]):
8 | self.chat_types = chat_types
9 |
10 | async def __call__(self, message: Message) -> bool:
11 | return message.chat.type in self.chat_types
12 |
--------------------------------------------------------------------------------
/filters/admin.py:
--------------------------------------------------------------------------------
1 | from aiogram.filters import BaseFilter
2 | from aiogram.types import Message
3 |
4 |
5 | class IsBotAdminFilter(BaseFilter):
6 | def __init__(self, user_ids: list):
7 | self.user_ids = user_ids
8 |
9 | async def __call__(self, message: Message) -> bool:
10 | admin_ids_int = [int(id) for id in self.user_ids]
11 | return int(message.from_user.id) in admin_ids_int
12 |
--------------------------------------------------------------------------------
/utils/api/base.py:
--------------------------------------------------------------------------------
1 | import aiohttp
2 |
3 |
4 | class BaseAPIClient:
5 | def __init__(self):
6 | self.session = aiohttp.ClientSession()
7 |
8 | async def _send_request(
9 | self,
10 | method,
11 | url,
12 | json=None,
13 | headers=None,
14 | ):
15 |
16 | response = await self.session.request(
17 | method=method, url=url, json=json, headers=headers
18 | )
19 | return await response.json()
20 |
--------------------------------------------------------------------------------
/utils/shortcuts.py:
--------------------------------------------------------------------------------
1 | MARKDOWN_ESCAPE_CHARS = ['_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!']
2 |
3 |
4 | def safe_markdown(_string):
5 | _string = str(_string)
6 | text = ""
7 | for letter in _string:
8 | if letter in MARKDOWN_ESCAPE_CHARS:
9 | text += '\{}'.format(letter)
10 | else:
11 | text += letter
12 | return text
13 |
--------------------------------------------------------------------------------
/utils/set_bot_commands.py:
--------------------------------------------------------------------------------
1 | from aiogram import Bot
2 | from aiogram.methods.set_my_commands import BotCommand
3 | from aiogram.types import BotCommandScopeAllPrivateChats
4 |
5 |
6 | async def set_default_commands(bot: Bot):
7 | commands = [
8 | BotCommand(command="/start", description="Botni ishga tushirish"),
9 | BotCommand(command="/help", description="Yordam"),
10 | ]
11 | await bot.set_my_commands(commands=commands, scope=BotCommandScopeAllPrivateChats())
12 |
--------------------------------------------------------------------------------
/data/config.py:
--------------------------------------------------------------------------------
1 | from environs import Env
2 |
3 | # environs kutubxonasidan foydalanish
4 | env = Env()
5 | env.read_env()
6 |
7 | # .env fayl ichidan quyidagilarni o'qiymiz
8 | BOT_TOKEN = env.str("BOT_TOKEN") # Bot Token
9 | ADMINS = env.list("ADMINS") # adminlar ro'yxati
10 |
11 |
12 | DB_USER = env.str("DB_USER")
13 | DB_PASS = env.str("DB_PASS")
14 | DB_NAME = env.str("DB_NAME")
15 | DB_HOST = env.str("DB_HOST")
16 | DB_PORT = env.str("DB_PORT")
17 |
18 | BACKEND_HOST = env.str("BACKEND_HOST", "http://localhost:8000")
19 |
--------------------------------------------------------------------------------
/handlers/__init__.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router
2 | from aiogram.enums import ChatType
3 |
4 | from filters import ChatTypeFilter
5 |
6 |
7 | def setup_routers() -> Router:
8 | from .users import admin, start, help, echo
9 | from .errors import error_handler
10 |
11 | router = Router()
12 |
13 | # Agar kerak bo'lsa, o'z filteringizni o'rnating
14 | start.router.message.filter(ChatTypeFilter(chat_types=[ChatType.PRIVATE]))
15 |
16 | router.include_routers(admin.router, start.router, help.router, echo.router, error_handler.router)
17 |
18 | return router
19 |
--------------------------------------------------------------------------------
/utils/notify_admins.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from aiogram import Bot
4 |
5 | from data.config import ADMINS
6 |
7 |
8 | async def on_startup_notify(bot: Bot):
9 | for admin in ADMINS:
10 | try:
11 | bot_properties = await bot.me()
12 | message = ["Bot ishga tushdi.\n",
13 | f"Bot ID: {bot_properties.id}",
14 | f"Bot Username: {bot_properties.username}"]
15 | await bot.send_message(int(admin), "\n".join(message))
16 | except Exception as err:
17 | logging.exception(err)
18 |
--------------------------------------------------------------------------------
/loader.py:
--------------------------------------------------------------------------------
1 | from aiogram import Bot, Dispatcher
2 | from aiogram.client.default import DefaultBotProperties
3 | from aiogram.enums.parse_mode import ParseMode
4 | from aiogram.utils.i18n import I18n, FSMI18nMiddleware
5 | from aiogram.fsm.storage.memory import MemoryStorage
6 |
7 | from utils.db.postgres import Database
8 | from data.config import BOT_TOKEN
9 |
10 |
11 | db = Database()
12 | bot = Bot(token=BOT_TOKEN, default=DefaultBotProperties(parse_mode=ParseMode.HTML))
13 |
14 | i18n = I18n(path="./locales", domain="messages")
15 | I18n.set_current(i18n)
16 | storage = MemoryStorage()
17 | dispatcher = Dispatcher(storage=storage)
18 | i18n_middleware = FSMI18nMiddleware(i18n=i18n)
19 | i18n_middleware.setup(dispatcher)
20 |
--------------------------------------------------------------------------------
/locales/uz/LC_MESSAGES/messages.po:
--------------------------------------------------------------------------------
1 | # Uzbek translations for PROJECT.
2 | # Copyright (C) 2024 ORGANIZATION
3 | # This file is distributed under the same license as the PROJECT project.
4 | # FIRST AUTHOR , 2024.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: PROJECT VERSION\n"
9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10 | "POT-Creation-Date: 2024-10-26 21:39+0500\n"
11 | "PO-Revision-Date: 2024-10-26 21:39+0500\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language: uz\n"
14 | "Language-Team: uz \n"
15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.9.1\n"
20 |
21 |
--------------------------------------------------------------------------------
/locales/en/LC_MESSAGES/messages.po:
--------------------------------------------------------------------------------
1 | # English translations for PROJECT.
2 | # Copyright (C) 2024 ORGANIZATION
3 | # This file is distributed under the same license as the PROJECT project.
4 | # FIRST AUTHOR , 2024.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: PROJECT VERSION\n"
9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10 | "POT-Creation-Date: 2024-10-26 21:39+0500\n"
11 | "PO-Revision-Date: 2024-10-26 21:39+0500\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language: en\n"
14 | "Language-Team: en \n"
15 | "Plural-Forms: nplurals=2; plural=(n != 1)\n"
16 | "MIME-Version: 1.0\n"
17 | "Content-Type: text/plain; charset=utf-8\n"
18 | "Content-Transfer-Encoding: 8bit\n"
19 | "Generated-By: Babel 2.9.1\n"
20 |
21 |
--------------------------------------------------------------------------------
/locales/ru/LC_MESSAGES/messages.po:
--------------------------------------------------------------------------------
1 | # Russian translations for PROJECT.
2 | # Copyright (C) 2024 ORGANIZATION
3 | # This file is distributed under the same license as the PROJECT project.
4 | # FIRST AUTHOR , 2024.
5 | #
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: PROJECT VERSION\n"
9 | "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10 | "POT-Creation-Date: 2024-10-26 21:39+0500\n"
11 | "PO-Revision-Date: 2024-10-26 21:39+0500\n"
12 | "Last-Translator: FULL NAME \n"
13 | "Language: ru\n"
14 | "Language-Team: ru \n"
15 | "Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
16 | "n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
17 | "MIME-Version: 1.0\n"
18 | "Content-Type: text/plain; charset=utf-8\n"
19 | "Content-Transfer-Encoding: 8bit\n"
20 | "Generated-By: Babel 2.9.1\n"
21 |
22 |
--------------------------------------------------------------------------------
/middlewares/throttling.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from aiogram.dispatcher.middlewares.base import BaseMiddleware
4 | from aiogram.types import Message
5 |
6 |
7 | class ThrottlingMiddleware(BaseMiddleware):
8 | def __init__(self, slow_mode_delay=0.5):
9 | self.user_timeouts = {}
10 | self.slow_mode_delay = slow_mode_delay
11 | super(ThrottlingMiddleware, self).__init__()
12 |
13 | async def __call__(self, handler, event: Message, data):
14 | user_id = event.from_user.id
15 | current_time = time.time()
16 |
17 | # Ushbu foydalanuvchining so'nggi so'rovi bo'yicha yozuv mavjudligini tekshirish
18 | last_request_time = self.user_timeouts.get(user_id, 0)
19 | if current_time - last_request_time < self.slow_mode_delay:
20 | # Agar so'rovlar juda tez-tez bo'lsa, sekin rejimni yoqish
21 | await event.reply("Juda ko'p so'rov! Biroz kuting.")
22 | return
23 |
24 | else:
25 | # Oxirgi so'rovning vaqtini yangilash
26 | self.user_timeouts[user_id] = current_time
27 | # Event ni handlerga o'tkazish
28 | return await handler(event, data)
29 |
--------------------------------------------------------------------------------
/utils/pgtoexcel.py:
--------------------------------------------------------------------------------
1 | import openpyxl
2 | from openpyxl.styles import Font
3 |
4 |
5 | async def export_to_excel(data, headings, filepath):
6 | """
7 | Exports data from PostgreSQL to an Excel spreadsheet using psycopg2.
8 |
9 | Arguments:
10 | connection - an open psycopg2 (this function does not close the connection)
11 | query_string - SQL to get data
12 | headings - list of strings to use as column headings
13 | filepath - path and filename of the Excel file
14 |
15 | psycopg2 and file handling errors bubble up to calling code.
16 | """
17 |
18 | wb = openpyxl.Workbook()
19 | sheet = wb.active
20 |
21 | sheet.row_dimensions[1].font = Font(bold=True)
22 |
23 | # Spreadsheet row and column indexes start at 1,
24 | # so we use "start = 1" in enumerate, so
25 | # we don't need to add 1 to the indexes.
26 | for colno, heading in enumerate(headings, start=1):
27 | sheet.cell(row=1, column=colno).value = heading
28 |
29 | # This time we use "start = 2" to skip the heading row.
30 | for rowno, row in enumerate(data, start=2):
31 | for colno, cell_value in enumerate(row, start=1):
32 | sheet.cell(row=rowno, column=colno).value = cell_value
33 |
34 | wb.save(filepath)
35 |
--------------------------------------------------------------------------------
/handlers/users/start.py:
--------------------------------------------------------------------------------
1 | from aiogram import Router, types
2 | from aiogram.filters import CommandStart
3 | from aiogram.enums.parse_mode import ParseMode
4 | from aiogram.client.session.middlewares.request_logging import logger
5 | from loader import db, bot
6 | from data.config import ADMINS
7 | from utils.shortcuts import safe_markdown
8 |
9 | router = Router()
10 |
11 |
12 | @router.message(CommandStart())
13 | async def do_start(message: types.Message):
14 | """
15 | MARKDOWN V2 | HTML
16 | link: [Google](https://google.com/) | Google
17 | bold: *Qalin text* | Qalin text
18 | italic: _Yotiq shriftdagi text_ | Yotiq shriftdagi text
19 |
20 |
21 |
22 | ************** Note **************
23 | Markdownda _ * [ ] ( ) ~ ` > # + - = | { } . ! belgilari to'g'ridan to'g'ri ishlatilmaydi!!!
24 | Bu belgilarni ishlatish uchun oldidan \ qo'yish esdan chiqmasin. Masalan \. ko'rinishi . belgisini ishlatish uchun yozilgan.
25 | """
26 |
27 | telegram_id = message.from_user.id
28 | full_name = message.from_user.full_name
29 | username = message.from_user.username
30 | user = None
31 | try:
32 | user = await db.add_user(telegram_id=telegram_id, full_name=full_name, username=username)
33 | except Exception as error:
34 | logger.info(error)
35 | if user:
36 | count = await db.count_users()
37 | msg = (f"[{safe_markdown(user['full_name'])}](tg://user?id={user['telegram_id']}) bazaga qo'shildi\.\nBazada {count} ta foydalanuvchi bor\.")
38 | else:
39 | msg = f"[{safe_markdown(full_name)}](tg://user?id={telegram_id}) bazaga oldin qo'shilgan"
40 | for admin in ADMINS:
41 | try:
42 | await bot.send_message(
43 | chat_id=admin,
44 | text=msg,
45 | parse_mode=ParseMode.MARKDOWN_V2
46 | )
47 | except Exception as error:
48 | logger.info(f"Data did not send to admin: {admin}. Error: {error}")
49 | await message.answer(f"Assalomu alaykum {safe_markdown(full_name)}\!", parse_mode=ParseMode.MARKDOWN_V2)
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Aiogram New Template (aiogram 3)
2 |
3 | ### 1. Create virtual environment and install packages
4 | Windows
5 | ```shell
6 | python -m venv venv && .\venv\Scripts\activate && pip install -r requirements.txt
7 | ```
8 |
9 | Linux/Mac
10 | ```shell
11 | python3 -m venv venv && source venv/bin/activate && pip3 install -r requirements.txt
12 | ```
13 |
14 | ### 2. Create .env file and copy all variables from .env_example to it and customize your self (if needed)
15 |
16 | ### 3. Run app.py
17 | Windows
18 | ```shell
19 | python app.py
20 | ```
21 | Linux/Mac
22 | ```shell
23 | python3 app.py
24 | ```
25 |
26 | 3. Compile translations in locales dir with this command
27 | ```shell
28 | pybabel compile -d locales -D messages
29 | ```
30 |
31 | # Set up Postgresql on server
32 |
33 | ### 1. Install postgresql (if needed)
34 | ```shell
35 | sudo apt install -y postgresql postgresql-contrib
36 | ```
37 |
38 | ### 2. Log in to the postgresql shell
39 | ```shell
40 | sudo -u postres psql
41 | ```
42 |
43 | ### 3. Create a database (in postgresql shell)
44 | ```shell
45 | CREATE DATABASE database_name WITH template = template0 ENCODING 'UTF8' LC_CTYPE 'C' LC_COLLATE 'C';
46 | ```
47 |
48 | ### 4. Create a user (in postgresql shell)
49 | ```shell
50 | CREATE USER user_name WITH PASSWORD 'password';
51 | ```
52 |
53 | ### 5. Set encoding (in postgresql shell)
54 | ```shell
55 | ALTER ROLE user_name SET client_encoding TO 'utf8';
56 | ```
57 |
58 | ### 6. Restrict transactions from an unexpected db user (in postgresql shell)
59 | ```shell
60 | ALTER ROLE user_name SET default_transaction_isolation TO 'read committed';
61 | ```
62 |
63 | ### 7. Set timezone (in postgresql shell)
64 | ```shell
65 | ALTER ROLE user_name SET timezone TO 'UTC';
66 | ```
67 | > **_Note:_** If you use another timezone in your project, replace **'UTC'** with yours.
68 |
69 | ### 8. Grant the user the right to manage the db (in postgresql shell)
70 | ```shell
71 | GRANT ALL PRIVILEGES ON DATABASE database_name TO user_name;
72 | ```
73 |
74 | ### 9. Quit postgresql (in postgresql shell)
75 | ```shell
76 | \q
77 | ```
78 |
79 | ## If you have questions for this project, join and ask our community: https://t.me/+Wu3loL2thM8yZDMy
80 |
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/handlers/users/admin.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import asyncio
3 | from aiogram import Router, types
4 | from aiogram.filters import Command
5 | from aiogram.fsm.context import FSMContext
6 | from loader import db, bot
7 | from keyboards.inline.buttons import are_you_sure_markup
8 | from states.test import AdminState
9 | from filters.admin import IsBotAdminFilter
10 | from data.config import ADMINS
11 | from utils.pgtoexcel import export_to_excel
12 |
13 | router = Router()
14 |
15 |
16 | @router.message(Command('allusers'), IsBotAdminFilter(ADMINS))
17 | async def get_all_users(message: types.Message):
18 | users = await db.select_all_users()
19 |
20 | file_path = f"data/users_list.xlsx"
21 | await export_to_excel(data=users, headings=['ID', 'Full Name', 'Username', 'Telegram ID'], filepath=file_path)
22 |
23 | await message.answer_document(types.input_file.FSInputFile(file_path))
24 |
25 |
26 | @router.message(Command('reklama'), IsBotAdminFilter(ADMINS))
27 | async def ask_ad_content(message: types.Message, state: FSMContext):
28 | await message.answer("Reklama uchun post yuboring")
29 | await state.set_state(AdminState.ask_ad_content)
30 |
31 |
32 | @router.message(AdminState.ask_ad_content, IsBotAdminFilter(ADMINS))
33 | async def send_ad_to_users(message: types.Message, state: FSMContext):
34 | users = await db.select_all_users()
35 | count = 0
36 | for user in users:
37 | user_id = user[-1]
38 | try:
39 | await message.send_copy(chat_id=user_id)
40 | count += 1
41 | await asyncio.sleep(0.05)
42 | except Exception as error:
43 | logging.info(f"Ad did not send to user: {user_id}. Error: {error}")
44 | await message.answer(text=f"Reklama {count} ta foydalauvchiga muvaffaqiyatli yuborildi.")
45 | await state.clear()
46 |
47 |
48 | @router.message(Command('cleandb'), IsBotAdminFilter(ADMINS))
49 | async def ask_are_you_sure(message: types.Message, state: FSMContext):
50 | msg = await message.reply("Haqiqatdan ham bazani tozalab yubormoqchimisiz?", reply_markup=are_you_sure_markup)
51 | await state.update_data(msg_id=msg.message_id)
52 | await state.set_state(AdminState.are_you_sure)
53 |
54 |
55 | @router.callback_query(AdminState.are_you_sure, IsBotAdminFilter(ADMINS))
56 | async def clean_db(call: types.CallbackQuery, state: FSMContext):
57 | data = await state.get_data()
58 | msg_id = data.get('msg_id')
59 | if call.data == 'yes':
60 | await db.delete_users()
61 | text = "Baza tozalandi!"
62 | elif call.data == 'no':
63 | text = "Bekor qilindi."
64 | await bot.edit_message_text(text=text, chat_id=call.message.chat.id, message_id=msg_id)
65 | await state.clear()
66 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | from aiogram import Bot, Dispatcher
4 | from aiogram.client.session.middlewares.request_logging import logger
5 | from aiogram.enums import ChatType
6 | from loader import db
7 |
8 |
9 | def setup_handlers(dispatcher: Dispatcher) -> None:
10 | """HANDLERS"""
11 | from handlers import setup_routers
12 |
13 | dispatcher.include_router(setup_routers())
14 |
15 |
16 | def setup_middlewares(dispatcher: Dispatcher, bot: Bot) -> None:
17 | """MIDDLEWARE"""
18 | from middlewares.throttling import ThrottlingMiddleware
19 |
20 | # Spamdan himoya qilish uchun klassik ichki o'rta dastur. So'rovlar orasidagi asosiy vaqtlar 0,5 soniya
21 | dispatcher.message.middleware(ThrottlingMiddleware(slow_mode_delay=0.5))
22 |
23 |
24 | def setup_filters(dispatcher: Dispatcher) -> None:
25 | """FILTERS"""
26 | from filters import ChatTypeFilter
27 |
28 | # Chat turini aniqlash uchun klassik umumiy filtr
29 | # Filtrni handlers/users/__init__ -dagi har bir routerga alohida o'rnatish mumkin
30 | dispatcher.message.filter(ChatTypeFilter(chat_types=[ChatType.PRIVATE]))
31 |
32 |
33 | async def setup_aiogram(dispatcher: Dispatcher, bot: Bot) -> None:
34 | logger.info("Configuring aiogram")
35 | setup_handlers(dispatcher=dispatcher)
36 | setup_middlewares(dispatcher=dispatcher, bot=bot)
37 | setup_filters(dispatcher=dispatcher)
38 | logger.info("Configured aiogram")
39 |
40 |
41 | async def database_connected():
42 | # Ma'lumotlar bazasini yaratamiz:
43 | await db.create()
44 | # await db.drop_users()
45 | await db.create_table_users()
46 |
47 |
48 | async def aiogram_on_startup_polling(dispatcher: Dispatcher, bot: Bot) -> None:
49 | from utils.set_bot_commands import set_default_commands
50 | from utils.notify_admins import on_startup_notify
51 |
52 | logger.info("Database connected")
53 | await database_connected()
54 |
55 | logger.info("Starting polling")
56 | await bot.delete_webhook(drop_pending_updates=True)
57 | await setup_aiogram(bot=bot, dispatcher=dispatcher)
58 | await on_startup_notify(bot=bot)
59 | await set_default_commands(bot=bot)
60 |
61 |
62 | async def aiogram_on_shutdown_polling(dispatcher: Dispatcher, bot: Bot):
63 | logger.info("Stopping polling")
64 | await bot.session.close()
65 | await dispatcher.storage.close()
66 |
67 |
68 | def main():
69 | """CONFIG"""
70 | from data.config import BOT_TOKEN
71 | from aiogram.enums import ParseMode
72 | from aiogram.fsm.storage.memory import MemoryStorage
73 |
74 | bot = Bot(token=BOT_TOKEN, parse_mode=ParseMode.HTML)
75 | storage = MemoryStorage()
76 | dispatcher = Dispatcher(storage=storage)
77 |
78 | dispatcher.startup.register(aiogram_on_startup_polling)
79 | dispatcher.shutdown.register(aiogram_on_shutdown_polling)
80 | asyncio.run(dispatcher.start_polling(bot, close_bot_session=True))
81 | # allowed_updates=['message', 'chat_member']
82 |
83 |
84 | if __name__ == "__main__":
85 | try:
86 | main()
87 | except KeyboardInterrupt:
88 | logger.info("Bot stopped!")
89 |
--------------------------------------------------------------------------------
/utils/db/postgres.py:
--------------------------------------------------------------------------------
1 | from typing import Union
2 |
3 | import asyncpg
4 | from asyncpg import Connection
5 | from asyncpg.pool import Pool
6 |
7 | from data import config
8 |
9 |
10 | class Database:
11 | def __init__(self):
12 | self.pool: Union[Pool, None] = None
13 |
14 | async def create(self):
15 | self.pool = await asyncpg.create_pool(
16 | user=config.DB_USER,
17 | password=config.DB_PASS,
18 | host=config.DB_HOST,
19 | database=config.DB_NAME,
20 | )
21 |
22 | async def execute(
23 | self,
24 | command,
25 | *args,
26 | fetch: bool = False,
27 | fetchval: bool = False,
28 | fetchrow: bool = False,
29 | execute: bool = False,
30 | ):
31 |
32 | async with self.pool.acquire() as connection:
33 | connection: Connection
34 | async with connection.transaction():
35 | if fetch:
36 | result = await connection.fetch(command, *args)
37 | elif fetchval:
38 | result = await connection.fetchval(command, *args)
39 | elif fetchrow:
40 | result = await connection.fetchrow(command, *args)
41 | elif execute:
42 | result = await connection.execute(command, *args)
43 | return result
44 |
45 | async def create_table_users(self):
46 | sql = """
47 | CREATE TABLE IF NOT EXISTS Users (
48 | id SERIAL PRIMARY KEY,
49 | full_name VARCHAR(255) NOT NULL,
50 | username varchar(255) NULL,
51 | telegram_id BIGINT NOT NULL UNIQUE
52 | );
53 | """
54 | await self.execute(sql, execute=True)
55 |
56 | @staticmethod
57 | def format_args(sql, parameters: dict):
58 | sql += " AND ".join(
59 | [f"{item} = ${num}" for num, item in enumerate(parameters.keys(), start=1)]
60 | )
61 | return sql, tuple(parameters.values())
62 |
63 | async def add_user(self, full_name, username, telegram_id):
64 | sql = "INSERT INTO users (full_name, username, telegram_id) VALUES($1, $2, $3) returning *"
65 | return await self.execute(sql, full_name, username, telegram_id, fetchrow=True)
66 |
67 | async def select_all_users(self):
68 | sql = "SELECT * FROM Users"
69 | return await self.execute(sql, fetch=True)
70 |
71 | async def select_user(self, **kwargs):
72 | sql = "SELECT * FROM Users WHERE "
73 | sql, parameters = self.format_args(sql, parameters=kwargs)
74 | return await self.execute(sql, *parameters, fetchrow=True)
75 |
76 | async def count_users(self):
77 | sql = "SELECT COUNT(*) FROM Users"
78 | return await self.execute(sql, fetchval=True)
79 |
80 | async def update_user_username(self, username, telegram_id):
81 | sql = "UPDATE Users SET username=$1 WHERE telegram_id=$2"
82 | return await self.execute(sql, username, telegram_id, execute=True)
83 |
84 | async def delete_users(self):
85 | await self.execute("DELETE FROM Users WHERE TRUE", execute=True)
86 |
87 | async def drop_users(self):
88 | await self.execute("DROP TABLE Users", execute=True)
89 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | share/python-wheels/
24 | *.egg-info/
25 | .installed.cfg
26 | *.egg
27 | MANIFEST
28 |
29 | # PyInstaller
30 | # Usually these files are written by a python script from a template
31 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
32 | *.manifest
33 | *.spec
34 |
35 | # Installer logs
36 | pip-log.txt
37 | pip-delete-this-directory.txt
38 |
39 | # Unit test / coverage reports
40 | htmlcov/
41 | .tox/
42 | .nox/
43 | .coverage
44 | .coverage.*
45 | .cache
46 | nosetests.xml
47 | coverage.xml
48 | *.cover
49 | *.py,cover
50 | .hypothesis/
51 | .pytest_cache/
52 | cover/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | .pybuilder/
76 | target/
77 |
78 | # Jupyter Notebook
79 | .ipynb_checkpoints
80 |
81 | # IPython
82 | profile_default/
83 | ipython_config.py
84 |
85 | # pyenv
86 | # For a library or package, you might want to ignore these files since the code is
87 | # intended to run in multiple environments; otherwise, check them in:
88 | # .python-version
89 |
90 | # pipenv
91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
94 | # install all needed dependencies.
95 | #Pipfile.lock
96 |
97 | # poetry
98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99 | # This is especially recommended for binary packages to ensure reproducibility, and is more
100 | # commonly ignored for libraries.
101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102 | #poetry.lock
103 |
104 | # pdm
105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106 | #pdm.lock
107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108 | # in version control.
109 | # https://pdm.fming.dev/#use-with-ide
110 | .pdm.toml
111 |
112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
113 | __pypackages__/
114 |
115 | # Celery stuff
116 | celerybeat-schedule
117 | celerybeat.pid
118 |
119 | # SageMath parsed files
120 | *.sage.py
121 |
122 | # Environments
123 | .env
124 | .venv
125 | env/
126 | venv/
127 | ENV/
128 | env.bak/
129 | venv.bak/
130 |
131 | # Spyder project settings
132 | .spyderproject
133 | .spyproject
134 |
135 | # Rope project settings
136 | .ropeproject
137 |
138 | # mkdocs documentation
139 | /site
140 |
141 | # mypy
142 | .mypy_cache/
143 | .dmypy.json
144 | dmypy.json
145 |
146 | # Pyre type checker
147 | .pyre/
148 |
149 | # pytype static type analyzer
150 | .pytype/
151 |
152 | # Cython debug symbols
153 | cython_debug/
154 |
155 | # PyCharm
156 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
157 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
158 | # and can be added to the global gitignore or merged into this file. For a more nuclear
159 | # option (not recommended) you can uncomment the following to ignore the entire idea folder.
160 | .idea/
161 | data/*.xlsx
--------------------------------------------------------------------------------
/handlers/errors/error_handler.py:
--------------------------------------------------------------------------------
1 | import logging
2 | from typing import Any
3 |
4 | from aiogram import Router
5 | from aiogram.exceptions import (TelegramAPIError,
6 | TelegramUnauthorizedError,
7 | TelegramBadRequest,
8 | TelegramNetworkError,
9 | TelegramNotFound,
10 | TelegramConflictError,
11 | TelegramForbiddenError,
12 | RestartingTelegram,
13 | CallbackAnswerException,
14 | TelegramEntityTooLarge,
15 | TelegramRetryAfter,
16 | TelegramMigrateToChat,
17 | TelegramServerError)
18 | from aiogram.handlers import ErrorHandler
19 |
20 |
21 | router = Router()
22 |
23 |
24 | @router.errors()
25 | class MyErrorHandler(ErrorHandler):
26 | async def handle(self, ) -> Any:
27 | """
28 | Exceptions handler. Catches all exceptions within task factory tasks.
29 | :param dispatcher:
30 | :param update:
31 | :param exception:
32 | :return: stdout logging
33 | """
34 | if isinstance(self.exception_name, TelegramUnauthorizedError):
35 | """
36 | Bot tokeni yaroqsiz bo'lsa, xatolik uyushtiriladi.
37 | """
38 | logging.info(f'Unauthorized: {self.exception_message}')
39 | return True
40 |
41 | if isinstance(self.exception_name, TelegramNetworkError):
42 | """
43 | Telegram tarmog'idagi barcha xatoliklar uchun xatolik uyushtiriladi.
44 | """
45 | logging.exception(f'NetworkError: {self.exception_message} \nUpdate: {self.update}')
46 | return True
47 |
48 | if isinstance(self.exception_name, TelegramNotFound):
49 | """
50 | Suhbat, xabar, foydalanuvchi va boshqalar topilmasa, xatolik uyushtiriladi.
51 | """
52 | logging.exception(f'NotFound: {self.exception_message} \nUpdate: {self.update}')
53 | return True
54 |
55 | if isinstance(self.exception_name, TelegramConflictError):
56 | """
57 | Bot tokeni takroran ishlatilinayotganida xatolik uyushtiriladi.
58 | """
59 | logging.exception(f'ConflictError: {self.exception_message} \nUpdate: {self.update}')
60 | return True
61 |
62 | if isinstance(self.exception_name, TelegramForbiddenError):
63 | """
64 | Bot chatdan chiqarib yuborilishi kabi holatlarda xatolik uyushtiriladi.
65 | """
66 | logging.exception(f'ForbiddenError: {self.exception_message} \nUpdate: {self.update}')
67 | return True
68 |
69 | if isinstance(self.exception_name, CallbackAnswerException):
70 | """
71 | Javob qaytmasligi kabi holatlarda xatolik uyushtiriladi.
72 | """
73 | logging.exception(f'CallbackAnswerException: {self.exception_message} \nUpdate: {self.update}')
74 | return True
75 |
76 | if isinstance(self.exception_name, TelegramMigrateToChat):
77 | """
78 | Suhbat superguruhga ko'chirilganda xatolik uyushtiriladi.
79 | """
80 | logging.exception(f'BadRequest: {self.exception_message} \nUpdate: {self.update}')
81 | return True
82 |
83 | if isinstance(self.exception_name, TelegramServerError):
84 | """
85 | Telegram serveri 5xx xatosini qaytarsa, xatolik uyushtiriladi.
86 | """
87 | logging.exception(f'BadRequest: {self.exception_message} \nUpdate: {self.update}')
88 | return True
89 |
90 | if isinstance(self.exception_name, TelegramAPIError):
91 | """
92 | Barcha Telegram API xatoliklari uchun xatolik uyushtiriladi.
93 | """
94 | logging.exception(f'EntityTooLarge: {self.exception_message} \nUpdate: {self.update}')
95 | return True
96 |
97 | if isinstance(self.exception_name, TelegramRetryAfter):
98 | """
99 | So'rovlar ko'payib ketganda xatolik uyushtiriladi.
100 | """
101 | logging.exception(f'BadRequest: {self.exception_message} \nUpdate: {self.update}')
102 | return True
103 |
104 | if isinstance(self.exception_name, TelegramEntityTooLarge):
105 | """
106 | So'rov paytida ma'lumotlar limitdan oshganda xatolik uyushtiriladi.
107 | """
108 | logging.exception(f'EntityTooLarge: {self.exception_message} \nUpdate: {self.update}')
109 | return True
110 |
111 | if isinstance(self.exception_name, TelegramBadRequest):
112 | """
113 | So'rov noto'g'ri formatda bo'lganda xatolik uyushtiriladi.
114 | """
115 | logging.exception(f'BadRequest: {self.exception_message} \nUpdate: {self.update}')
116 | return True
117 |
118 | if isinstance(self.exception_name, RestartingTelegram):
119 | """
120 | Telegram serverini qayta ishga tushirishda xatolik uyushtiriladi.
121 | """
122 | logging.exception(f'RestartingTelegram: {self.exception_message} \nUpdate: {self.update}')
123 | return True
124 |
125 | logging.exception(f'Update: {self.update} \n{self.exception_name}')
126 |
--------------------------------------------------------------------------------