✨ 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.bak/
|
||||
venv.bak/
|
||||
config.ini
|
||||
*session*
|
||||
data/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
@ -113,3 +116,6 @@ dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.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