diff --git a/.gitignore b/.gitignore index 11614af..82d185a 100644 --- a/.gitignore +++ b/.gitignore @@ -95,6 +95,9 @@ venv/ ENV/ env.bak/ venv.bak/ +config.ini +*session* +data/ # Spyder project settings .spyderproject @@ -113,3 +116,6 @@ dmypy.json # Pyre type checker .pyre/ + +# pycharm +.idea/ diff --git a/ci.py b/ci.py new file mode 100644 index 0000000..41c3a9c --- /dev/null +++ b/ci.py @@ -0,0 +1,52 @@ +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", autocommit=True) +# data.sqlite 结构如下: +# { +# "module 名称": { +# "subscribes": [订阅id], +# }, +# "update_time": "", +# } +# 读取配置文件 +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) + + +# 自定义类型 +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/format_time.py b/defs/format_time.py new file mode 100644 index 0000000..9f2fe30 --- /dev/null +++ b/defs/format_time.py @@ -0,0 +1,19 @@ +from datetime import datetime, timedelta + +date_format = "%Y-%m-%dT%H:%M:%SZ" + + +def strf_time(data: str) -> str: + # data = "2021-07-17T09:14:05Z" + ts = datetime.strptime(data, date_format) + # UTC+8 + ts = ts + timedelta(hours=8) + return ts.strftime("%Y/%m/%d %H:%M:%S") + + +def now_time() -> str: + # UTC + ts = datetime.utcnow() + # UTC+8 + ts = ts + timedelta(hours=8) + return ts.strftime("%Y/%m/%d %H:%M:%S") diff --git a/defs/msg.py b/defs/msg.py new file mode 100644 index 0000000..eadb36a --- /dev/null +++ b/defs/msg.py @@ -0,0 +1,50 @@ +from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton + +from defs.utils import Module, TrackMessage +from ci import me + +template = """ +{} + +模块:{} +简介:{} +版本:{} +更新时间:{} +更新日志: + +{} + +@lsposed_Modules_Updates_Tracker | @lsposed_Geeks_Bot +""" + + +def gen_button(data: Module) -> InlineKeyboardMarkup: + data_ = [] + if data.releases: + if data.releases[0].releaseAssets: + data_ = [[InlineKeyboardButton("⬇️ 下载", url=data.releases[0].releaseAssets[0].url)]] + data_.extend([[InlineKeyboardButton("Release", url=data.releases[0].url), + InlineKeyboardButton("主页", url=data.homepageUrl), + InlineKeyboardButton( + "订阅", + url=f"https://t.me/{me.username}?start={data.name.replace('.', '_')}"),]]) + return InlineKeyboardMarkup(data_) + + +def gen_tags(data: Module) -> str: + text = f"#{data.description.split()[0]} " + text += f"#{data.collaborators[0]} " + return text + + +def gen_update_msg(data: Module) -> TrackMessage: + text = template.format(gen_tags(data), data.name, data.description, data.latestRelease, + data.updatedAt, + data.releases[0].description.replace(r"\r\n", "\n")) + url = None + if data.releases: + if data.releases[0].releaseAssets: + url = data.releases[0].releaseAssets[0].url + name = data.name.replace('.', '_') + "-" + data.latestRelease + button = gen_button(data) + return TrackMessage(text, url, name, button) diff --git a/defs/source.py b/defs/source.py new file mode 100644 index 0000000..bc76464 --- /dev/null +++ b/defs/source.py @@ -0,0 +1,50 @@ +from os import sep +from os.path import exists +from shutil import copyfile +from typing import List + +from ci import client, sqlite +from json import load +from defs.format_time import now_time +from defs.utils import Module + +new_modules: List[Module] = [] +old_modules: List[Module] = [] +if exists(f"data{sep}modules.json"): + with open(f"data{sep}modules.json", "r", encoding="utf-8") as file: + new_modules = load(file) +if exists(f"data{sep}old_modules.json"): + with open(f"data{sep}old_modules.json", "r", encoding="utf-8") as file: + old_modules = load(file) + + +async def update_data() -> None: + global new_modules, old_modules + if exists(f"data{sep}modules.json"): + copyfile(f"data{sep}modules.json", f"data{sep}old_modules.json") + data = await client.get("https://modules.lsposed.org/modules.json") + with open(f"data{sep}modules.json", "w", encoding="utf-8") as f: + f.write(data.text) + data = data.json() + old_modules = new_modules + new_modules = [] + for i in data: + new_modules.append(Module(i)) + sqlite["update_time"] = now_time() + + +def compare() -> List[Module]: + data = [] + old_data = {i.name: i.latestRelease for i in old_modules} + for i in new_modules: + if i.latestRelease != old_data.get(i.name, ""): + data.append(i) + return data + + +async def download(url: str, name: str) -> str: + content = await client.get(url) + content = content.content + with open(f"data{sep}{name}", 'wb') as f: + f.write(content) + return f"data{sep}{name}" diff --git a/defs/utils.py b/defs/utils.py new file mode 100644 index 0000000..e58506a --- /dev/null +++ b/defs/utils.py @@ -0,0 +1,58 @@ +from typing import List +from defs.format_time import strf_time + + +class Assets: + def __init__(self, data: dict): + self.name = data["name"] + self.url = data["downloadUrl"] + + +class Release: + def __init__(self, data: dict): + self.name: str = data["name"] + self.url: str = data["url"] + self.description: str = data["description"] + self.publishedAt: str = strf_time(data["publishedAt"]) + self.tagName: str = data["tagName"] + self.isPrerelease: bool = data["isPrerelease"] + assets = [] + if data["releaseAssets"]: + for i in data["releaseAssets"]: + assets.append(Assets(i)) + self.releaseAssets: List[Assets] = assets + self.releaseAssetsLen = len(assets) + + +class Module: + def __init__(self, data: dict): + self.name: str = data["name"] + self.description: str = data["description"] + self.url: str = data["url"] + self.homepageUrl: str = data["homepageUrl"] if data["homepageUrl"] else data["url"] + self.sourceUrl: str = data["sourceUrl"] + self.hide: bool = data["hide"] + self.createdAt: str = strf_time(data["createdAt"]) + self.updatedAt: str = strf_time(data["updatedAt"]) + text = [] + for i in data["collaborators"]: + if i["name"]: + text.append(i["name"]) + else: + text.append(i["login"]) + self.collaborators: List[str] = text + self.latestRelease: str = data["latestRelease"] + releases = [] + if data["releases"]: + for i in data["releases"]: + releases.append(Release(i)) + self.releases: List[Release] = releases + self.summary = data["summary"] + + +class TrackMessage: + def __init__(self, text, url, name, button): + self.text = text + self.url = url + self.name = name + self.button = button 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/ping.py b/plugins/ping.py new file mode 100644 index 0000000..0199fac --- /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 force_update(bot: 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..7574822 --- /dev/null +++ b/plugins/start.py @@ -0,0 +1,22 @@ +from pyrogram import Client, filters +from pyrogram.types import Message +from ci import me + +des = """ +你好!{} 我是 [{}]({}),一个为 lsposed 用户打造的一体化机器人! +我可以帮助你获取最新的 lsposed 模块的下载链接和信息查询! + +点击下面的帮助按钮来查看使用方法。 +加入 [我的频道](https://t.me/lsposed_Modules_Updates_Tracker) 获取关于 lsposed 模块的所有更新和公告! +""" + + +@Client.on_message(filters.incoming & filters.private & + filters.command(["start"])) +async def start_command(client: Client, message: Message): + """ + 回应消息 + """ + await message.reply(des.format(message.from_user.mention(), + me.name, + f"https://t.me/{me.username}"), quote=True,) diff --git a/plugins/track.py b/plugins/track.py new file mode 100644 index 0000000..12e6e93 --- /dev/null +++ b/plugins/track.py @@ -0,0 +1,73 @@ +import traceback +from asyncio import sleep +from os import remove +from random import uniform + +from pyrogram.errors import FloodWait, ButtonUrlInvalid +from pyrogram.types import Message + +from ci import app, scheduler, channel_id, admin_id +from pyrogram import Client, filters +from defs.msg import gen_update_msg +from defs.source import update_data, compare, download + + +async def send_track_msg(file, track_msg): + if file: + await app.send_document(channel_id, file, + caption=track_msg.text, + file_name=track_msg.name, + force_document=True, + parse_mode="html", + reply_markup=track_msg.button) + else: + await app.send_message(channel_id, track_msg.text, + parse_mode="html", + reply_markup=track_msg.button) + + +# @scheduler.scheduled_job("cron", minute="*/30", id="0") +async def run_every_30_minute(): + await update_data() + need_update = compare() + for i in need_update: + track_msg = gen_update_msg(i) + if track_msg.url: + file = await download(track_msg.url, track_msg.name) + try: + await send_track_msg(file, track_msg) + except FloodWait as e: + print(f"Send document flood - Sleep for {e.x} second(s)") + await sleep(uniform(0.5, 1.0)) + await send_track_msg(file, track_msg) + except ButtonUrlInvalid: + print(f"Send button error") + await app.send_document(channel_id, file, + caption=track_msg.text, + file_name=track_msg.name, + force_document=True, + parse_mode="html",) + except Exception as e: + traceback.print_exc() + try: + remove(file) + except FileNotFoundError: + pass + else: + try: + await send_track_msg(None, track_msg) + except FloodWait as e: + print(f"Send document flood - Sleep for {e.x} second(s)") + await sleep(uniform(0.5, 1.0)) + await send_track_msg(None, track_msg) + except ButtonUrlInvalid: + print(f"Send button error") + await app.send_message(channel_id, track_msg.text, parse_mode="html",) + except Exception as e: + traceback.print_exc() + + +@Client.on_message(filters.incoming & filters.private & filters.chat(admin_id) & + filters.command(["force_update", ])) +async def force_update(bot: Client, message: Message): + await run_every_30_minute() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..563cb74 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +Pyrogram>=1.4.8 +Tgcrypto>=1.2.3 +pyromod +httpx>=0.22.0 +apscheduler>=3.8.1 +sqlitedict>=2.0.0