✨ Begin a project
This commit is contained in:
parent
201196733a
commit
bd2d03f9e7
6
.gitignore
vendored
6
.gitignore
vendored
@ -95,6 +95,9 @@ venv/
|
|||||||
ENV/
|
ENV/
|
||||||
env.bak/
|
env.bak/
|
||||||
venv.bak/
|
venv.bak/
|
||||||
|
config.ini
|
||||||
|
*session*
|
||||||
|
data/
|
||||||
|
|
||||||
# Spyder project settings
|
# Spyder project settings
|
||||||
.spyderproject
|
.spyderproject
|
||||||
@ -113,3 +116,6 @@ dmypy.json
|
|||||||
|
|
||||||
# Pyre type checker
|
# Pyre type checker
|
||||||
.pyre/
|
.pyre/
|
||||||
|
|
||||||
|
# pycharm
|
||||||
|
.idea/
|
||||||
|
52
ci.py
Normal file
52
ci.py
Normal file
@ -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)
|
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
|
19
defs/format_time.py
Normal file
19
defs/format_time.py
Normal file
@ -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")
|
50
defs/msg.py
Normal file
50
defs/msg.py
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from pyrogram.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||||
|
|
||||||
|
from defs.utils import Module, TrackMessage
|
||||||
|
from ci import me
|
||||||
|
|
||||||
|
template = """
|
||||||
|
{}
|
||||||
|
|
||||||
|
<b>模块:</b><code>{}</code>
|
||||||
|
<b>简介:</b><code>{}</code>
|
||||||
|
<b>版本:</b><code>{}</code>
|
||||||
|
<b>更新时间:</b><code>{}</code>
|
||||||
|
<b>更新日志:</b>
|
||||||
|
<code>
|
||||||
|
{}
|
||||||
|
</code>
|
||||||
|
@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)
|
50
defs/source.py
Normal file
50
defs/source.py
Normal file
@ -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}"
|
58
defs/utils.py
Normal file
58
defs/utils.py
Normal file
@ -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
|
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()
|
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 force_update(bot: Client, message: Message):
|
||||||
|
await message.reply("poi ~", quote=True)
|
22
plugins/start.py
Normal file
22
plugins/start.py
Normal file
@ -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,)
|
73
plugins/track.py
Normal file
73
plugins/track.py
Normal file
@ -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()
|
6
requirements.txt
Normal file
6
requirements.txt
Normal file
@ -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
|
Loading…
Reference in New Issue
Block a user