diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..82d185a
--- /dev/null
+++ b/.gitignore
@@ -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/
diff --git a/ci.py b/ci.py
new file mode 100644
index 0000000..bfb86aa
--- /dev/null
+++ b/ci.py
@@ -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)
diff --git a/config.ini.example b/config.ini.example
new file mode 100644
index 0000000..481dbce
--- /dev/null
+++ b/config.ini.example
@@ -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
diff --git a/defs/decorators.py b/defs/decorators.py
new file mode 100644
index 0000000..e69de29
diff --git a/defs/format_time.py b/defs/format_time.py
new file mode 100644
index 0000000..da0ae16
--- /dev/null
+++ b/defs/format_time.py
@@ -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)
diff --git a/defs/msg.py b/defs/msg.py
new file mode 100644
index 0000000..012ce39
--- /dev/null
+++ b/defs/msg.py
@@ -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 = """
+{}
+
+{} 正在直播
+
+标题:{}
+人气值:{}
+开播时间:{}
+
+@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)
diff --git a/defs/source.py b/defs/source.py
new file mode 100644
index 0000000..9cc8d70
--- /dev/null
+++ b/defs/source.py
@@ -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
diff --git a/defs/subs.py b/defs/subs.py
new file mode 100644
index 0000000..759e418
--- /dev/null
+++ b/defs/subs.py
@@ -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 = """
+{} 正在直播!
+
+标题:{}
+人气值:{}
+开播时间:{}
+"""
+subs_list_msg = """
+您订阅了:{}
+"""
+subs_list_no_msg = """
+您订阅了个寂寞!
+"""
+
+
+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
diff --git a/defs/thumbnail.py b/defs/thumbnail.py
new file mode 100644
index 0000000..1f6767d
--- /dev/null
+++ b/defs/thumbnail.py
@@ -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"
diff --git a/defs/utils.py b/defs/utils.py
new file mode 100644
index 0000000..4998315
--- /dev/null
+++ b/defs/utils.py
@@ -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
diff --git a/main.py b/main.py
new file mode 100644
index 0000000..22ac90a
--- /dev/null
+++ b/main.py
@@ -0,0 +1,6 @@
+import logging
+from ci import app
+
+# 日志记录
+logging.basicConfig(level=logging.INFO)
+app.run()
diff --git a/plugins/callback.py b/plugins/callback.py
new file mode 100644
index 0000000..a35ea09
--- /dev/null
+++ b/plugins/callback.py
@@ -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,
+ )
diff --git a/plugins/help.py b/plugins/help.py
new file mode 100644
index 0000000..e69bcfa
--- /dev/null
+++ b/plugins/help.py
@@ -0,0 +1,30 @@
+from pyrogram import Client, filters
+from pyrogram.types import Message, InlineKeyboardMarkup, InlineKeyboardButton
+
+help_msg = """
+下面是我学会了的指令列表:
+
+👩🏻💼 » /subscribe space_id|昵称|room_id
- 订阅直播间
+ /subscribe 5659864
+ /subscribe 鹿野灸
+ /subscribe 2064239
+
+👩🏻💼 » /unsubscribe space_id|昵称|room_id
- 取消订阅直播间
+
+👩🏻💼 » /subscription - 列出您当前的订阅
+
+👩🏻💼 » /info space_id|昵称|room_id
- 查询主播信息
+"""
+
+
+@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,
+ )
diff --git a/plugins/info.py b/plugins/info.py
new file mode 100644
index 0000000..8d47f0d
--- /dev/null
+++ b/plugins/info.py
@@ -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 space_id|昵称|room_id
- 查询模块信息
+ /info 5659864
+ /info 鹿野灸
+ /info 2064239
+"""
+vtuber_msg = """
+{}
+
+粉丝数:{}
+更新时间:{}
+通知:
+
+{}
+
+@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)
diff --git a/plugins/inline.py b/plugins/inline.py
new file mode 100644
index 0000000..1632960
--- /dev/null
+++ b/plugins/inline.py
@@ -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", )
diff --git a/plugins/ping.py b/plugins/ping.py
new file mode 100644
index 0000000..bd762b7
--- /dev/null
+++ b/plugins/ping.py
@@ -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)
diff --git a/plugins/start.py b/plugins/start.py
new file mode 100644
index 0000000..cb8ea2c
--- /dev/null
+++ b/plugins/start.py
@@ -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 = """
+成功退订了 {}
的开播提醒!
+"""
+not_sub_msg = """
+你好像没有订阅 {}
的开播提醒!
+"""
+sub_msg = """
+成功订阅了 {}
的开播提醒!
+"""
+already_sub_msg = """
+已经订阅过 {}
的开播提醒!
+"""
+not_found_msg = """
+没有找到名为 {}
的 Vtuber!
+"""
+
+
+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)
diff --git a/plugins/subs.py b/plugins/subs.py
new file mode 100644
index 0000000..610996d
--- /dev/null
+++ b/plugins/subs.py
@@ -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 space_id|昵称|room_id
- 订阅直播间
+ /subscribe 5659864
+ /subscribe 鹿野灸
+ /subscribe 2064239
+"""
+unsub_help_msg = """
+👩🏻💼 » /unsubscribe space_id|昵称|room_id
- 取消订阅直播间
+ /unsubscribe 5659864
+ /unsubscribe 鹿野灸
+ /unsubscribe 2064239
+"""
+
+
+@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)
diff --git a/plugins/track.py b/plugins/track.py
new file mode 100644
index 0000000..6549109
--- /dev/null
+++ b/plugins/track.py
@@ -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()
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..ca2712b
--- /dev/null
+++ b/requirements.txt
@@ -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
diff --git a/source/LightGreen.png b/source/LightGreen.png
new file mode 100644
index 0000000..db7e0ff
Binary files /dev/null and b/source/LightGreen.png differ
diff --git a/source/SourceHanSansCN-Medium-2.otf b/source/SourceHanSansCN-Medium-2.otf
new file mode 100644
index 0000000..53e03e0
Binary files /dev/null and b/source/SourceHanSansCN-Medium-2.otf differ
diff --git a/source/SourceHanSansCN-Regular-2.otf b/source/SourceHanSansCN-Regular-2.otf
new file mode 100644
index 0000000..886f82f
Binary files /dev/null and b/source/SourceHanSansCN-Regular-2.otf differ