# █▀▀▄ █▀▄▀█ █▀█ █▀▄ █▀
# ▀▀▀█ ▄ █ ▀ █ █▄█ █▄▀ ▄█
# #### Copyright (c) 2025 Archquise #####
# 💬 Contact: https://t.me/archquise
# 🔒 Licensed under the GNU AGPLv3.
# 📄 LICENSE: https://raw.githubusercontent.com/archquise/Q.Mods/main/LICENSE
# ---------------------------------------------------------------------------------
# Name: Shortener
# Description: Module for using bit.ly API
# Author: @quise_m
# ---------------------------------------------------------------------------------
# meta developer: @quise_m
# meta banner: https://raw.githubusercontent.com/archquise/qmods_meta/main/Shortener.png
# ---------------------------------------------------------------------------------
import logging
import re
from http import HTTPStatus
import aiohttp
from .. import loader, utils
logger = logging.getLogger(__name__)
@loader.tds
class ShortenerMod(loader.Module):
"""Module for using bit.ly API."""
strings = { # noqa: RUF012
"name": "Shortener",
"no_api": "❌ You have not specified an API token from the site bit.ly",
"statclcmd": "📊 Statistics on clicks for this link: {c}",
"shortencmd": "✅ Your shortened link is ready: {c}",
"no_args": "❌ Please provide a URL to shorten.",
"invalid_url": "❌ Invalid URL format.",
"api_error": "❌ API error: {error}",
"_cls_doc": "Module for using bit.ly API",
}
strings_ru = { # noqa: RUF012
"no_api": "❌ Вы не указали api токен с сайта bit.ly",
"statclcmd": "📊 Статистика о переходе по этой ссылке: {c}",
"shortencmd": "✅ Ваша сокращённая ссылка готова: {c}",
"no_args": "❌ Пожалуйста, укажите URL для сокращения.",
"invalid_url": "❌ Неверный формат URL.",
"api_error": "❌ Ошибка API: {error}",
"_cls_doc": "Модуль для использования API bit.ly",
}
def __init__(self): # noqa: ANN204, D107
self.config = loader.ModuleConfig(
loader.ConfigValue(
"token",
None,
lambda: "Need a token with https://app.bitly.com/settings/api/",
validator=loader.validators.Hidden(),
),
)
async def client_ready(self, client, db): # noqa: D102, ARG002, ANN001, ANN201
self._aioclient = aiohttp.ClientSession()
def _validate_url(self, url: str) -> bool:
"""Validate URL format."""
if not url:
return False
url_pattern = re.compile(
r"^https?://"
r"(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+[A-Z]{2,6}\.?|"
r"localhost|"
r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})"
r"(?::\d+)?"
r"(?:/?|[/?]\S+)$",
re.IGNORECASE,
)
return url_pattern.match(url) is not None
async def shorten_url(self, url: str, token: str) -> str:
"""Short URL trough bit.ly API."""
async with self._aioclient.post(
"https://api-ssl.bitly.com/v4/shorten",
json={"long_url": url},
headers={"Authorization": f"Bearer {token}"},
) as resp:
if resp.status == HTTPStatus.CREATED:
json_response = await resp.json()
return json_response["link"]
logger.error("Error occurred! Status code: %s", resp.status)
return None
async def get_bitlink_stats(self, bitlink: str, token: str) -> str:
"""Get bitlink clicks stats."""
async with self._aioclient.get(
f"https://api-ssl.bitly.com/v4/bitlinks/{bitlink}/clicks/summary",
headers={"Authorization": f"Bearer {token}"},
) as resp:
if resp.status == HTTPStatus.OK:
json_response = await resp.json()
return json_response["total_clicks"]
logger.error("Error occurred! Status code: %s", resp.status)
return None
@loader.command(
ru_doc="Сократить ссылку через bit.ly (ссылка с https://)", # noqa: RUF001
en_doc="Shorten the link via bit.ly (url with https://)",
)
async def shortencmd(self, message): # noqa: ANN001, ANN201
"""Shorten URL using bit.ly API."""
if self.config["token"] is None:
await utils.answer(message, self.strings["no_api"])
return
args = utils.get_args_raw(message)
if not args:
await utils.answer(message, self.strings["no_args"])
return
if not self._validate_url(args):
await utils.answer(message, self.strings["invalid_url"])
return
try:
short_url = await self.shorten_url(url=args, token=self.config["token"])
await utils.answer(message, self.strings["shortencmd"].format(c=short_url))
except Exception as e:
logger.exception("Error shortening URL!")
await utils.answer(message, self.strings["api_error"].format(error=str(e)))
@loader.command(
ru_doc="Посмотреть статистику ссылки через bit.ly (ссылка без https:// | Доступно только на платных аккаунтах)",
en_doc="View link statistics via bit.ly (link without https:// | Works only on paid accounts)",
)
async def statclcmd(self, message): # noqa: ANN001, ANN201
"""Get click statistics for shortened URL."""
if self.config["token"] is None:
await utils.answer(message, self.strings["no_api"])
return
args = utils.get_args_raw(message)
if not args:
await utils.answer(message, self.strings["no_args"])
return
try:
if not args.startswith("bit.ly/"):
await utils.answer(message, self.strings["invalid_url"])
return
clicks = await self.get_bitlink_stats(
bitlink=args, token=self.config["token"]
)
await utils.answer(message, self.strings["statclcmd"].format(c=clicks))
except Exception as e:
logger.exception("Error getting statistics!")
await utils.answer(message, self.strings["api_error"].format(error=str(e)))