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