mirror of
https://github.com/PaiGramTeam/PaiGram.git
synced 2024-11-22 07:07:46 +00:00
✨ Add inline search
This commit is contained in:
parent
bfab1ec213
commit
e20e20111d
10
core/search/__init__.py
Normal file
10
core/search/__init__.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
from core.service import init_service
|
||||||
|
from .services import SearchServices as _SearchServices
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
|
|
||||||
|
|
||||||
|
@init_service
|
||||||
|
def create_search_service():
|
||||||
|
_service = _SearchServices()
|
||||||
|
return _service
|
73
core/search/models.py
Normal file
73
core/search/models.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
from abc import abstractmethod
|
||||||
|
from typing import Optional, List
|
||||||
|
|
||||||
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
__all__ = ["BaseEntry", "WeaponEntry", "WeaponsEntry", "StrategyEntry", "StrategyEntryList"]
|
||||||
|
|
||||||
|
from thefuzz import fuzz
|
||||||
|
|
||||||
|
|
||||||
|
class BaseEntry(BaseModel):
|
||||||
|
"""所有可搜索条目的基类。
|
||||||
|
|
||||||
|
Base class for all searchable entries."""
|
||||||
|
|
||||||
|
key: str # 每个条目的Key必须唯一
|
||||||
|
title: str
|
||||||
|
description: str
|
||||||
|
tags: Optional[List[str]] = []
|
||||||
|
caption: Optional[str] = None
|
||||||
|
parse_mode: Optional[str] = None
|
||||||
|
photo_url: Optional[str] = None
|
||||||
|
photo_file_id: Optional[str] = None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def compare_to_query(self, search_query: str) -> float:
|
||||||
|
"""返回一个数字 ∈[0,100] 描述搜索查询与此条目的相似程度。
|
||||||
|
|
||||||
|
Gives a number ∈[0,100] describing how similar the search query is to this entry."""
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponEntry(BaseEntry):
|
||||||
|
def compare_to_query(self, search_query: str) -> float:
|
||||||
|
score = 0.0
|
||||||
|
if search_query == self.title:
|
||||||
|
return 100
|
||||||
|
if self.tags:
|
||||||
|
if search_query in self.tags:
|
||||||
|
return 99
|
||||||
|
for tag in self.tags:
|
||||||
|
_score = fuzz.partial_token_set_ratio(tag, search_query)
|
||||||
|
if _score >= score:
|
||||||
|
score = _score
|
||||||
|
if score >= 90:
|
||||||
|
return score * 0.99
|
||||||
|
if self.description:
|
||||||
|
_score = fuzz.partial_token_set_ratio(self.description, search_query)
|
||||||
|
if _score >= score:
|
||||||
|
return _score
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
class WeaponsEntry(BaseModel):
|
||||||
|
data: Optional[List[WeaponEntry]]
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyEntry(BaseEntry):
|
||||||
|
def compare_to_query(self, search_query: str) -> float:
|
||||||
|
score = 0.0
|
||||||
|
if search_query == self.title:
|
||||||
|
return 100
|
||||||
|
if self.tags:
|
||||||
|
if search_query in self.tags:
|
||||||
|
return 99
|
||||||
|
for tag in self.tags:
|
||||||
|
_score = fuzz.partial_token_set_ratio(tag, search_query)
|
||||||
|
if _score >= score:
|
||||||
|
score = _score
|
||||||
|
return score
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyEntryList(BaseModel):
|
||||||
|
data: Optional[List[StrategyEntry]]
|
148
core/search/services.py
Normal file
148
core/search/services.py
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
import asyncio
|
||||||
|
import heapq
|
||||||
|
import itertools
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Tuple, List, Optional, Dict
|
||||||
|
|
||||||
|
import aiofiles
|
||||||
|
from async_lru import alru_cache
|
||||||
|
|
||||||
|
from core.search.models import WeaponEntry, BaseEntry, WeaponsEntry, StrategyEntry, StrategyEntryList
|
||||||
|
from utils.const import PROJECT_ROOT
|
||||||
|
|
||||||
|
ENTRY_DAYA_PATH = PROJECT_ROOT.joinpath("data", "entry")
|
||||||
|
ENTRY_DAYA_PATH.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
class SearchServices:
|
||||||
|
def __init__(self):
|
||||||
|
self._lock = asyncio.Lock() # 访问和修改操作成员变量必须加锁操作
|
||||||
|
self.weapons: List[WeaponEntry] = []
|
||||||
|
self.strategy: List[StrategyEntry] = []
|
||||||
|
self.entry_data_path: Path = ENTRY_DAYA_PATH
|
||||||
|
self.weapons_entry_data_path = self.entry_data_path / "weapon.json"
|
||||||
|
self.strategy_entry_data_path = self.entry_data_path / "strategy.json"
|
||||||
|
self.replace_time: Dict[str, float] = {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def load_json(path):
|
||||||
|
async with aiofiles.open(path, "r", encoding="utf-8") as f:
|
||||||
|
return json.loads(await f.read())
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
async def save_json(path, data):
|
||||||
|
async with aiofiles.open(path, "w", encoding="utf-8") as f:
|
||||||
|
await f.write(data)
|
||||||
|
|
||||||
|
async def load_data(self):
|
||||||
|
async with self._lock:
|
||||||
|
if self.weapons_entry_data_path.exists():
|
||||||
|
weapon_json = await self.load_json(self.weapons_entry_data_path)
|
||||||
|
weapons = WeaponsEntry.parse_obj(weapon_json)
|
||||||
|
for weapon in weapons.data:
|
||||||
|
self.weapons.append(weapon.copy())
|
||||||
|
if self.strategy_entry_data_path.exists():
|
||||||
|
strategy_json = await self.load_json(self.strategy_entry_data_path)
|
||||||
|
strategy = StrategyEntryList.parse_obj(strategy_json)
|
||||||
|
for strategy in strategy.data:
|
||||||
|
self.strategy.append(strategy.copy())
|
||||||
|
|
||||||
|
async def save_entry(self) -> None:
|
||||||
|
"""保存条目
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
if len(self.weapons) > 0:
|
||||||
|
weapons = WeaponsEntry(data=self.weapons)
|
||||||
|
await self.save_json(self.weapons_entry_data_path, weapons.json())
|
||||||
|
if len(self.strategy) > 0:
|
||||||
|
strategy = StrategyEntryList(data=self.strategy)
|
||||||
|
await self.save_json(self.strategy_entry_data_path, strategy.json())
|
||||||
|
|
||||||
|
async def add_entry(self, entry: BaseEntry, update: bool = False, ttl: int = 3600):
|
||||||
|
"""添加条目
|
||||||
|
:param entry: 条目数据
|
||||||
|
:param update: 如果条目存在是否覆盖
|
||||||
|
:param ttl: 条目存在时需要多久时间覆盖
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
replace_time = self.replace_time.get(entry.key)
|
||||||
|
if replace_time and replace_time <= time.time() + ttl:
|
||||||
|
return
|
||||||
|
if isinstance(entry, WeaponEntry):
|
||||||
|
for index, value in enumerate(self.weapons):
|
||||||
|
if value.key == entry.key:
|
||||||
|
if update:
|
||||||
|
self.replace_time[entry.key] = time.time()
|
||||||
|
self.weapons[index] = entry
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.weapons.append(entry)
|
||||||
|
elif isinstance(entry, StrategyEntry):
|
||||||
|
for index, value in enumerate(self.strategy):
|
||||||
|
if value.key == entry.key:
|
||||||
|
if update:
|
||||||
|
self.replace_time[entry.key] = time.time()
|
||||||
|
self.strategy[index] = entry
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.strategy.append(entry)
|
||||||
|
|
||||||
|
async def remove_all_entry(self):
|
||||||
|
"""移除全部条目
|
||||||
|
:return: None
|
||||||
|
"""
|
||||||
|
async with self._lock:
|
||||||
|
self.weapons = []
|
||||||
|
if self.weapons_entry_data_path.exists():
|
||||||
|
os.remove(self.weapons_entry_data_path)
|
||||||
|
self.strategy = []
|
||||||
|
if self.strategy_entry_data_path.exists():
|
||||||
|
os.remove(self.strategy_entry_data_path)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sort_key(entry: BaseEntry, search_query: str) -> float:
|
||||||
|
return entry.compare_to_query(search_query)
|
||||||
|
|
||||||
|
@alru_cache(maxsize=64)
|
||||||
|
async def multi_search_combinations(self, search_queries: Tuple[str], results_per_query: int = 3):
|
||||||
|
"""多个关键词搜索
|
||||||
|
:param search_queries: 搜索文本
|
||||||
|
:param results_per_query: 约定返回的数目
|
||||||
|
:return: 搜索结果
|
||||||
|
"""
|
||||||
|
results = {}
|
||||||
|
effective_queries = list(dict.fromkeys(search_queries))
|
||||||
|
for query in effective_queries:
|
||||||
|
if res := await self.search(search_query=query, amount=results_per_query):
|
||||||
|
results[query] = res
|
||||||
|
|
||||||
|
@alru_cache(maxsize=64)
|
||||||
|
async def search(self, search_query: Optional[str], amount: int = None) -> Optional[List[BaseEntry]]:
|
||||||
|
"""在所有可用条目中搜索适当的结果
|
||||||
|
:param search_query: 搜索文本
|
||||||
|
:param amount: 约定返回的数目
|
||||||
|
:return: 搜索结果
|
||||||
|
"""
|
||||||
|
# search_entries: Iterable[BaseEntry] = []
|
||||||
|
async with self._lock:
|
||||||
|
search_entries = itertools.chain(self.weapons, self.strategy)
|
||||||
|
|
||||||
|
if not search_query:
|
||||||
|
return search_entries if isinstance(search_entries, list) else list(search_entries)
|
||||||
|
|
||||||
|
if not amount:
|
||||||
|
return sorted(
|
||||||
|
search_entries,
|
||||||
|
key=lambda entry: self._sort_key(entry, search_query), # type: ignore
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
return heapq.nlargest(
|
||||||
|
amount,
|
||||||
|
search_entries,
|
||||||
|
key=lambda entry: self._sort_key(entry, search_query), # type: ignore[arg-type]
|
||||||
|
)
|
@ -1,10 +1,11 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
from typing import List
|
||||||
|
|
||||||
from metadata.genshin import WEAPON_DATA
|
from metadata.genshin import WEAPON_DATA
|
||||||
|
|
||||||
__all__ = ["roles", "weapons", "roleToId", "roleToName", "weaponToName", "weaponToId", "not_real_roles"]
|
__all__ = ["roles", "weapons", "roleToId", "roleToName", "weaponToName", "weaponToId", "not_real_roles", "roleToTag"]
|
||||||
|
|
||||||
# noinspection SpellCheckingInspection
|
# noinspection SpellCheckingInspection
|
||||||
roles = {
|
roles = {
|
||||||
@ -393,3 +394,10 @@ def weaponToName(shortname: str) -> str:
|
|||||||
def weaponToId(name: str) -> int | None:
|
def weaponToId(name: str) -> int | None:
|
||||||
"""获取武器ID"""
|
"""获取武器ID"""
|
||||||
return next((int(key) for key, value in WEAPON_DATA.items() if weaponToName(name) in value["name"]), None)
|
return next((int(key) for key, value in WEAPON_DATA.items() if weaponToName(name) in value["name"]), None)
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyPep8Naming
|
||||||
|
@functools.lru_cache()
|
||||||
|
def roleToTag(role_name: str) -> List[str]:
|
||||||
|
"""通过角色名获取TAG"""
|
||||||
|
return next((value for value in roles.values() if value[0] == role_name), [role_name])
|
||||||
|
@ -8,7 +8,9 @@ from telegram.ext import MessageHandler, filters
|
|||||||
from core.baseplugin import BasePlugin
|
from core.baseplugin import BasePlugin
|
||||||
from core.game.services import GameStrategyService
|
from core.game.services import GameStrategyService
|
||||||
from core.plugin import Plugin, handler
|
from core.plugin import Plugin, handler
|
||||||
from metadata.shortname import roleToName
|
from core.search.models import StrategyEntry
|
||||||
|
from core.search.services import SearchServices
|
||||||
|
from metadata.shortname import roleToName, roleToTag
|
||||||
from utils.bot import get_args
|
from utils.bot import get_args
|
||||||
from utils.decorators.error import error_callable
|
from utils.decorators.error import error_callable
|
||||||
from utils.decorators.restricts import restricts
|
from utils.decorators.restricts import restricts
|
||||||
@ -21,8 +23,13 @@ class StrategyPlugin(Plugin, BasePlugin):
|
|||||||
|
|
||||||
KEYBOARD = [[InlineKeyboardButton(text="查看角色攻略列表并查询", switch_inline_query_current_chat="查看角色攻略列表并查询")]]
|
KEYBOARD = [[InlineKeyboardButton(text="查看角色攻略列表并查询", switch_inline_query_current_chat="查看角色攻略列表并查询")]]
|
||||||
|
|
||||||
def __init__(self, game_strategy_service: GameStrategyService = None):
|
def __init__(
|
||||||
|
self,
|
||||||
|
game_strategy_service: GameStrategyService = None,
|
||||||
|
search_service: SearchServices = None,
|
||||||
|
):
|
||||||
self.game_strategy_service = game_strategy_service
|
self.game_strategy_service = game_strategy_service
|
||||||
|
self.search_service = search_service
|
||||||
|
|
||||||
@handler(CommandHandler, command="strategy", block=False)
|
@handler(CommandHandler, command="strategy", block=False)
|
||||||
@handler(MessageHandler, filters=filters.Regex("^角色攻略查询(.*)"), block=False)
|
@handler(MessageHandler, filters=filters.Regex("^角色攻略查询(.*)"), block=False)
|
||||||
@ -53,11 +60,24 @@ class StrategyPlugin(Plugin, BasePlugin):
|
|||||||
logger.info(f"用户 {user.full_name}[{user.id}] 查询角色攻略命令请求 || 参数 {character_name}")
|
logger.info(f"用户 {user.full_name}[{user.id}] 查询角色攻略命令请求 || 参数 {character_name}")
|
||||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||||
file_path = await url_to_file(url, return_path=True)
|
file_path = await url_to_file(url, return_path=True)
|
||||||
caption = "From 米游社 西风驿站 " f"查看 [原图]({url})"
|
caption = f"From 米游社 西风驿站 查看<a href='{url}'>原图</a>"
|
||||||
await message.reply_photo(
|
reply_photo = await message.reply_photo(
|
||||||
photo=open(file_path, "rb"),
|
photo=open(file_path, "rb"),
|
||||||
caption=caption,
|
caption=caption,
|
||||||
filename=f"{character_name}.png",
|
filename=f"{character_name}.png",
|
||||||
allow_sending_without_reply=True,
|
allow_sending_without_reply=True,
|
||||||
parse_mode=ParseMode.MARKDOWN_V2,
|
parse_mode=ParseMode.HTML,
|
||||||
)
|
)
|
||||||
|
if reply_photo.photo:
|
||||||
|
tags = roleToTag(character_name)
|
||||||
|
photo_file_id = reply_photo.photo[0].file_id
|
||||||
|
entry = StrategyEntry(
|
||||||
|
key=f"plugin:strategy:{character_name}",
|
||||||
|
title=character_name,
|
||||||
|
description=f"{character_name} 角色攻略",
|
||||||
|
tags=tags,
|
||||||
|
caption=caption,
|
||||||
|
parse_mode="HTML",
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
|
)
|
||||||
|
await self.search_service.add_entry(entry)
|
||||||
|
@ -5,10 +5,12 @@ from telegram.ext import CallbackContext, CommandHandler, MessageHandler, filter
|
|||||||
from core.base.assets import AssetsService, AssetsCouldNotFound
|
from core.base.assets import AssetsService, AssetsCouldNotFound
|
||||||
from core.baseplugin import BasePlugin
|
from core.baseplugin import BasePlugin
|
||||||
from core.plugin import Plugin, handler
|
from core.plugin import Plugin, handler
|
||||||
|
from core.search.models import WeaponEntry
|
||||||
|
from core.search.services import SearchServices
|
||||||
from core.template import TemplateService
|
from core.template import TemplateService
|
||||||
from core.wiki.services import WikiService
|
from core.wiki.services import WikiService
|
||||||
from metadata.genshin import honey_id_to_game_id
|
from metadata.genshin import honey_id_to_game_id
|
||||||
from metadata.shortname import weaponToName
|
from metadata.shortname import weaponToName, weapons as _weapons_data
|
||||||
from modules.wiki.weapon import Weapon
|
from modules.wiki.weapon import Weapon
|
||||||
from utils.bot import get_args
|
from utils.bot import get_args
|
||||||
from utils.decorators.error import error_callable
|
from utils.decorators.error import error_callable
|
||||||
@ -27,10 +29,12 @@ class WeaponPlugin(Plugin, BasePlugin):
|
|||||||
template_service: TemplateService = None,
|
template_service: TemplateService = None,
|
||||||
wiki_service: WikiService = None,
|
wiki_service: WikiService = None,
|
||||||
assets_service: AssetsService = None,
|
assets_service: AssetsService = None,
|
||||||
|
search_service: SearchServices = None,
|
||||||
):
|
):
|
||||||
self.wiki_service = wiki_service
|
self.wiki_service = wiki_service
|
||||||
self.template_service = template_service
|
self.template_service = template_service
|
||||||
self.assets_service = assets_service
|
self.assets_service = assets_service
|
||||||
|
self.search_service = search_service
|
||||||
|
|
||||||
@handler(CommandHandler, command="weapon", block=False)
|
@handler(CommandHandler, command="weapon", block=False)
|
||||||
@handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False)
|
@handler(MessageHandler, filters=filters.Regex("^武器查询(.*)"), block=False)
|
||||||
@ -122,8 +126,19 @@ class WeaponPlugin(Plugin, BasePlugin):
|
|||||||
"genshin/weapon/weapon.html", template_data, {"width": 540, "height": 540}, ttl=31 * 24 * 60 * 60
|
"genshin/weapon/weapon.html", template_data, {"width": 540, "height": 540}, ttl=31 * 24 * 60 * 60
|
||||||
)
|
)
|
||||||
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
await message.reply_chat_action(ChatAction.UPLOAD_PHOTO)
|
||||||
await png_data.reply_photo(
|
reply_photo = await png_data.reply_photo(
|
||||||
message,
|
message,
|
||||||
filename=f"{template_data['weapon_name']}.png",
|
filename=f"{template_data['weapon_name']}.png",
|
||||||
allow_sending_without_reply=True,
|
allow_sending_without_reply=True,
|
||||||
)
|
)
|
||||||
|
if reply_photo.photo:
|
||||||
|
photo_file_id = reply_photo.photo[0].file_id
|
||||||
|
tags = _weapons_data.get(weapon_name)
|
||||||
|
entry = WeaponEntry(
|
||||||
|
key=f"plugin:weapon:{weapon_name}",
|
||||||
|
title=weapon_name,
|
||||||
|
description=weapon_data.story,
|
||||||
|
tags=tags,
|
||||||
|
photo_file_id=photo_file_id,
|
||||||
|
)
|
||||||
|
await self.search_service.add_entry(entry)
|
||||||
|
@ -2,13 +2,20 @@ import asyncio
|
|||||||
from typing import cast, Dict, Awaitable, List
|
from typing import cast, Dict, Awaitable, List
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
|
||||||
from telegram import InlineQueryResultArticle, InputTextMessageContent, Update, InlineQuery
|
from telegram import (
|
||||||
|
InlineQueryResultArticle,
|
||||||
|
InputTextMessageContent,
|
||||||
|
Update,
|
||||||
|
InlineQuery,
|
||||||
|
InlineQueryResultCachedPhoto,
|
||||||
|
)
|
||||||
from telegram.constants import ParseMode
|
from telegram.constants import ParseMode
|
||||||
from telegram.error import BadRequest
|
from telegram.error import BadRequest
|
||||||
from telegram.ext import CallbackContext, InlineQueryHandler
|
from telegram.ext import CallbackContext, InlineQueryHandler
|
||||||
|
|
||||||
from core.base.assets import AssetsService, AssetsCouldNotFound
|
from core.base.assets import AssetsService, AssetsCouldNotFound
|
||||||
from core.plugin import handler, Plugin
|
from core.plugin import handler, Plugin
|
||||||
|
from core.search.services import SearchServices
|
||||||
from core.wiki import WikiService
|
from core.wiki import WikiService
|
||||||
from utils.decorators.error import error_callable
|
from utils.decorators.error import error_callable
|
||||||
from utils.log import logger
|
from utils.log import logger
|
||||||
@ -21,12 +28,14 @@ class Inline(Plugin):
|
|||||||
self,
|
self,
|
||||||
wiki_service: WikiService = None,
|
wiki_service: WikiService = None,
|
||||||
assets_service: AssetsService = None,
|
assets_service: AssetsService = None,
|
||||||
|
search_service: SearchServices = None,
|
||||||
):
|
):
|
||||||
self.assets_service = assets_service
|
self.assets_service = assets_service
|
||||||
self.wiki_service = wiki_service
|
self.wiki_service = wiki_service
|
||||||
self.weapons_list: List[Dict[str, str]] = []
|
self.weapons_list: List[Dict[str, str]] = []
|
||||||
self.characters_list: List[Dict[str, str]] = []
|
self.characters_list: List[Dict[str, str]] = []
|
||||||
self.refresh_task: List[Awaitable] = []
|
self.refresh_task: List[Awaitable] = []
|
||||||
|
self.search_service = search_service
|
||||||
|
|
||||||
async def __async_init__(self):
|
async def __async_init__(self):
|
||||||
# todo: 整合进 wiki 或者单独模块 从Redis中读取
|
# todo: 整合进 wiki 或者单独模块 从Redis中读取
|
||||||
@ -74,7 +83,22 @@ class Inline(Plugin):
|
|||||||
results_list = []
|
results_list = []
|
||||||
args = query.split(" ")
|
args = query.split(" ")
|
||||||
if args[0] == "":
|
if args[0] == "":
|
||||||
pass
|
results_list.append(
|
||||||
|
InlineQueryResultArticle(
|
||||||
|
id=str(uuid4()),
|
||||||
|
title="武器图鉴查询",
|
||||||
|
description="输入武器名称即可查询武器图鉴",
|
||||||
|
input_message_content=InputTextMessageContent("武器图鉴查询"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
results_list.append(
|
||||||
|
InlineQueryResultArticle(
|
||||||
|
id=str(uuid4()),
|
||||||
|
title="角色攻略查询",
|
||||||
|
description="输入角色名即可查询角色攻略",
|
||||||
|
input_message_content=InputTextMessageContent("角色攻略查询"),
|
||||||
|
)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if "查看武器列表并查询" == args[0]:
|
if "查看武器列表并查询" == args[0]:
|
||||||
for weapon in self.weapons_list:
|
for weapon in self.weapons_list:
|
||||||
@ -119,6 +143,33 @@ class Inline(Plugin):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
simple_search_results = await self.search_service.search(args[0])
|
||||||
|
if simple_search_results:
|
||||||
|
results_list.append(
|
||||||
|
InlineQueryResultArticle(
|
||||||
|
id=str(uuid4()),
|
||||||
|
title=f"当前查询内容为 {args[0]}",
|
||||||
|
description="如果无查看图片描述 这是正常的 客户端问题",
|
||||||
|
thumb_url="https://www.miyoushe.com/_nuxt/img/game-ys.dfc535b.jpg",
|
||||||
|
input_message_content=InputTextMessageContent(f"当前查询内容为 {args[0]}\n如果无查看图片描述 这是正常的 客户端问题"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
for simple_search_result in simple_search_results:
|
||||||
|
if simple_search_result.photo_file_id:
|
||||||
|
description = simple_search_result.description
|
||||||
|
if len(description) >= 10:
|
||||||
|
description = description[:10]
|
||||||
|
results_list.append(
|
||||||
|
InlineQueryResultCachedPhoto(
|
||||||
|
id=str(uuid4()),
|
||||||
|
title=simple_search_result.title,
|
||||||
|
photo_file_id=simple_search_result.photo_file_id,
|
||||||
|
description=description,
|
||||||
|
caption=simple_search_result.caption,
|
||||||
|
parse_mode=simple_search_result.parse_mode,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
if not results_list:
|
if not results_list:
|
||||||
results_list.append(
|
results_list.append(
|
||||||
|
64
plugins/system/search.py
Normal file
64
plugins/system/search.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import asyncio
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from telegram import Update
|
||||||
|
from telegram.ext import CallbackContext
|
||||||
|
|
||||||
|
from core.plugin import handler, Plugin, job
|
||||||
|
from core.search.services import SearchServices
|
||||||
|
from utils.decorators.admins import bot_admins_rights_check
|
||||||
|
from utils.decorators.restricts import restricts
|
||||||
|
from utils.log import logger
|
||||||
|
|
||||||
|
__all__ = []
|
||||||
|
|
||||||
|
|
||||||
|
class SearchPlugin(Plugin):
|
||||||
|
def __init__(self, search: SearchServices = None):
|
||||||
|
self.search = search
|
||||||
|
self._lock = asyncio.Lock()
|
||||||
|
|
||||||
|
async def __async_init__(self):
|
||||||
|
async def load_data():
|
||||||
|
logger.info("Search 插件模块正在加载搜索条目")
|
||||||
|
async with self._lock:
|
||||||
|
await self.search.load_data()
|
||||||
|
logger.success("Search 插件加载模块搜索条目成功")
|
||||||
|
|
||||||
|
asyncio.create_task(load_data())
|
||||||
|
|
||||||
|
@job.run_repeating(interval=datetime.timedelta(hours=1), name="SaveEntryJob")
|
||||||
|
async def save_entry_job(self, _: CallbackContext):
|
||||||
|
if self._lock.locked():
|
||||||
|
logger.warning("条目数据正在保存 跳过本次定时任务")
|
||||||
|
else:
|
||||||
|
async with self._lock:
|
||||||
|
logger.info("条目数据正在自动保存")
|
||||||
|
await self.search.save_entry()
|
||||||
|
logger.success("条目数据自动保存成功")
|
||||||
|
|
||||||
|
@handler.command("save_entry", block=False)
|
||||||
|
@bot_admins_rights_check
|
||||||
|
@restricts()
|
||||||
|
async def save_entry(self, update: Update, _: CallbackContext):
|
||||||
|
user = update.effective_user
|
||||||
|
message = update.effective_message
|
||||||
|
logger.info("用户 %s[%s] 保存条目数据命令请求", user.full_name, user.id)
|
||||||
|
if self._lock.locked():
|
||||||
|
await message.reply_text("条目数据正在保存 请稍后重试")
|
||||||
|
else:
|
||||||
|
async with self._lock:
|
||||||
|
reply_text = await message.reply_text("正在保存数据")
|
||||||
|
await self.search.save_entry()
|
||||||
|
await reply_text.edit_text("数据保存成功")
|
||||||
|
|
||||||
|
@handler.command("remove_all_entry", block=False)
|
||||||
|
@bot_admins_rights_check
|
||||||
|
@restricts()
|
||||||
|
async def remove_all_entry(self, update: Update, _: CallbackContext):
|
||||||
|
user = update.effective_user
|
||||||
|
message = update.effective_message
|
||||||
|
logger.info("用户 %s[%s] 删除全部条目数据命令请求", user.full_name, user.id)
|
||||||
|
reply_text = await message.reply_text("正在删除全部条目数据")
|
||||||
|
await self.search.remove_all_entry()
|
||||||
|
await reply_text.edit_text("删除全部条目数据成功")
|
51
poetry.lock
generated
51
poetry.lock
generated
@ -131,6 +131,14 @@ typing-extensions = "*"
|
|||||||
[package.extras]
|
[package.extras]
|
||||||
test = ["pytest", "pytest-rerunfailures"]
|
test = ["pytest", "pytest-rerunfailures"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "async-lru"
|
||||||
|
version = "1.0.3"
|
||||||
|
description = "Simple lru_cache for asyncio"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-timeout"
|
name = "async-timeout"
|
||||||
version = "4.0.2"
|
version = "4.0.2"
|
||||||
@ -159,7 +167,7 @@ python-versions = ">=3.5"
|
|||||||
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||||
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"]
|
||||||
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"]
|
||||||
tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
tests_no_zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backports.zoneinfo"
|
name = "backports.zoneinfo"
|
||||||
@ -234,7 +242,7 @@ optional = false
|
|||||||
python-versions = ">=3.6.0"
|
python-versions = ">=3.6.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
unicode-backport = ["unicodedata2"]
|
unicode_backport = ["unicodedata2"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
@ -951,7 +959,7 @@ falcon = ["falcon (>=1.4)"]
|
|||||||
fastapi = ["fastapi (>=0.79.0)"]
|
fastapi = ["fastapi (>=0.79.0)"]
|
||||||
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
|
flask = ["blinker (>=1.1)", "flask (>=0.11)"]
|
||||||
httpx = ["httpx (>=0.16.0)"]
|
httpx = ["httpx (>=0.16.0)"]
|
||||||
pure-eval = ["asttokens", "executing", "pure-eval"]
|
pure_eval = ["asttokens", "executing", "pure-eval"]
|
||||||
pymongo = ["pymongo (>=3.1)"]
|
pymongo = ["pymongo (>=3.1)"]
|
||||||
pyspark = ["pyspark (>=2.4.4)"]
|
pyspark = ["pyspark (>=2.4.4)"]
|
||||||
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
|
quart = ["blinker (>=1.1)", "quart (>=0.16.1)"]
|
||||||
@ -1030,19 +1038,19 @@ aiomysql = ["aiomysql", "greenlet (!=0.4.17)"]
|
|||||||
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
aiosqlite = ["aiosqlite", "greenlet (!=0.4.17)", "typing_extensions (!=3.10.0.1)"]
|
||||||
asyncio = ["greenlet (!=0.4.17)"]
|
asyncio = ["greenlet (!=0.4.17)"]
|
||||||
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
|
asyncmy = ["asyncmy (>=0.2.3,!=0.2.4)", "greenlet (!=0.4.17)"]
|
||||||
mariadb-connector = ["mariadb (>=1.0.1,!=1.1.2)"]
|
mariadb_connector = ["mariadb (>=1.0.1,!=1.1.2)"]
|
||||||
mssql = ["pyodbc"]
|
mssql = ["pyodbc"]
|
||||||
mssql-pymssql = ["pymssql"]
|
mssql_pymssql = ["pymssql"]
|
||||||
mssql-pyodbc = ["pyodbc"]
|
mssql_pyodbc = ["pyodbc"]
|
||||||
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
mypy = ["mypy (>=0.910)", "sqlalchemy2-stubs"]
|
||||||
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
|
mysql = ["mysqlclient (>=1.4.0)", "mysqlclient (>=1.4.0,<2)"]
|
||||||
mysql-connector = ["mysql-connector-python"]
|
mysql_connector = ["mysql-connector-python"]
|
||||||
oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
|
oracle = ["cx_oracle (>=7)", "cx_oracle (>=7,<8)"]
|
||||||
postgresql = ["psycopg2 (>=2.7)"]
|
postgresql = ["psycopg2 (>=2.7)"]
|
||||||
postgresql-asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
postgresql_asyncpg = ["asyncpg", "greenlet (!=0.4.17)"]
|
||||||
postgresql-pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
postgresql_pg8000 = ["pg8000 (>=1.16.6,!=1.29.0)"]
|
||||||
postgresql-psycopg2binary = ["psycopg2-binary"]
|
postgresql_psycopg2binary = ["psycopg2-binary"]
|
||||||
postgresql-psycopg2cffi = ["psycopg2cffi"]
|
postgresql_psycopg2cffi = ["psycopg2cffi"]
|
||||||
pymysql = ["pymysql", "pymysql (<1)"]
|
pymysql = ["pymysql", "pymysql (<1)"]
|
||||||
sqlcipher = ["sqlcipher3_binary"]
|
sqlcipher = ["sqlcipher3_binary"]
|
||||||
|
|
||||||
@ -1093,6 +1101,17 @@ category = "main"
|
|||||||
optional = true
|
optional = true
|
||||||
python-versions = "~=3.7"
|
python-versions = "~=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "thefuzz"
|
||||||
|
version = "0.19.0"
|
||||||
|
description = "Fuzzy string matching in python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
speedup = ["python-levenshtein (>=0.12)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tomli"
|
name = "tomli"
|
||||||
version = "2.0.1"
|
version = "2.0.1"
|
||||||
@ -1274,7 +1293,7 @@ test = ["pytest", "pytest-asyncio", "flaky"]
|
|||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = "^3.8"
|
python-versions = "^3.8"
|
||||||
content-hash = "0c5cb6b738281012aa5ade35a22efc33159c38c4ddf58140c48a148aa70cb912"
|
content-hash = "dda8009b7611580764e09bafcb38c2e32804975869364e017fbea69e38524d73"
|
||||||
|
|
||||||
[metadata.files]
|
[metadata.files]
|
||||||
aiofiles = [
|
aiofiles = [
|
||||||
@ -1398,6 +1417,10 @@ arko-wrapper = [
|
|||||||
{file = "arko-wrapper-0.2.4.tar.gz", hash = "sha256:92b7771c72f3b18d5193b8b4375f1b0f52cb94d3933b00118c281d6e582819ae"},
|
{file = "arko-wrapper-0.2.4.tar.gz", hash = "sha256:92b7771c72f3b18d5193b8b4375f1b0f52cb94d3933b00118c281d6e582819ae"},
|
||||||
{file = "arko_wrapper-0.2.4-py3-none-any.whl", hash = "sha256:703dcfeeb95c43631d8cba02eebbc57973a3f0bdca7353dfc57b38b257a94774"},
|
{file = "arko_wrapper-0.2.4-py3-none-any.whl", hash = "sha256:703dcfeeb95c43631d8cba02eebbc57973a3f0bdca7353dfc57b38b257a94774"},
|
||||||
]
|
]
|
||||||
|
async-lru = [
|
||||||
|
{file = "async-lru-1.0.3.tar.gz", hash = "sha256:c2cb9b2915eb14e6cf3e717154b40f715bf90e596d73623677affd0d1fbcd32a"},
|
||||||
|
{file = "async_lru-1.0.3-py3-none-any.whl", hash = "sha256:ea692c303feb6211ff260d230dae1583636f13e05c9ae616eada77855b7f415c"},
|
||||||
|
]
|
||||||
async-timeout = [
|
async-timeout = [
|
||||||
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
{file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"},
|
||||||
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
{file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"},
|
||||||
@ -2280,6 +2303,10 @@ tgcrypto = [
|
|||||||
{file = "TgCrypto-1.2.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6b0c2dc84e632ce7b3d0b767cfe20967e557ad7d71ea5dbd7df2dd544323181"},
|
{file = "TgCrypto-1.2.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:d6b0c2dc84e632ce7b3d0b767cfe20967e557ad7d71ea5dbd7df2dd544323181"},
|
||||||
{file = "TgCrypto-1.2.5.tar.gz", hash = "sha256:9bc2cac6fb9a12ef5b08f3dd500174fe374d89b660cce981f57e3138559cb682"},
|
{file = "TgCrypto-1.2.5.tar.gz", hash = "sha256:9bc2cac6fb9a12ef5b08f3dd500174fe374d89b660cce981f57e3138559cb682"},
|
||||||
]
|
]
|
||||||
|
thefuzz = [
|
||||||
|
{file = "thefuzz-0.19.0-py2.py3-none-any.whl", hash = "sha256:4fcdde8e40f5ca5e8106bc7665181f9598a9c8b18b0a4d38c41a095ba6788972"},
|
||||||
|
{file = "thefuzz-0.19.0.tar.gz", hash = "sha256:6f7126db2f2c8a54212b05e3a740e45f4291c497d75d20751728f635bb74aa3d"},
|
||||||
|
]
|
||||||
tomli = [
|
tomli = [
|
||||||
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
{file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
|
||||||
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
|
||||||
|
@ -39,6 +39,8 @@ uvicorn = {extras = ["standard"], version = "^0.19.0"}
|
|||||||
sentry-sdk = "^1.11.0"
|
sentry-sdk = "^1.11.0"
|
||||||
GitPython = "^3.1.29"
|
GitPython = "^3.1.29"
|
||||||
openpyxl = "^3.0.10"
|
openpyxl = "^3.0.10"
|
||||||
|
async-lru = "^1.0.3"
|
||||||
|
thefuzz = "^0.19.0"
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
pyro = ["Pyrogram", "TgCrypto"]
|
pyro = ["Pyrogram", "TgCrypto"]
|
||||||
|
Loading…
Reference in New Issue
Block a user