feat: exchange inline query

This commit is contained in:
xtaodada 2024-11-26 22:04:09 +08:00
parent c20bce5105
commit b186e7ad2f
Signed by: xtaodada
GPG Key ID: 4CBB3F4FA8C85659
11 changed files with 297 additions and 305 deletions

View File

@ -21,8 +21,8 @@ class Exchange:
except Exception:
pass
async def check_ex(self, message):
tlist = message.text.split()
async def check_ex(self, text: str):
tlist = text.split()
if not 2 < len(tlist) < 5:
return "help"
elif len(tlist) == 3:

View File

@ -1,145 +0,0 @@
import contextlib
import re
from datetime import datetime
from typing import Optional
from bs4 import BeautifulSoup
from init import request
from models.services.fragment import (
AuctionStatus,
UserName,
TON_TO_USD_RATE,
Price,
FragmentSubText,
FragmentSub,
)
class NotAvailable(Exception):
pass
async def get_fragment_html(username: str):
try:
resp = await request.get(
f"https://fragment.com/username/{username}", follow_redirects=False
)
assert resp.status_code == 200
return resp.text
except AssertionError as e:
raise AssertionError from e
except Exception as e:
raise NotAvailable from e
def refresh_rate(html: str) -> None:
pattern = re.compile(r'"tonRate":"(.+)"}}')
with contextlib.suppress(Exception):
TON_TO_USD_RATE["rate"] = float(pattern.findall(html)[0])
def parse_user(username: str, html: str) -> UserName:
soup = BeautifulSoup(html, "lxml")
try:
refresh_rate(html)
status = AuctionStatus(
soup.find("span", {"class": "tm-section-header-status"}).getText()
)
if status == AuctionStatus.OnAuction and "Highest Bid" not in html:
status = AuctionStatus.Available
user = UserName(name=username, status=status)
if user.status == AuctionStatus.Available:
user.now_price = Price(
ton=int(
soup.find(
"div",
{"class": "table-cell-value tm-value icon-before icon-ton"},
)
.getText()
.replace(",", "")
)
)
elif user.status in [AuctionStatus.OnAuction, AuctionStatus.Sale]:
info = soup.find("div", {"class": "tm-section-box tm-section-bid-info"})
user.now_price = Price(
ton=int(
info.find(
"div",
{"class": "table-cell-value tm-value icon-before icon-ton"},
)
.getText()
.replace(",", "")
)
)
user.end_time = datetime.fromisoformat(
soup.find("time", {"class": "tm-countdown-timer"})["datetime"]
)
elif user.status == AuctionStatus.Sold:
info = soup.find("div", {"class": "tm-section-box tm-section-bid-info"})
user.now_price = Price(
ton=int(
info.find(
"div",
{"class": "table-cell-value tm-value icon-before icon-ton"},
)
.getText()
.replace(",", "")
)
)
user.purchaser = info.find("a")["href"].split("/")[-1]
user.end_time = datetime.fromisoformat(info.find("time")["datetime"])
return user
except (AttributeError, ValueError) as e:
raise NotAvailable from e
async def search_fragment_html(username: str) -> str:
try:
resp = await request.get(
f"https://fragment.com/?query={username}", follow_redirects=False
)
return resp.text
except Exception as e:
raise NotAvailable from e
def search_user(username: str, html: str) -> UserName:
soup = BeautifulSoup(html, "lxml")
try:
user = soup.find_all("tr", {"class": "tm-row-selectable"})[0]
status = AuctionStatus(
user.find("div", {"class": "table-cell-status-thin"}).getText()
)
return UserName(name=username, status=status)
except (AttributeError, ValueError, IndexError) as e:
raise NotAvailable from e
async def parse_fragment(username: str) -> UserName:
try:
html = await get_fragment_html(username)
return parse_user(username, html)
except AssertionError:
html = await search_fragment_html(username)
return search_user(username, html)
async def parse_sub(status: FragmentSubText, user: Optional[UserName], cid: int) -> str:
if status == FragmentSubText.Subscribe:
if user.status == [AuctionStatus.Sold, AuctionStatus.Unavailable]:
return "用户名已被卖出或者已被注册,无法订阅"
if await FragmentSub.get_by_cid_and_username(cid, user.name):
return "已经订阅过了这个用户名"
await FragmentSub.subscribe(cid, user.name)
return "订阅成功"
elif status == FragmentSubText.Unsubscribe:
if data := (await FragmentSub.get_by_cid_and_username(cid, user.name)):
await FragmentSub.unsubscribe(data)
return "取消订阅成功"
return "当前没有订阅这个用户名"
elif status == FragmentSubText.List:
if data := (await FragmentSub.get_by_cid(cid)):
return "目前已订阅:\n\n" + "\n".join(
[f"{i+1}. @{d.username}" for i, d in enumerate(data)]
)
return "还没有订阅任何用户名"

