🎉 Begin a project
This commit is contained in:
parent
7b3755ae9e
commit
d89de2e1af
121
.gitignore
vendored
Normal file
121
.gitignore
vendored
Normal file
@ -0,0 +1,121 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
config.ini
|
||||
*session*
|
||||
data/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pycharm
|
||||
.idea/
|
54
ci.py
Normal file
54
ci.py
Normal file
@ -0,0 +1,54 @@
|
||||
import json
|
||||
from configparser import RawConfigParser
|
||||
from os import sep, mkdir
|
||||
from os.path import exists
|
||||
|
||||
import pyromod.listen
|
||||
from pyrogram import Client
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from httpx import AsyncClient, get
|
||||
from sqlitedict import SqliteDict
|
||||
|
||||
if not exists("data"):
|
||||
mkdir("data")
|
||||
sqlite = SqliteDict(f"data{sep}data.sqlite", encode=json.dumps, decode=json.loads, autocommit=True)
|
||||
# data.sqlite 结构如下:
|
||||
# {
|
||||
# "room_id": {
|
||||
# "msg_link": str,
|
||||
# "subscribes": List[订阅id: int],
|
||||
# },
|
||||
# "update_time": str,
|
||||
# }
|
||||
# 读取配置文件
|
||||
config = RawConfigParser()
|
||||
config.read("config.ini")
|
||||
bot_token: str = ""
|
||||
admin_id: int = 0
|
||||
channel_id: int = 0
|
||||
bot_token = config.get("basic", "bot_token", fallback=bot_token)
|
||||
admin_id = config.getint("basic", "admin", fallback=admin_id)
|
||||
channel_id = config.getint("basic", "channel_id", fallback=channel_id)
|
||||
""" Init httpx client """
|
||||
# 使用自定义 UA
|
||||
headers = {
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.132 Safari/537.36"
|
||||
}
|
||||
client = AsyncClient(timeout=10.0, headers=headers, follow_redirects=True)
|
||||
|
||||
|
||||
# 自定义类型
|
||||
class Bot:
|
||||
def __init__(self, data: dict):
|
||||
self.uid = data["id"]
|
||||
self.username = data["username"]
|
||||
self.name = data["first_name"]
|
||||
|
||||
|
||||
me = Bot(get(f"https://api.telegram.org/bot{bot_token}/getme").json()["result"])
|
||||
# 初始化客户端
|
||||
scheduler = AsyncIOScheduler()
|
||||
if not scheduler.running:
|
||||
scheduler.configure(timezone="Asia/ShangHai")
|
||||
scheduler.start()
|
||||
app = Client("bot", bot_token=bot_token)
|
16
config.ini.example
Normal file
16
config.ini.example
Normal file
@ -0,0 +1,16 @@
|
||||
[pyrogram]
|
||||
api_id = 12345
|
||||
api_hash = 0123456789abc0123456789abc
|
||||
|
||||
[basic]
|
||||
bot_token = 111:abc
|
||||
admin = 777000
|
||||
channel_id = 0
|
||||
|
||||
[plugins]
|
||||
root = plugins
|
||||
|
||||
[proxy]
|
||||
enabled = False
|
||||
hostname = 127.0.0.1
|
||||
port = 1080
|
0
defs/decorators.py
Normal file
0
defs/decorators.py
Normal file
19
defs/format_time.py
Normal file
19
defs/format_time.py
Normal file
@ -0,0 +1,19 @@
|
||||
import pytz
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
pytz.timezone("Asia/Shanghai")
|
||||
date_format = "%Y/%m/%d %H:%M:%S"
|
||||
|
||||
|
||||
def strf_time(data: int) -> str:
|
||||
# data = 1648111686000
|
||||
ts = datetime.fromtimestamp(data/1000)
|
||||
return ts.strftime(date_format)
|
||||
|
||||
|
||||
def now_time() -> str:
|
||||
# UTC
|
||||
ts = datetime.utcnow()
|
||||
# UTC+8
|
||||
ts = ts + timedelta(hours=8)
|
||||
return ts.strftime(date_format)
|
48
defs/msg.py
Normal file
48
defs/msg.py
Normal file
@ -0,0 +1,48 @@
|
||||
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from defs.utils import Vtuber, TrackMessage
|
||||
from defs.thumbnail import thumb
|
||||
from ci import me
|
||||
|
||||
template = """
|
||||
{}
|
||||
|
||||
<b>{}</b> 正在直播
|
||||
|
||||
<b>标题:</b><code>{}</code>
|
||||
<b>人气值:</b><code>{}</code>
|
||||
<b>开播时间:</b><code>{}</code>
|
||||
|
||||
@DD_YTbs_Live_Tracker | @DD_YTbs_Bot
|
||||
"""
|
||||
|
||||
|
||||
def gen_button(data: Vtuber) -> InlineKeyboardMarkup:
|
||||
data_ = [[InlineKeyboardButton("🔗️ 观看", url=data.room_link)],
|
||||
[InlineKeyboardButton("主页", url=data.space_link),
|
||||
InlineKeyboardButton(
|
||||
"订阅",
|
||||
url=f"https://t.me/{me.username}?start={data.mid}"), ]
|
||||
]
|
||||
return InlineKeyboardMarkup(data_)
|
||||
|
||||
|
||||
def format_text(text: str) -> str:
|
||||
text = text.strip()
|
||||
for i in ["/", " ", "-", "@", "(", ]:
|
||||
text = text.replace(i, "_")
|
||||
for i in ["【", "】", "[", "]", "!", "(", ")", "`", "!", ]:
|
||||
text = text.replace(i, "")
|
||||
return text.strip()
|
||||
|
||||
|
||||
def gen_tags(data: Vtuber) -> str:
|
||||
return f"#id{data.mid} #{format_text(data.name.split()[0])} "
|
||||
|
||||
|
||||
async def gen_update_msg(data: Vtuber) -> TrackMessage:
|
||||
text = template.format(gen_tags(data), data.name, data.title, data.online,
|
||||
data.liveStartTimeStr,)
|
||||
button = gen_button(data)
|
||||
img = await thumb(data.face, data.title, data.name)
|
||||
return TrackMessage(text, button, img)
|
84
defs/source.py
Normal file
84
defs/source.py
Normal file
@ -0,0 +1,84 @@
|
||||
from os import sep
|
||||
from os.path import exists
|
||||
from shutil import copyfile
|
||||
from typing import List, Optional
|
||||
|
||||
from ci import client, sqlite
|
||||
from json import load
|
||||
from defs.format_time import now_time
|
||||
from defs.utils import Vtuber
|
||||
|
||||
vtubers_info: dict[int:Vtuber] = {}
|
||||
new_vtubers: List[int] = []
|
||||
old_vtubers: List[int] = []
|
||||
if exists(f"data{sep}info.json"):
|
||||
with open(f"data{sep}info.json", "r", encoding="utf-8") as file:
|
||||
temp_data = load(file)
|
||||
for temp in temp_data:
|
||||
temp_data_ = Vtuber(temp)
|
||||
vtubers_info[temp_data_.room_id] = temp_data_
|
||||
if exists(f"data{sep}vtubers.json"):
|
||||
with open(f"data{sep}vtubers.json", "r", encoding="utf-8") as file:
|
||||
temp_data = load(file)
|
||||
new_vtubers = temp_data
|
||||
if exists(f"data{sep}old_vtubers.json"):
|
||||
with open(f"data{sep}old_vtubers.json", "r", encoding="utf-8") as file:
|
||||
temp_data = load(file)
|
||||
old_vtubers = temp_data
|
||||
|
||||
|
||||
async def update_data() -> None:
|
||||
global new_vtubers, old_vtubers
|
||||
if exists(f"data{sep}vtubers.json"):
|
||||
copyfile(f"data{sep}vtubers.json", f"data{sep}old_vtubers.json")
|
||||
data = await client.get("https://api.tokyo.vtbs.moe/v1/living")
|
||||
with open(f"data{sep}vtubers.json", "w", encoding="utf-8") as f:
|
||||
f.write(data.text)
|
||||
data = data.json()
|
||||
old_vtubers = new_vtubers
|
||||
new_vtubers = data
|
||||
sqlite["update_time"] = now_time()
|
||||
|
||||
|
||||
async def update_info() -> None:
|
||||
global vtubers_info
|
||||
data = await client.get("https://api.tokyo.vtbs.moe/v1/fullInfo")
|
||||
with open(f"data{sep}info.json", "w", encoding="utf-8") as f:
|
||||
f.write(data.text)
|
||||
data = data.json()
|
||||
for i in data:
|
||||
data_ = Vtuber(i)
|
||||
vtubers_info[data_.room_id] = data_
|
||||
|
||||
|
||||
def compare() -> List[Vtuber]:
|
||||
data = []
|
||||
for i in new_vtubers:
|
||||
if i not in old_vtubers:
|
||||
data.append(vtubers_info[i])
|
||||
return data
|
||||
|
||||
|
||||
def from_name_to_v(name: str) -> Optional[Vtuber]:
|
||||
try:
|
||||
data = int(name)
|
||||
except ValueError:
|
||||
return None
|
||||
return vtubers_info.get(data, None)
|
||||
|
||||
|
||||
def from_list_to_name(data: List) -> str:
|
||||
data_ = ""
|
||||
for i in data:
|
||||
v = vtubers_info.get(int(i), None)
|
||||
if isinstance(v, Vtuber):
|
||||
data_ += f"\n{v.name}"
|
||||
return data_
|
||||
|
||||
|
||||
def from_keyword_to_v(keyword: str) -> Optional[Vtuber]:
|
||||
for value in vtubers_info.values():
|
||||
data = str(value.mid) + value.name + str(value.room_id)
|
||||
if keyword in data:
|
||||
return value
|
||||
return None
|
100
defs/subs.py
Normal file
100
defs/subs.py
Normal file
@ -0,0 +1,100 @@
|
||||
import traceback
|
||||
from asyncio import sleep
|
||||
from random import uniform
|
||||
|
||||
from pyrogram.errors import FloodWait, ButtonUrlInvalid, BadRequest
|
||||
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from ci import app, sqlite, me
|
||||
from defs.source import from_list_to_name
|
||||
from defs.utils import Vtuber
|
||||
|
||||
subs_msg = """
|
||||
<b>{} 正在直播!</b>
|
||||
|
||||
<b>标题:</b><code>{}</code>
|
||||
<b>人气值:</b><code>{}</code>
|
||||
<b>开播时间:</b><code>{}</code>
|
||||
"""
|
||||
subs_list_msg = """
|
||||
<b>您订阅了:</b>{}
|
||||
"""
|
||||
subs_list_no_msg = """
|
||||
<b>您订阅了个寂寞!</b>
|
||||
"""
|
||||
|
||||
|
||||
def gen_subs_button(data: Vtuber, link: str) -> InlineKeyboardMarkup:
|
||||
data_ = [[InlineKeyboardButton("详情", url=link),
|
||||
InlineKeyboardButton("退订",
|
||||
url=f"https://t.me/{me.username}?start=un-{data.mid}"), ]]
|
||||
return InlineKeyboardMarkup(data_)
|
||||
|
||||
|
||||
def gen_back_button() -> InlineKeyboardMarkup:
|
||||
return InlineKeyboardMarkup([[InlineKeyboardButton("返回", callback_data="help"), ]])
|
||||
|
||||
|
||||
def gen_subs_msg(cid: int) -> str:
|
||||
data_ = []
|
||||
for key, value in sqlite.items():
|
||||
if key == "update_time":
|
||||
continue
|
||||
data = value.get("subscribes", [])
|
||||
if cid in data:
|
||||
data_.append(key)
|
||||
if data_:
|
||||
text = subs_list_msg.format(from_list_to_name(data_))
|
||||
else:
|
||||
text = subs_list_no_msg
|
||||
return text
|
||||
|
||||
|
||||
async def send_subs_msg(cid: int, data: Vtuber, link: str):
|
||||
return await app.send_message(cid,
|
||||
subs_msg.format(data.name, data.title,
|
||||
data.online, data.liveStartTimeStr),
|
||||
reply_markup=gen_subs_button(data, link))
|
||||
|
||||
|
||||
async def send_to_subscribes(data: Vtuber):
|
||||
users = sqlite.get(str(data.room_id), {}).get("subscribes", [])
|
||||
link = sqlite.get(str(data.room_id), {}).get("msg_link", "https://t.me/DD_YTbs_Live_Tracker")
|
||||
for i in users:
|
||||
try:
|
||||
await send_subs_msg(i, data, link)
|
||||
except FloodWait as e:
|
||||
print(f"Send subscribes msg flood - Sleep for {e.x} second(s)")
|
||||
await sleep(uniform(0.5, 1.0))
|
||||
await send_subs_msg(i, data, link)
|
||||
except ButtonUrlInvalid:
|
||||
print(f"Send button error")
|
||||
await app.send_message(i, subs_msg.format(data.name, data.title,
|
||||
data.online, data.liveStartTimeStr), )
|
||||
except BadRequest:
|
||||
users.remove(i)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
sqlite[str(data.room_id)]["subscribes"] = users
|
||||
|
||||
|
||||
def add_to_subs(cid: int, data: Vtuber) -> bool:
|
||||
users = sqlite.get(str(data.room_id), {}).get("subscribes", [])
|
||||
if cid not in users:
|
||||
users.append(cid)
|
||||
data_ = sqlite.get(str(data.room_id), {"subscribes": []})
|
||||
data_["subscribes"] = users
|
||||
sqlite[str(data.room_id)] = data_
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def remove_from_subs(cid: int, data: Vtuber) -> bool:
|
||||
users = sqlite.get(str(data.room_id), {}).get("subscribes", [])
|
||||
if cid in users:
|
||||
users.remove(cid)
|
||||
data_ = sqlite.get(str(data.room_id), {"subscribes": []})
|
||||
data_["subscribes"] = users
|
||||
sqlite[str(data.room_id)] = data_
|
||||
return True
|
||||
return False
|
57
defs/thumbnail.py
Normal file
57
defs/thumbnail.py
Normal file
@ -0,0 +1,57 @@
|
||||
import os
|
||||
from io import BytesIO
|
||||
from ci import client
|
||||
from PIL import (
|
||||
Image,
|
||||
ImageDraw,
|
||||
ImageFont,
|
||||
)
|
||||
|
||||
|
||||
def changeImageSize(maxWidth, maxHeight, image):
|
||||
if image.size[0] == image.size[1]:
|
||||
# Does not change the scale of the orientation image and displays it centered.
|
||||
# It may look even better
|
||||
newImage = image.resize((maxHeight, maxHeight))
|
||||
img = Image.new("RGBA", (maxWidth, maxHeight))
|
||||
img.paste(newImage, (int((maxWidth - maxHeight) / 2), 0))
|
||||
return img
|
||||
else:
|
||||
widthRatio = maxWidth / image.size[0]
|
||||
heightRatio = maxHeight / image.size[1]
|
||||
newWidth = int(widthRatio * image.size[0])
|
||||
newHeight = int(heightRatio * image.size[1])
|
||||
newImage = image.resize((newWidth, newHeight))
|
||||
return newImage
|
||||
|
||||
|
||||
async def thumb(thumbnail, title, ctitle):
|
||||
resp = await client.get(thumbnail)
|
||||
if resp.status_code == 200:
|
||||
image1 = Image.open(BytesIO(resp.content))
|
||||
else:
|
||||
return None
|
||||
image2 = Image.open(f"source{os.sep}LightGreen.png")
|
||||
image3 = changeImageSize(1280, 720, image1)
|
||||
image4 = changeImageSize(1280, 720, image2)
|
||||
image5 = image3.convert("RGBA")
|
||||
image6 = image4.convert("RGBA")
|
||||
Image.alpha_composite(image5, image6).save(f"data{os.sep}temp.png")
|
||||
img = Image.open(f"data{os.sep}temp.png")
|
||||
draw = ImageDraw.Draw(img)
|
||||
font = ImageFont.truetype(f"source{os.sep}SourceHanSansCN-Regular-2.otf", 50)
|
||||
font2 = ImageFont.truetype(f"source{os.sep}SourceHanSansCN-Medium-2.otf", 72)
|
||||
draw.text(
|
||||
(25, 615),
|
||||
f"{title[:20]}...",
|
||||
fill="black",
|
||||
font=font2,
|
||||
)
|
||||
draw.text(
|
||||
(27, 543),
|
||||
f"{ctitle[:12]} 正在直播",
|
||||
fill="black",
|
||||
font=font,
|
||||
)
|
||||
img.save(f"data{os.sep}final.png")
|
||||
return f"data{os.sep}final.png"
|
28
defs/utils.py
Normal file
28
defs/utils.py
Normal file
@ -0,0 +1,28 @@
|
||||
from typing import Optional
|
||||
from defs.format_time import strf_time
|
||||
|
||||
|
||||
class Vtuber:
|
||||
def __init__(self, data: dict):
|
||||
self.name: str = data["uname"]
|
||||
self.mid: int = data["mid"]
|
||||
self.space_link: str = f"https://space.bilibili.com/{self.mid}"
|
||||
self.title: str = data["title"]
|
||||
self.room_id: str = data["roomid"]
|
||||
self.room_link: str = f"https://live.bilibili.com/{self.room_id}"
|
||||
self.face: str = data["face"]
|
||||
self.follower: int = data["follower"]
|
||||
self.liveStatus: bool = data["liveStatus"]
|
||||
self.online: Optional[int, bool] = data["online"]
|
||||
self.notice: str = data["notice"].replace(r"\n", "\n")
|
||||
self.time: int = data["time"]
|
||||
self.timeStr: str = strf_time(self.time)
|
||||
self.liveStartTime: int = data["liveStartTime"]
|
||||
self.liveStartTimeStr: str = strf_time(self.liveStartTime)
|
||||
|
||||
|
||||
class TrackMessage:
|
||||
def __init__(self, text, button, img=None):
|
||||
self.text = text
|
||||
self.button = button
|
||||
self.img = img
|
6
main.py
Normal file
6
main.py
Normal file
@ -0,0 +1,6 @@
|
||||
import logging
|
||||
from ci import app
|
||||
|
||||
# 日志记录
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
app.run()
|
26
plugins/callback.py
Normal file
26
plugins/callback.py
Normal file
@ -0,0 +1,26 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import CallbackQuery, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from defs.subs import gen_subs_msg, gen_back_button
|
||||
from plugins.help import help_msg
|
||||
|
||||
|
||||
@Client.on_callback_query(filters.regex("help"))
|
||||
async def help_set(_, query: CallbackQuery):
|
||||
await query.edit_message_text(
|
||||
help_msg,
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("订阅", callback_data="subs")]]
|
||||
),
|
||||
disable_web_page_preview=True,
|
||||
)
|
||||
|
||||
|
||||
@Client.on_callback_query(filters.regex("subs"))
|
||||
async def subs_set(_, query: CallbackQuery):
|
||||
text = gen_subs_msg(query.from_user.id)
|
||||
await query.edit_message_text(
|
||||
text,
|
||||
reply_markup=gen_back_button(),
|
||||
disable_web_page_preview=True,
|
||||
)
|
30
plugins/help.py
Normal file
30
plugins/help.py
Normal file
@ -0,0 +1,30 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
help_msg = """
|
||||
下面是我学会了的指令列表:
|
||||
|
||||
👩🏻💼 » /subscribe <code>space_id|昵称|room_id</code> - 订阅直播间
|
||||
<code>/subscribe 5659864</code>
|
||||
<code>/subscribe 鹿野灸</code>
|
||||
<code>/subscribe 2064239</code>
|
||||
|
||||
👩🏻💼 » /unsubscribe <code>space_id|昵称|room_id</code> - 取消订阅直播间
|
||||
|
||||
👩🏻💼 » /subscription - 列出您当前的订阅
|
||||
|
||||
👩🏻💼 » /info <code>space_id|昵称|room_id</code> - 查询主播信息
|
||||
"""
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private &
|
||||
filters.command(["help"]))
|
||||
async def help_command(_: Client, message: Message):
|
||||
await message.reply(
|
||||
help_msg,
|
||||
reply_markup=InlineKeyboardMarkup(
|
||||
[[InlineKeyboardButton("订阅", callback_data="subs")]]
|
||||
),
|
||||
disable_web_page_preview=True,
|
||||
quote=True,
|
||||
)
|
56
plugins/info.py
Normal file
56
plugins/info.py
Normal file
@ -0,0 +1,56 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
|
||||
from ci import sqlite, me
|
||||
from defs.source import from_keyword_to_v
|
||||
from defs.utils import Vtuber
|
||||
from plugins.start import not_found_msg
|
||||
|
||||
info_help_msg = """
|
||||
👩🏻💼 » /info <code>space_id|昵称|room_id</code> - 查询模块信息
|
||||
<code>/info 5659864</code>
|
||||
<code>/info 鹿野灸</code>
|
||||
<code>/info 2064239</code>
|
||||
"""
|
||||
vtuber_msg = """
|
||||
<b>{}</b>
|
||||
|
||||
<b>粉丝数:</b><code>{}</code>
|
||||
<b>更新时间:</b><code>{}</code>
|
||||
<b>通知:</b>
|
||||
|
||||
<code>{}</code>
|
||||
|
||||
@DD_YTbs_Live_Tracker | @DD_YTbs_Bot
|
||||
"""
|
||||
|
||||
|
||||
def gen_info_button(data: Vtuber) -> InlineKeyboardMarkup:
|
||||
msg_link = sqlite.get(str(data.room_id), {}).get("msg_link", "https://t.me/DD_YTbs_Live_Tracker")
|
||||
data_ = [[InlineKeyboardButton("详情", url=msg_link),
|
||||
InlineKeyboardButton("订阅",
|
||||
url=f"https://t.me/{me.username}?start={data.mid}"), ]]
|
||||
return InlineKeyboardMarkup(data_)
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private &
|
||||
filters.command(["info"]))
|
||||
async def info_command(_: Client, message: Message):
|
||||
if len(message.command) == 1:
|
||||
await message.reply(info_help_msg, quote=True)
|
||||
else:
|
||||
data = " ".join(message.command[1:])
|
||||
v = from_keyword_to_v(data)
|
||||
if v:
|
||||
await message.reply(
|
||||
vtuber_msg.format(
|
||||
v.name,
|
||||
v.follower,
|
||||
v.timeStr,
|
||||
v.notice,
|
||||
),
|
||||
reply_markup=gen_info_button(v),
|
||||
quote=True,
|
||||
)
|
||||
else:
|
||||
await message.reply(not_found_msg.format(data), quote=True)
|
57
plugins/inline.py
Normal file
57
plugins/inline.py
Normal file
@ -0,0 +1,57 @@
|
||||
from pyrogram import Client, emoji
|
||||
from pyrogram.types import InlineQuery, InputTextMessageContent, InlineQueryResultArticle
|
||||
|
||||
from defs.source import vtubers_info
|
||||
from plugins.info import vtuber_msg, gen_info_button
|
||||
|
||||
|
||||
@Client.on_inline_query()
|
||||
async def inline_process(_: Client, query: InlineQuery):
|
||||
data = []
|
||||
text = query.query.split()
|
||||
nums = 0
|
||||
if not vtubers_info:
|
||||
return
|
||||
data_ = vtubers_info
|
||||
for key, v in data_.items():
|
||||
if len(text) == 0:
|
||||
data.append(InlineQueryResultArticle(
|
||||
v.name,
|
||||
InputTextMessageContent(vtuber_msg.format(
|
||||
v.name,
|
||||
v.follower,
|
||||
v.timeStr,
|
||||
v.notice,
|
||||
)),
|
||||
reply_markup=gen_info_button(v),
|
||||
))
|
||||
nums += 1
|
||||
else:
|
||||
name = str(v.mid) + v.name + str(v.room_id)
|
||||
skip = False
|
||||
for i in text:
|
||||
if i not in name:
|
||||
skip = True
|
||||
if not skip:
|
||||
data.append(InlineQueryResultArticle(
|
||||
v.name,
|
||||
InputTextMessageContent(vtuber_msg.format(
|
||||
v.name,
|
||||
v.follower,
|
||||
v.timeStr,
|
||||
v.notice,
|
||||
)),
|
||||
reply_markup=gen_info_button(v),
|
||||
))
|
||||
nums += 1
|
||||
if nums >= 50:
|
||||
break
|
||||
if nums == 0:
|
||||
return await query.answer(
|
||||
results=[],
|
||||
switch_pm_text=f'{emoji.CROSS_MARK} 字符串 "{" ".join(text)}" 没有搜索到任何结果',
|
||||
switch_pm_parameter="help",
|
||||
)
|
||||
await query.answer(data,
|
||||
switch_pm_text=f'{emoji.KEY} 搜索了 {len(vtubers_info.values())} 个 Vtuber',
|
||||
switch_pm_parameter="help", )
|
8
plugins/ping.py
Normal file
8
plugins/ping.py
Normal file
@ -0,0 +1,8 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private &
|
||||
filters.command(["ping", ]))
|
||||
async def ping_check(_: Client, message: Message):
|
||||
await message.reply("poi ~", quote=True)
|
76
plugins/start.py
Normal file
76
plugins/start.py
Normal file
@ -0,0 +1,76 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from ci import me
|
||||
from defs.source import from_name_to_v
|
||||
from defs.subs import add_to_subs, remove_from_subs
|
||||
|
||||
des = """
|
||||
你好!{} 我是 [{}]({}),一个为 BiliBili Vtuber 用户打造的一体化机器人!
|
||||
我可以帮助你获取 BiliBili Vtuber 的开播提醒和信息查询!
|
||||
|
||||
点击下面的帮助按钮来查看使用方法。
|
||||
加入 [我的频道](https://t.me/DD_YTbs_Live_Tracker) 获取关于 BiliBili Vtuber 的所有开播提醒和公告!
|
||||
"""
|
||||
unsub_msg = """
|
||||
<b>成功退订了</b> <code>{}</code> <b>的开播提醒!</b>
|
||||
"""
|
||||
not_sub_msg = """
|
||||
<b>你好像没有订阅</b> <code>{}</code> <b>的开播提醒!</b>
|
||||
"""
|
||||
sub_msg = """
|
||||
<b>成功订阅了</b> <code>{}</code> <b>的开播提醒!</b>
|
||||
"""
|
||||
already_sub_msg = """
|
||||
<b>已经订阅过</b> <code>{}</code> <b>的开播提醒!</b>
|
||||
"""
|
||||
not_found_msg = """
|
||||
<b>没有找到名为</b> <code>{}</code> <b>的 Vtuber!</b>
|
||||
"""
|
||||
|
||||
|
||||
def gen_help_button() -> InlineKeyboardMarkup:
|
||||
data_ = [[InlineKeyboardButton("📢 官方频道", url="https://t.me/DD_YTbs_Live_Tracker"),
|
||||
InlineKeyboardButton("💬 官方群组", url="https://t.me/Invite_Challenge_Bot?start=1"), ],
|
||||
[InlineKeyboardButton("❓ 阅读帮助", callback_data="help")],
|
||||
]
|
||||
return InlineKeyboardMarkup(data_)
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private &
|
||||
filters.command(["start"]))
|
||||
async def start_command(_: Client, message: Message):
|
||||
"""
|
||||
回应消息
|
||||
"""
|
||||
if len(message.command) == 1:
|
||||
await message.reply(des.format(message.from_user.mention(),
|
||||
me.name,
|
||||
f"https://t.me/{me.username}"),
|
||||
reply_markup=gen_help_button(),
|
||||
quote=True, )
|
||||
else:
|
||||
data = message.command[1]
|
||||
if data.startswith("un-"):
|
||||
# 退订
|
||||
name = data[3:]
|
||||
data = from_name_to_v(name)
|
||||
if data:
|
||||
success = remove_from_subs(message.from_user.id, data)
|
||||
if success:
|
||||
await message.reply(unsub_msg.format(data.name), quote=True)
|
||||
else:
|
||||
await message.reply(not_sub_msg.format(data.name), quote=True)
|
||||
else:
|
||||
await message.reply(not_found_msg.format(name), quote=True)
|
||||
else:
|
||||
# 订阅
|
||||
name = data
|
||||
data = from_name_to_v(data)
|
||||
if data:
|
||||
success = add_to_subs(message.from_user.id, data)
|
||||
if success:
|
||||
await message.reply(sub_msg.format(data.name), quote=True)
|
||||
else:
|
||||
await message.reply(already_sub_msg.format(data.name), quote=True)
|
||||
else:
|
||||
await message.reply(not_found_msg.format(name), quote=True)
|
62
plugins/subs.py
Normal file
62
plugins/subs.py
Normal file
@ -0,0 +1,62 @@
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message
|
||||
|
||||
from defs.source import from_keyword_to_v
|
||||
from defs.subs import gen_subs_msg, gen_back_button, add_to_subs, remove_from_subs
|
||||
from plugins.start import sub_msg, not_found_msg, already_sub_msg, unsub_msg, not_sub_msg
|
||||
|
||||
sub_help_msg = """
|
||||
👩🏻💼 » /subscribe <code>space_id|昵称|room_id</code> - 订阅直播间
|
||||
<code>/subscribe 5659864</code>
|
||||
<code>/subscribe 鹿野灸</code>
|
||||
<code>/subscribe 2064239</code>
|
||||
"""
|
||||
unsub_help_msg = """
|
||||
👩🏻💼 » /unsubscribe <code>space_id|昵称|room_id</code> - 取消订阅直播间
|
||||
<code>/unsubscribe 5659864</code>
|
||||
<code>/unsubscribe 鹿野灸</code>
|
||||
<code>/unsubscribe 2064239</code>
|
||||
"""
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private &
|
||||
filters.command(["subscription"]))
|
||||
async def subscription_command(_: Client, message: Message):
|
||||
text = gen_subs_msg(message.from_user.id)
|
||||
await message.reply(text, reply_markup=gen_back_button(), quote=True, )
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private &
|
||||
filters.command(["subscribe"]))
|
||||
async def sub_command(_: Client, message: Message):
|
||||
if len(message.command) == 1:
|
||||
await message.reply(sub_help_msg, reply_markup=gen_back_button(), quote=True)
|
||||
else:
|
||||
data = " ".join(message.command[1:])
|
||||
module = from_keyword_to_v(data)
|
||||
if module:
|
||||
success = add_to_subs(message.from_user.id, module)
|
||||
if success:
|
||||
await message.reply(sub_msg.format(module.name), quote=True)
|
||||
else:
|
||||
await message.reply(already_sub_msg.format(module.name), quote=True)
|
||||
else:
|
||||
await message.reply(not_found_msg.format(data), quote=True)
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private &
|
||||
filters.command(["unsubscribe"]))
|
||||
async def un_sub_command(_: Client, message: Message):
|
||||
if len(message.command) == 1:
|
||||
await message.reply(unsub_help_msg, reply_markup=gen_back_button(), quote=True)
|
||||
else:
|
||||
data = " ".join(message.command[1:])
|
||||
module = from_keyword_to_v(data)
|
||||
if module:
|
||||
success = remove_from_subs(message.from_user.id, module)
|
||||
if success:
|
||||
await message.reply(unsub_msg.format(module.name), quote=True)
|
||||
else:
|
||||
await message.reply(not_sub_msg.format(module.name), quote=True)
|
||||
else:
|
||||
await message.reply(not_found_msg.format(data), quote=True)
|
72
plugins/track.py
Normal file
72
plugins/track.py
Normal file
@ -0,0 +1,72 @@
|
||||
import traceback
|
||||
from asyncio import sleep
|
||||
from random import uniform
|
||||
|
||||
from pyrogram.errors import FloodWait, ButtonUrlInvalid
|
||||
from pyrogram.types import Message
|
||||
|
||||
from ci import app, scheduler, channel_id, admin_id, sqlite, client
|
||||
from pyrogram import Client, filters
|
||||
|
||||
from defs.format_time import strf_time, now_time
|
||||
from defs.msg import gen_update_msg
|
||||
from defs.source import update_data, compare, update_info
|
||||
from defs.subs import send_to_subscribes
|
||||
|
||||
|
||||
async def send_track_msg(track_msg, no_button=False) -> Message:
|
||||
button = None if no_button else track_msg.button
|
||||
if track_msg.img:
|
||||
return await app.send_photo(channel_id, track_msg.img, caption=track_msg.text,
|
||||
parse_mode="html",
|
||||
reply_markup=button)
|
||||
return await app.send_message(channel_id, track_msg.text,
|
||||
parse_mode="html",
|
||||
reply_markup=button)
|
||||
|
||||
|
||||
@scheduler.scheduled_job("cron", minute="*/10", id="0")
|
||||
async def run_every_10_minute():
|
||||
await update_data()
|
||||
need_update = compare()
|
||||
for i in need_update:
|
||||
data = (await client.get(f"https://api.tokyo.vtbs.moe/v1/room/{i.room_id}")).json()
|
||||
i.liveStartTime = data["live_time"]
|
||||
if i.liveStartTime == 0:
|
||||
i.liveStartTimeStr = now_time()
|
||||
else:
|
||||
i.liveStartTimeStr = strf_time(i.liveStartTime)
|
||||
track_msg = await gen_update_msg(i)
|
||||
msg = None
|
||||
try:
|
||||
msg = await send_track_msg(track_msg)
|
||||
except FloodWait as e:
|
||||
print(f"Send document flood - Sleep for {e.x} second(s)")
|
||||
await sleep(e.x + uniform(0.5, 1.0))
|
||||
msg = await send_track_msg(track_msg)
|
||||
except ButtonUrlInvalid:
|
||||
print(f"Send button error")
|
||||
msg = await send_track_msg(track_msg, no_button=True)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
await sleep(uniform(0.5, 2.0))
|
||||
data_ = sqlite.get(str(i.room_id), {"msg_link": ""})
|
||||
if msg:
|
||||
data_["msg_link"] = msg.link
|
||||
else:
|
||||
data_["msg_link"] = "https://t.me/DD_YTbs_Live_Tracker"
|
||||
sqlite[str(i.room_id)] = data_
|
||||
await send_to_subscribes(i)
|
||||
await sleep(uniform(0.5, 2.0))
|
||||
|
||||
|
||||
@scheduler.scheduled_job("cron", hour="*/12", id="0")
|
||||
async def run_every_12_hour():
|
||||
await update_info()
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.private & filters.chat(admin_id) &
|
||||
filters.command(["force_update", ]))
|
||||
async def force_update(_: Client, __: Message):
|
||||
await run_every_12_hour()
|
||||
await run_every_10_minute()
|
8
requirements.txt
Normal file
8
requirements.txt
Normal file
@ -0,0 +1,8 @@
|
||||
Pyrogram>=1.4.9
|
||||
Tgcrypto>=1.2.3
|
||||
pyromod
|
||||
httpx>=0.22.0
|
||||
apscheduler>=3.8.1
|
||||
sqlitedict>=2.0.0
|
||||
pytz
|
||||
pillow
|
BIN
source/LightGreen.png
Normal file
BIN
source/LightGreen.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 350 KiB |
BIN
source/SourceHanSansCN-Medium-2.otf
Normal file
BIN
source/SourceHanSansCN-Medium-2.otf
Normal file
Binary file not shown.
BIN
source/SourceHanSansCN-Regular-2.otf
Normal file
BIN
source/SourceHanSansCN-Regular-2.otf
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user