View File

@ -0,0 +1,31 @@
import re
from re import Pattern
from typing import Union
from pyrogram.filters import create
from pyrogram.types import Message, CallbackQuery, InlineQuery, PreCheckoutQuery, ChosenInlineResult, Update
def regex(pattern: Union[str, Pattern], flags: int = 0):
async def func(flt, _, update: Update):
if isinstance(update, Message):
value = update.text or update.caption
elif isinstance(update, CallbackQuery):
value = update.data
elif isinstance(update, (InlineQuery, ChosenInlineResult)):
value = update.query
elif isinstance(update, PreCheckoutQuery):
value = update.invoice_payload
else:
raise ValueError(f"Regex filter doesn't work with {type(update)}")
if value:
update.matches = list(flt.p.finditer(value)) or None
return bool(update.matches)
return create(
func,
"RegexFilter",
p=pattern if isinstance(pattern, Pattern) else re.compile(pattern, flags)
)

View File

@ -12,6 +12,7 @@ from pyrogram.types import (
)
from pyrogram.utils import unpack_inline_message_id
from defs import inline_result_filters
from init import bot
geo_dic = {
@ -84,10 +85,8 @@ def get_dc_text(dc: int):
return f"此会话所在数据中心为: <b>DC{dc}</b>\n" f"该数据中心位于 <b>{geo_dic[str(dc)]}</b>"
@bot.on_chosen_inline_result()
@bot.on_chosen_inline_result(inline_result_filters.regex(r"^dc$"))
async def dc_choose_callback(_: Client, chosen_inline_result: ChosenInlineResult):
if chosen_inline_result.query != "dc":
chosen_inline_result.continue_propagation()
mid = chosen_inline_result.inline_message_id
if not mid:
return

View File

@ -1,6 +1,6 @@
from pyrogram import Client, filters
from pyrogram.enums import ChatType
from pyrogram.types import Message
from pyrogram.types import Message, InlineQuery, InlineQueryResultArticle, InputTextMessageContent
from defs.exchange import exchange_client
from scheduler import scheduler
@ -12,19 +12,15 @@ async def exchange_refresh() -> None:
await exchange_client.refresh()
@bot.on_message(
filters.incoming & filters.command(["exchange", f"exchange@{bot.me.username}"])
)
async def exchange_command(_: Client, message: Message):
async def get_text(text: str, inline: bool):
if not exchange_client.inited:
await exchange_client.refresh()
if not exchange_client.inited:
return await message.reply("获取汇率数据出现错误!")
text = await exchange_client.check_ex(message)
return "获取汇率数据出现错误!", False
text = await exchange_client.check_ex(text)
if text == "help":
prefix = "" if inline else "/"
text = (
"该指令可用于查询汇率。\n使用方式举例:\n/exchange USD CNY - 1 USD 等于多少 CNY\n"
"/exchange 11 CNY USD - 11 CNY 等于多少 USD"
f"该指令可用于查询汇率。\n使用方式举例:\n{prefix}exchange USD CNY - 1 USD 等于多少 CNY\n"
f"{prefix}exchange 11 CNY USD - 11 CNY 等于多少 USD"
)
elif text == "ValueError":
text = "金额不合法"
@ -37,9 +33,37 @@ async def exchange_command(_: Client, message: Message):
elif text == "ToError":
text = "不支持的目标币种。"
else:
return await message.reply(text)
return text, True
return text, False
@bot.on_message(
filters.incoming & filters.command(["exchange", f"exchange@{bot.me.username}"])
)
async def exchange_command(_: Client, message: Message):
if not exchange_client.inited:
await exchange_client.refresh()
text, success = await get_text(message.text, False)
reply_ = await message.reply(text)
if message.chat.type == ChatType.PRIVATE:
if not success and message.chat.type == ChatType.PRIVATE:
await reply_.reply(
"支持货币: <code>" + ", ".join(exchange_client.currencies) + "</code>"
)
@bot.on_inline_query(filters.regex("^exchange"))
async def exchange_inline(_: Client, inline_query: InlineQuery):
text, success = await get_text(inline_query.query, True)
results = [
InlineQueryResultArticle(
title="查询汇率数据成功" if success else "查询汇率数据失败",
input_message_content=InputTextMessageContent(message_text=text),
# reply_markup=InlineKeyboardMarkup(
# [[InlineKeyboardButton(text="重试", callback_data="dc")]]
# ),
)
]
await inline_query.answer(
results=results,
cache_time=0,
)

View File

@ -1,120 +0,0 @@
import contextlib
import re
from pyrogram import Client, filters, ContinuePropagation
from pyrogram.enums import ChatMemberStatus
from pyrogram.types import (
InlineQuery,
InlineQueryResultArticle,
InputTextMessageContent,
InlineKeyboardMarkup,
InlineKeyboardButton,
Message,
)
from models.services.fragment import FragmentSubText, FragmentSub, AuctionStatus
from defs.fragment import parse_fragment, NotAvailable, parse_sub
from init import bot
from scheduler import scheduler, add_delete_message_job
QUERY_PATTERN = re.compile(r"^@\w[a-zA-Z0-9_]{3,32}$")
@bot.on_message(
filters.incoming & filters.command(["username", f"username@{bot.me.username}"])
)
async def fragment_command(client: Client, message: Message):
status = None
user = None
if len(message.command) <= 1:
return await message.reply("没有找到要查询的用户名 ...")
elif message.command[1] == "订阅列表":
status = FragmentSubText.List
elif len(message.command) > 2:
if message.command[2] not in ["订阅", "退订"]:
return await message.reply("只能查询一个用户名 ...")
status = FragmentSubText(message.command[2])
if status and message.from_user:
data = await client.get_chat_member(message.chat.id, message.from_user.id)
if data.status not in [ChatMemberStatus.ADMINISTRATOR, ChatMemberStatus.OWNER]:
rep = await message.reply("You are not an admin of this chat.")
add_delete_message_job(rep)
raise ContinuePropagation
if status == FragmentSubText.List:
text = await parse_sub(status, user, message.chat.id)
else:
username = message.command[1]
if not username.startswith("@"):
username = f"@{username}"
if not QUERY_PATTERN.match(username):
return await message.reply("无效的用户名")
username = username[1:]
try:
user = await parse_fragment(username)
text = user.text
except NotAvailable:
text = "解析失败了 ... 请稍后再试"
except Exception:
text = "查询失败了 ... 请稍后再试"
if status and user is not None:
text = await parse_sub(status, user, message.chat.id)
await message.reply(text)
@bot.on_inline_query(filters=filters.regex(r"^@\w[a-zA-Z0-9_]{3,32}$"))
async def fragment_inline(_, inline_query: InlineQuery):
username = inline_query.query
username = username[1:]
try:
user = await parse_fragment(username)
text = user.text
except NotAvailable:
text = f"用户名:@{username}\n状态:暂未开放购买\n"
except Exception:
text = ""
if not text:
await inline_query.answer(
results=[],
switch_pm_text="查询失败了 ~ 呜呜呜",
switch_pm_parameter="start",
cache_time=0,
)
inline_query.stop_propagation()
results = [
InlineQueryResultArticle(
title=username,
input_message_content=InputTextMessageContent(text),
url=f"https://fragment.com/username/{username}",
description="点击发送详情",
reply_markup=InlineKeyboardMarkup(
[
[
InlineKeyboardButton(
"Open", url=f"https://fragment.com/username/{username}"
)
]
]
),
),
]
await inline_query.answer(
results=results,
switch_pm_text="查询成功",
switch_pm_parameter="start",
cache_time=0,
)
inline_query.stop_propagation()
@scheduler.scheduled_job("cron", hour="8", minute="1", id="fragment.sub")
async def fragment_sub() -> None:
data = await FragmentSub.get_all()
if not data:
return
for item in data:
with contextlib.suppress(NotAvailable, Exception):
user = await parse_fragment(item.username)
text = user.text
if user.status in [AuctionStatus.Sold, AuctionStatus.Unavailable]:
await FragmentSub.unsubscribe(item)
await bot.send_message(item.cid, text)

View File

@ -64,6 +64,11 @@ async def empty_inline(_, inline_query: InlineQuery):
input_message_content=InputTextMessageContent("使用 dc 来查询会话数据中心"),
description="使用 dc 来查询会话数据中心",
),
InlineQueryResultArticle(
title="exchange",
input_message_content=InputTextMessageContent("使用 exchange 来查询汇率数据"),
description="使用 exchange 来查询汇率数据",
),
]
return await inline_query.answer(
results=results,

7
pyproject.toml Normal file
View File

@ -0,0 +1,7 @@
[project]
name = "ishotabot"
version = "0.1.0"
description = "ishotabot"
readme = "README.md"
requires-python = ">=3.9"
dependencies = []

22
requirements.in Normal file
View File

@ -0,0 +1,22 @@
git+https://github.com/TeamPGM/pyrogram
pyrotgcrypto
bilibili-api-python
httpx
pillow
cashews
coloredlogs
qrcode
playwright
uvicorn
jinja2
apscheduler
pytz
beautifulsoup4
lxml
sqlalchemy
sqlmodel
aiosqlite
aiofiles
pydantic
lottie
atproto

View File

@ -1,22 +1,184 @@
git+https://github.com/TeamPGM/pyrogram
tgcrypto==1.2.5
bilibili-api-python==16.2.1
httpx
pillow
cashews
coloredlogs
qrcode
playwright
uvicorn
jinja2
apscheduler
pytz
beautifulsoup4
lxml
sqlalchemy
sqlmodel
aiosqlite
aiofiles
pydantic
lottie
atproto
# This file was autogenerated by uv via the following command:
# uv pip compile requirements.txt -o requirements.out
aiofiles==24.1.0
# via -r requirements.txt
aiohappyeyeballs==2.4.3
# via aiohttp
aiohttp==3.10.5
# via bilibili-api-python
aiosignal==1.3.1
# via aiohttp
aiosqlite==0.20.0
# via -r requirements.txt
annotated-types==0.7.0
# via pydantic
anyio==4.6.2.post1
# via httpx
apscheduler==3.10.4
# via
# -r requirements.txt
# bilibili-api-python
atproto==0.0.55
# via -r requirements.txt
attrs==24.2.0
# via aiohttp
beautifulsoup4==4.12.3
# via
# -r requirements.txt
# bilibili-api-python
bilibili-api-python==16.3.0
# via -r requirements.txt
brotli==1.1.0
# via bilibili-api-python
cashews==7.4.0
# via -r requirements.txt
certifi==2024.8.30
# via
# httpcore
# httpx
cffi==1.17.1
# via cryptography
click==8.1.7
# via
# atproto
# uvicorn
colorama==0.4.6
# via
# bilibili-api-python
# click
# qrcode
# tqdm
coloredlogs==15.0.1
# via -r requirements.txt
cryptography==43.0.3
# via atproto
dnspython==2.7.0
# via atproto
frozenlist==1.5.0
# via
# aiohttp
# aiosignal
greenlet==3.1.1
# via
# playwright
# sqlalchemy
h11==0.14.0
# via
# httpcore
# uvicorn
httpcore==1.0.7
# via httpx
httpx==0.26.0
# via
# -r requirements.txt
# atproto
# bilibili-api-python
humanfriendly==10.0
# via coloredlogs
idna==3.10
# via
# anyio
# httpx
# yarl
jinja2==3.1.4
# via -r requirements.txt
libipld==3.0.0
# via atproto
lottie==0.7.1
# via -r requirements.txt
lxml==5.3.0
# via
# -r requirements.txt
# bilibili-api-python
markupsafe==3.0.2
# via jinja2
multidict==6.1.0
# via
# aiohttp
# yarl
pillow==10.4.0
# via
# -r requirements.txt
# bilibili-api-python
# qrcode-terminal
playwright==1.49.0
# via -r requirements.txt
pyaes==1.6.1
# via pyrogram
pyasn1==0.6.1
# via rsa
pycparser==2.22
# via cffi
pycryptodomex==3.20.0
# via bilibili-api-python
pydantic==2.10.2
# via
# -r requirements.txt
# atproto
# sqlmodel
pydantic-core==2.27.1
# via pydantic
pyee==12.0.0
# via playwright
pypng==0.20220715.0
# via qrcode
pyreadline3==3.5.4
# via humanfriendly
pyrogram @ git+https://github.com/TeamPGM/pyrogram@7206c0ec074c0e8b7d304dc81a8dbb4aca16b86d
# via -r requirements.txt
pyrotgcrypto==1.2.7
# via -r requirements.txt
pysocks==1.7.1
# via pyrogram
pytz==2024.2
# via
# -r requirements.txt
# apscheduler
pyyaml==6.0.2
# via bilibili-api-python
qrcode==7.4.2
# via
# -r requirements.txt
# bilibili-api-python
# qrcode-terminal
qrcode-terminal==0.8
# via bilibili-api-python
rsa==4.9
# via bilibili-api-python
six==1.16.0
# via apscheduler
sniffio==1.3.1
# via
# anyio
# httpx
soupsieve==2.6
# via beautifulsoup4
sqlalchemy==2.0.36
# via
# -r requirements.txt
# sqlmodel
sqlmodel==0.0.22
# via -r requirements.txt
tqdm==4.66.6
# via bilibili-api-python
typing-extensions==4.12.2
# via
# aiosqlite
# atproto
# pydantic
# pydantic-core
# pyee
# qrcode
# sqlalchemy
tzdata==2024.2
# via tzlocal
tzlocal==5.2
# via apscheduler
uvicorn==0.32.1
# via -r requirements.txt
websockets==13.1
# via atproto
yarl==1.11.1
# via
# aiohttp
# bilibili-api-python

7
uv.lock Normal file
View File

@ -0,0 +1,7 @@
version = 1
requires-python = ">=3.9"
[[package]]
name = "ishotabot"
version = "0.1.0"
source = { virtual = "." }