Release
This commit is contained in:
parent
cd4cb7a5b2
commit
836694df48
82
.github/workflows/docker-image.yml
vendored
Normal file
82
.github/workflows/docker-image.yml
vendored
Normal file
@ -0,0 +1,82 @@
|
||||
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow
|
||||
name: Build Docker Image
|
||||
|
||||
# 当 push 到 main 分支,或者创建以 v 开头的 tag 时触发,可根据需求修改
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE: iwumingz/sycgram # prinsss/ga-hit-counter
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# 这里用于定义 GITHUB_TOKEN 的权限
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# 缓存 Docker 镜像以加速构建
|
||||
- name: Cache Docker layers
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: /tmp/.buildx-cache
|
||||
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
||||
restore-keys: ${{ runner.os }}-buildx-
|
||||
|
||||
# 配置 QEMU 和 buildx 用于多架构镜像的构建
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v1
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Inspect builder
|
||||
run: |
|
||||
echo "Name: ${{ steps.buildx.outputs.name }}"
|
||||
echo "Endpoint: ${{ steps.buildx.outputs.endpoint }}"
|
||||
echo "Status: ${{ steps.buildx.outputs.status }}"
|
||||
echo "Flags: ${{ steps.buildx.outputs.flags }}"
|
||||
echo "Platforms: ${{ steps.buildx.outputs.platforms }}"
|
||||
|
||||
# 登录到 GitHub Packages 容器仓库
|
||||
# 注意 secrets.GITHUB_TOKEN 不需要手动添加,直接就可以用
|
||||
- name: Log in to the Container registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
# 根据输入自动生成 tag 和 label 等数据,说明见下
|
||||
- name: Extract metadata for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@v3
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE }}
|
||||
|
||||
# 构建并上传
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
builder: ${{ steps.buildx.outputs.name }}
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/tmp/.buildx-cache
|
||||
cache-to: type=local,dest=/tmp/.buildx-cache
|
||||
|
||||
- name: Inspect image
|
||||
run: docker buildx imagetools inspect ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ steps.meta.outputs.version }}
|
8
.gitignore
vendored
8
.gitignore
vendored
@ -127,3 +127,11 @@ dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
|
||||
data/app*
|
||||
data/config*
|
||||
data/log*
|
||||
data/img*
|
||||
*.debug.sh
|
||||
READ.md
|
||||
|
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@ -0,0 +1,16 @@
|
||||
FROM python:3.9-alpine
|
||||
LABEL maintainer=iwumingz
|
||||
|
||||
WORKDIR /sycgram
|
||||
COPY . /sycgram
|
||||
|
||||
RUN apk add --no-cache libjpeg libwebp libpng py3-lxml bc neofetch \
|
||||
&& apk add --no-cache --virtual build-deps gcc g++ zlib-dev jpeg-dev libxml2-dev libxslt-dev libwebp-dev libpng-dev \
|
||||
&& pip install -r requirements.txt --no-cache-dir \
|
||||
&& apk del build-deps \
|
||||
&& mkdir -p /sycgram/data \
|
||||
&& rm -rf .git .github .gitignore Dockerfile install.sh LICENSE README.md requirements.txt
|
||||
|
||||
VOLUME /sycgram/data
|
||||
|
||||
ENTRYPOINT ["/usr/local/bin/python3", "-u", "main.py"]
|
15
README.md
15
README.md
@ -1 +1,14 @@
|
||||
# sycgram
|
||||
# sycgram
|
||||
|
||||
## 安装与更新
|
||||
|
||||
```shell
|
||||
# 如果选择的是安装,则安装成功后,可先使用Ctrl+P,然后使用Ctrl+Q挂到后台运行
|
||||
bash <(curl -fsL "https://raw.githubusercontent.com/iwumingz/sycgram/main/install.sh")
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 脚本仅适用于Ubuntu/Debian
|
||||
- 按个人需求随缘更,仅用于学习用途。
|
||||
- 如果号码等输入错误了,重新安装即可
|
||||
|
13
core/__init__.py
Normal file
13
core/__init__.py
Normal file
@ -0,0 +1,13 @@
|
||||
from .custom import command
|
||||
from pyrogram import Client
|
||||
|
||||
app = Client(
|
||||
"./data/app",
|
||||
config_file='./data/config.ini',
|
||||
plugins=dict(root="plugins")
|
||||
)
|
||||
|
||||
|
||||
__all__ = (
|
||||
'app', 'command',
|
||||
)
|
37
core/custom.py
Normal file
37
core/custom.py
Normal file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- encoding: utf-8 -*-
|
||||
"""
|
||||
@File : custom.py
|
||||
@Time : 2022/04/02 10:17:03
|
||||
@Author : Viperorz
|
||||
@Version : 1.0.0
|
||||
@License : (C)Copyright 2021-2022
|
||||
@Desc : None
|
||||
"""
|
||||
|
||||
|
||||
from typing import List, Union
|
||||
from pyrogram import filters
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import STORE_TRACE_DATA
|
||||
from tools.storage import SimpleStore
|
||||
|
||||
|
||||
def command(command: Union[str, List[str]]):
|
||||
"""匹配UserBot指令"""
|
||||
return filters.me & filters.text & filters.command(command, '-')
|
||||
|
||||
|
||||
def is_traced():
|
||||
"""正则匹配用户输入指令及参数"""
|
||||
async def func(flt, _, msg: Message):
|
||||
async with SimpleStore(auto_flush=False) as store:
|
||||
trace_data = store.get_data(STORE_TRACE_DATA)
|
||||
if not trace_data:
|
||||
return False
|
||||
elif not trace_data.get(msg.from_user.id):
|
||||
return False
|
||||
return True
|
||||
|
||||
# "data" kwarg is accessed with "flt.data" above
|
||||
return filters.incoming & filters.create(func)
|
82
data/command.yml
Normal file
82
data/command.yml
Normal file
@ -0,0 +1,82 @@
|
||||
help:
|
||||
format: -help <command>
|
||||
usage: 指令列表
|
||||
|
||||
note:
|
||||
format: -note <save|del> <序号> or -note <序号|list|clear>
|
||||
usage: 回复一条消息,根据序号保存/删除该消息文本
|
||||
|
||||
dme:
|
||||
format: -dme <数量>
|
||||
usage: 直接使用。批量删除消息, 范围:1 ~ 1500,默认:1
|
||||
|
||||
f:
|
||||
format: -f <数量>
|
||||
usage: 回复一条消息,转发该消息n次。范围:1 ~ 30, 默认:1
|
||||
|
||||
cp:
|
||||
format: -cp <数量>
|
||||
usage: 回复一条消息,无引用转发该消息n次。范围:1 ~ 30, 默认:1
|
||||
|
||||
ghost:
|
||||
format: -ghost <status|list>
|
||||
usage: 直接使用。开启ghost的对话会被自动标记为已读
|
||||
|
||||
id:
|
||||
format: -id
|
||||
usage: 回复一条消息或直接使用,查看对话及消息的ID
|
||||
|
||||
sb:
|
||||
format: -sb
|
||||
usage: 回复一条消息,将在所有共同且拥有管理踢人权限的群组中踢出目标消息的主人
|
||||
|
||||
dc:
|
||||
format: -dc
|
||||
usage: 回复一条消息,或者直接使用。查看目标消息或当前对话的DC区
|
||||
|
||||
pingdc:
|
||||
format: -pingdc
|
||||
usage: 测试与各个DC的延时
|
||||
|
||||
ex:
|
||||
format: -ex <数字> <FROM> <TO>
|
||||
usage: 汇率转换
|
||||
|
||||
speedtest:
|
||||
format: -speedtest <无|节点ID|list|update>
|
||||
usage: 服务器本地网络测速
|
||||
|
||||
s:
|
||||
format: -s <无|emoji> or -s <sticker_set_title> <sticker_set_name>
|
||||
usage:
|
||||
收集回复的贴纸/图片/图片文件消息。直接使用时,可以设置默认贴纸包标题&名字;
|
||||
回复使用时,可以指定emoji,不指定则使用默认emoji
|
||||
|
||||
trace:
|
||||
format: -trace <emoji>
|
||||
usage: 回复一条消息,当目标消息的主人发消息时,自动丢<emoji>。默认:💩
|
||||
|
||||
cc:
|
||||
format: -cc <数量> or -cc <emoji>
|
||||
usage: 回复使用:遍历该消息的主人发过的消息并丢<数量>个<emoji>给Ta;直接使用:
|
||||
指令<emoji>为默认emoji。数量范围:1 ~ 233,Emoji默认为:💩
|
||||
|
||||
cal:
|
||||
format: -cal <四则运算式>
|
||||
usage: 直接使用。默认除法精确到小数点后4位
|
||||
|
||||
sh:
|
||||
format: -sh <shell脚本>
|
||||
usage: 直接使用
|
||||
|
||||
sysinfo:
|
||||
format: -sysinfo
|
||||
usage: 直接使用,查看系统信息
|
||||
|
||||
diss:
|
||||
format: -diss or -biss
|
||||
usage: 喷子语录
|
||||
|
||||
tg:
|
||||
format: -biss
|
||||
usage: 舔狗语录
|
154
install.sh
Normal file
154
install.sh
Normal file
@ -0,0 +1,154 @@
|
||||
#!/bin/bash
|
||||
clear
|
||||
|
||||
CONTAINER_NAME="sycgram"
|
||||
GITHUB_IMAGE_NAME="iwumingz/${CONTAINER_NAME}"
|
||||
GITHUB_IMAGE_PATH="ghcr.io/${GITHUB_IMAGE_NAME}"
|
||||
PROJECT_PATH="/opt/${CONTAINER_NAME}"
|
||||
PROJECT_VERSION="v1.0.0"
|
||||
|
||||
red='\033[0;31m'
|
||||
green='\033[0;32m'
|
||||
yellow='\033[0;33m'
|
||||
plain='\033[0m'
|
||||
|
||||
pre_check() {
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}错误: ${plain} 需要root权限\n" && exit 1
|
||||
|
||||
command -v git >/dev/null 2>&1
|
||||
if [[ $? != 0 ]]; then
|
||||
echo -e "正在安装Git..."
|
||||
apt install git -y >/dev/null 2>&1
|
||||
echo -e "${green}Git${plain} 安装成功"
|
||||
fi
|
||||
|
||||
command -v curl >/dev/null 2>&1
|
||||
if [[ $? != 0 ]]; then
|
||||
echo -e "正在安装curl..."
|
||||
apt install curl -y >/dev/null 2>&1
|
||||
echo -e "${green}curl${plain} 安装成功"
|
||||
fi
|
||||
|
||||
command -v docker >/dev/null 2>&1
|
||||
if [[ $? != 0 ]]; then
|
||||
echo -e "正在安装Docker..."
|
||||
bash <(curl -fsL https://get.docker.com) >/dev/null 2>&1
|
||||
echo -e "${green}Docker${plain} 安装成功"
|
||||
fi
|
||||
|
||||
command -v tar >/dev/null 2>&1
|
||||
if [[ $? == 0 ]]; then
|
||||
echo -e "正在安装tar..."
|
||||
apt install tar -y >/dev/null 2>&1
|
||||
echo -e "${green}tar${plain} 安装成功"
|
||||
fi
|
||||
}
|
||||
|
||||
delete_old_image_and_container(){
|
||||
# 获取最新指令说明
|
||||
target="https://raw.githubusercontent.com/iwumingz/sycgram/main/data/command.yml"
|
||||
curl -fsL ${target} > "${PROJECT_PATH}/data/command.yml"
|
||||
|
||||
echo "正在删除旧版本容器..."
|
||||
docker rm -f $(docker ps -a | grep ${CONTAINER_NAME} | awk '{print $1}')
|
||||
|
||||
echo "正在删除旧版本镜像..."
|
||||
docker image rm -f $(docker images | grep ${CONTAINER_NAME} | awk '{print $3}')
|
||||
}
|
||||
|
||||
check_and_create_config(){
|
||||
if [ ! -f ${PROJECT_PATH}/data/config.ini ]; then
|
||||
|
||||
mkdir -p "${PROJECT_PATH}/data" >/dev/null 2>&1
|
||||
|
||||
read -p "Please input your api_id:" api_id
|
||||
read -p "Please input your api_hash:" api_hash
|
||||
|
||||
cat > ${PROJECT_PATH}/data/config.ini <<EOF
|
||||
[pyrogram]
|
||||
api_id=${api_id}
|
||||
api_hash=${api_hash}
|
||||
|
||||
[plugins]
|
||||
root=plugins
|
||||
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
stop_sycgram(){
|
||||
docker stop $(docker ps -a | grep ${GITHUB_IMAGE_NAME} | awk '{print $1}')
|
||||
}
|
||||
|
||||
view_docker_log(){
|
||||
docker logs -f $(docker ps -a | grep sycgram | awk '{print $1}')
|
||||
}
|
||||
|
||||
uninstall_sycgram(){
|
||||
delete_old_image_and_container;
|
||||
rm -rf ${project_path}
|
||||
}
|
||||
|
||||
reinstall_sycgram(){
|
||||
rm -rf ${PROJECT_PATH}
|
||||
install_sycgram "-it"
|
||||
}
|
||||
|
||||
install_sycgram(){
|
||||
pre_check;
|
||||
check_and_create_config;
|
||||
delete_old_image_and_container;
|
||||
|
||||
echo -e "正在拉取镜像..."
|
||||
docker pull ghcr.io/iwumingz/sycgram:latest
|
||||
|
||||
echo -e "正在启动容器..."
|
||||
docker run $1 \
|
||||
--name ${CONTAINER_NAME} \
|
||||
--env TZ="Asia/Shanghai" \
|
||||
--restart always \
|
||||
-v ${PROJECT_PATH}/data:/sycgram/data \
|
||||
${GITHUB_IMAGE_PATH}:latest
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
echo -e "${green}Sycgram${plain} | ${green}管理脚本${plain} | ${red}${PROJECT_VERSION}${plain}"
|
||||
echo -e " ${green}1.${plain} 安装"
|
||||
echo -e " ${green}2.${plain} 更新"
|
||||
echo -e " ${green}3.${plain} 停止"
|
||||
echo -e " ${green}4.${plain} 查看日志"
|
||||
echo -e " ${green}5.${plain} 重新安装"
|
||||
echo -e " ${green}6.${plain} 卸载"
|
||||
echo -e " ${green}0.${plain} 退出脚本"
|
||||
echo -n "请选择编号: "
|
||||
read -ep "请输入选择 [0-6]: " option
|
||||
case "${option}" in
|
||||
0)
|
||||
exit 0
|
||||
;;
|
||||
1)
|
||||
install_sycgram "-it"
|
||||
;;
|
||||
2)
|
||||
install_sycgram "-itd"
|
||||
;;
|
||||
3)
|
||||
stop_sycgram
|
||||
;;
|
||||
4)
|
||||
view_docker_log
|
||||
;;
|
||||
5)
|
||||
reinstall_sycgram
|
||||
;;
|
||||
6)
|
||||
uninstall_sycgram
|
||||
;;
|
||||
*)
|
||||
echo -e "${yellow}已退出脚本...${plain}"
|
||||
exit
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
show_menu;
|
6
main.py
Normal file
6
main.py
Normal file
@ -0,0 +1,6 @@
|
||||
from core import app
|
||||
from tools.initializer import init_logger
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_logger()
|
||||
app.run()
|
0
plugins/__init__.py
Normal file
0
plugins/__init__.py
Normal file
29
plugins/calculate.py
Normal file
29
plugins/calculate.py
Normal file
@ -0,0 +1,29 @@
|
||||
import asyncio
|
||||
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait, RPCError
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import Parameters, basher
|
||||
|
||||
|
||||
@Client.on_message(command("cal"))
|
||||
async def calculate(_: Client, msg: Message):
|
||||
"""计算器"""
|
||||
_, args = Parameters.get(msg)
|
||||
try:
|
||||
res = await basher(f"""echo "scale=4;{args}" | bc""", 3)
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
await msg.edit_text("❗️ Connection Timeout")
|
||||
return
|
||||
if not res.get('output'):
|
||||
await msg.edit_text(f"Error:{res.get('error')}")
|
||||
return
|
||||
|
||||
text = f"""In:`{args}`\nOut:`{res.get('output')}`"""
|
||||
try:
|
||||
await msg.edit_text(text)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
except RPCError:
|
||||
await msg.edit_text(text)
|
107
plugins/cc.py
Normal file
107
plugins/cc.py
Normal file
@ -0,0 +1,107 @@
|
||||
import asyncio
|
||||
import re
|
||||
from random import choice, random
|
||||
from typing import List
|
||||
|
||||
from core import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import CC_MAX_TIMES, REACTIONS, STORE_CC_DATA, TG_GROUPS
|
||||
from tools.helpers import Parameters, delete_this, emoji_sender, get_cmd_error
|
||||
from tools.storage import SimpleStore
|
||||
|
||||
|
||||
@Client.on_message(command('cc'))
|
||||
async def cc(cli: Client, msg: Message):
|
||||
"""
|
||||
cc:
|
||||
format: -cc <数量> or -cc <emoji|set>
|
||||
usage:
|
||||
回复使用:遍历该消息的主人发过的消息并丢<数量>个<emoji>给Ta;直接使用:
|
||||
指令<emoji>为默认emoji。默认:💩
|
||||
"""
|
||||
cmd, opt = Parameters.get(msg)
|
||||
replied_msg = msg.reply_to_message
|
||||
|
||||
async with SimpleStore(auto_flush=False) as store:
|
||||
cc_emoji = store.data.get(STORE_CC_DATA)
|
||||
if not cc_emoji:
|
||||
cc_emoji = store.data[STORE_CC_DATA] = '💩'
|
||||
|
||||
if replied_msg and bool(re.match(r"[0-9]+", opt)):
|
||||
cc_times = int(opt)
|
||||
elif opt in REACTIONS or opt == 'set':
|
||||
store.data[STORE_CC_DATA] = choice(
|
||||
REACTIONS) if opt == 'set' else opt
|
||||
tmp = store.data[STORE_CC_DATA]
|
||||
store.flush()
|
||||
await msg.edit_text(f"Default emoji changed to `{tmp}`")
|
||||
return
|
||||
else:
|
||||
await msg.edit_text(get_cmd_error(cmd))
|
||||
return
|
||||
|
||||
# 攻击次数
|
||||
cc_times = cc_times if 1 <= cc_times <= CC_MAX_TIMES else CC_MAX_TIMES
|
||||
cc_msgs: List[int] = []
|
||||
|
||||
# 遍历和搜索消息
|
||||
if msg.chat.type in TG_GROUPS:
|
||||
async for target in cli.search_messages(
|
||||
chat_id=msg.chat.id, limit=1000,
|
||||
from_user=replied_msg.from_user.id,
|
||||
):
|
||||
if target.message_id > 1 and target.from_user:
|
||||
cc_msgs.append(target.message_id)
|
||||
if len(cc_msgs) == cc_times:
|
||||
break
|
||||
else:
|
||||
async for target in cli.iter_history(msg.chat.id, limit=1000):
|
||||
if target.message_id > 1 and target.from_user and \
|
||||
target.from_user.id == replied_msg.from_user.id:
|
||||
cc_msgs.append(target.message_id)
|
||||
if len(cc_msgs) == cc_times:
|
||||
break
|
||||
|
||||
if len(cc_msgs) > 0:
|
||||
await msg.edit_text("🔥 `Attacking ...`")
|
||||
shot = 0
|
||||
for n, target_id in enumerate(cc_msgs):
|
||||
try:
|
||||
res = await emoji_sender(
|
||||
cli=cli,
|
||||
chat_id=msg.chat.id,
|
||||
msg_id=target_id,
|
||||
emoji=cc_emoji
|
||||
)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e + 1)
|
||||
res = await emoji_sender(
|
||||
cli=cli,
|
||||
chat_id=msg.chat.id,
|
||||
msg_id=target_id,
|
||||
emoji=cc_emoji
|
||||
)
|
||||
|
||||
if not res and shot == 0:
|
||||
await msg.edit_text(
|
||||
f"This chat don't allow using {cc_emoji} to react."
|
||||
)
|
||||
return
|
||||
|
||||
shot = shot + 1
|
||||
logger.success(f"{cmd} | attacking | {n+1}")
|
||||
await asyncio.sleep(random() / 5)
|
||||
# Finished
|
||||
text = f"✅ Finished and the hit rate is {shot/cc_times*100}%"
|
||||
|
||||
else:
|
||||
# Finished
|
||||
text = "❓ Unable to find attack target!"
|
||||
|
||||
await delete_this(msg)
|
||||
res = await cli.send_message(msg.chat.id, text)
|
||||
await asyncio.sleep(3)
|
||||
await delete_this(res)
|
15
plugins/dc.py
Normal file
15
plugins/dc.py
Normal file
@ -0,0 +1,15 @@
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import get_dc_text
|
||||
|
||||
|
||||
@Client.on_message(command('dc'))
|
||||
async def dc(_: Client, msg: Message):
|
||||
"""获取群聊或者目标消息用户的dc_id"""
|
||||
_is_replied = bool(msg.reply_to_message)
|
||||
dc_id = msg.reply_to_message.from_user.dc_id \
|
||||
if _is_replied else msg.chat.dc_id
|
||||
name = msg.reply_to_message.from_user.mention(style="md") \
|
||||
if _is_replied else f"`{msg.chat.title}`"
|
||||
await msg.edit_text(get_dc_text(name, dc_id))
|
66
plugins/dme.py
Normal file
66
plugins/dme.py
Normal file
@ -0,0 +1,66 @@
|
||||
import asyncio
|
||||
import time
|
||||
from typing import List
|
||||
|
||||
from core import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait, RPCError
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import Parameters, get_iterlimit, is_deleted_id
|
||||
|
||||
|
||||
@Client.on_message(command('dme'))
|
||||
async def dme(client: Client, message: Message):
|
||||
"""删除指令数量的消息"""
|
||||
cmd, limit = Parameters.get_int(message, max_num=1500)
|
||||
counter, ids_deleted = 0, []
|
||||
await message.edit_text("🧹`Clearing history...`")
|
||||
start = time.time()
|
||||
|
||||
async def delete_messages(cli: Client, ids_deleted: List[int]):
|
||||
if len(ids_deleted) == 100:
|
||||
try:
|
||||
await cli.delete_messages(message.chat.id, ids_deleted)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x + 0.5)
|
||||
except RPCError as e:
|
||||
logger.error(e)
|
||||
else:
|
||||
ids_deleted.clear()
|
||||
|
||||
# 第一阶段,暴力扫描最近的消息,这些消息有可能无法搜索到
|
||||
async for msg in client.iter_history(message.chat.id, limit=get_iterlimit(limit)):
|
||||
if is_deleted_id(msg):
|
||||
logger.info(f'{cmd} | scanning | {msg.message_id}')
|
||||
ids_deleted.append(msg.message_id)
|
||||
counter = counter + 1
|
||||
await delete_messages(client, ids_deleted)
|
||||
if counter == limit:
|
||||
break
|
||||
|
||||
# 第二阶段,对于老的消息直接扫描性能不好,还会触发限制,使用搜索功能来提速
|
||||
if counter < limit:
|
||||
async for msg in client.search_messages(
|
||||
chat_id=message.chat.id,
|
||||
offset=counter,
|
||||
limit=limit - counter,
|
||||
from_user='me',
|
||||
):
|
||||
if is_deleted_id(msg) and msg.message_id not in ids_deleted:
|
||||
logger.info(f'{cmd} | searching | {msg.message_id}')
|
||||
ids_deleted.append(msg.message_id)
|
||||
counter = counter + 1
|
||||
await delete_messages(client, ids_deleted)
|
||||
if counter == limit:
|
||||
break
|
||||
|
||||
if len(ids_deleted) != 0:
|
||||
await client.delete_messages(message.chat.id, ids_deleted)
|
||||
text = f"🧹Deleted {counter} messages in {time.time() - start:.3f} seconds."
|
||||
res = await message.reply(text)
|
||||
await asyncio.sleep(3)
|
||||
await res.delete()
|
||||
# log
|
||||
logger.success(f"{cmd} | {text}")
|
||||
await logger.complete()
|
54
plugins/forward.py
Normal file
54
plugins/forward.py
Normal file
@ -0,0 +1,54 @@
|
||||
from core import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import RPCError
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import Parameters, delete_this
|
||||
|
||||
|
||||
async def check_replied_msg(msg: Message, cmd: str) -> bool:
|
||||
replied_msg = msg.reply_to_message
|
||||
if not replied_msg:
|
||||
await msg.edit_text(f"❗️ Please use `{cmd}` to reply to a message.")
|
||||
return False
|
||||
elif replied_msg.has_protected_content or replied_msg.chat.has_protected_content:
|
||||
await msg.edit_text("😮💨 Please don't foward protected messages")
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
@Client.on_message(command('f'))
|
||||
async def forward(_: Client, msg: Message):
|
||||
"""转发目标消息"""
|
||||
cmd, num = Parameters.get_int(msg)
|
||||
replied_msg = msg.reply_to_message
|
||||
if not await check_replied_msg(msg, cmd):
|
||||
return
|
||||
|
||||
await delete_this(msg)
|
||||
for _ in range(num):
|
||||
try:
|
||||
await replied_msg.forward(msg.chat.id, disable_notification=True)
|
||||
except RPCError as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
@Client.on_message(command('cp'))
|
||||
async def copy_forward(cli: Client, msg: Message):
|
||||
"""无引用转发"""
|
||||
cmd, num = Parameters.get_int(msg)
|
||||
if not await check_replied_msg(msg, cmd):
|
||||
return
|
||||
|
||||
await delete_this(msg)
|
||||
for _ in range(num):
|
||||
try:
|
||||
await cli.copy_message(
|
||||
chat_id=msg.chat.id,
|
||||
from_chat_id=msg.chat.id,
|
||||
message_id=msg.reply_to_message.message_id,
|
||||
disable_notification=True
|
||||
)
|
||||
except RPCError as e:
|
||||
logger.error(e)
|
58
plugins/ghost.py
Normal file
58
plugins/ghost.py
Normal file
@ -0,0 +1,58 @@
|
||||
import asyncio
|
||||
|
||||
from core.custom import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.errors import RPCError
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import STORE_GHOST_DATA
|
||||
from tools.ghosts import get_ghost_to_read
|
||||
from tools.helpers import Parameters, delete_this, get_fullname
|
||||
from tools.storage import SimpleStore
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming, group=-2)
|
||||
async def ghost_event(cli: Client, msg: Message):
|
||||
"""自动标记对话为<已读>"""
|
||||
if await get_ghost_to_read(msg.chat.id):
|
||||
try:
|
||||
await cli.read_history(msg.chat.id)
|
||||
except RPCError as e:
|
||||
logger.error(e)
|
||||
else:
|
||||
if msg.text or msg.caption:
|
||||
text = msg.text or msg.text
|
||||
text = f"Ghost | {msg.chat.title} | {get_fullname(msg.from_user)} | {text}"
|
||||
logger.debug(text)
|
||||
finally:
|
||||
await logger.complete()
|
||||
|
||||
|
||||
@Client.on_message(command('ghost'))
|
||||
async def ghost(_: Client, msg: Message):
|
||||
"""指令:将该对话标记为可自动<已读>状态"""
|
||||
_, opt = Parameters.get(msg)
|
||||
chat = msg.chat
|
||||
|
||||
async with SimpleStore(auto_flush=False) as store:
|
||||
ghost_data = store.get_data(STORE_GHOST_DATA)
|
||||
# ghost状态
|
||||
if opt == 'status':
|
||||
text = f"此对话是否开启ghost:{'✅' if chat.id in ghost_data else '❌'}"
|
||||
elif opt == 'list':
|
||||
tmp = '\n'.join(f'```{k} {v}```' for k, v in ghost_data.items())
|
||||
text = f"📢 已开启ghost的对话名单:\n{tmp}"
|
||||
# ghost开关
|
||||
else:
|
||||
if chat.id in ghost_data:
|
||||
text = "❌ 已关闭此对话的ghost"
|
||||
ghost_data.pop(chat.id, None)
|
||||
else:
|
||||
text = "✅ 已开启此对话的ghost"
|
||||
ghost_data[chat.id] = chat.title or get_fullname(msg.from_user)
|
||||
store.flush()
|
||||
|
||||
await msg.edit_text(text, parse_mode='md')
|
||||
await asyncio.sleep(1)
|
||||
if opt != 'status' and opt != 'list':
|
||||
await delete_this(msg)
|
24
plugins/help.py
Normal file
24
plugins/help.py
Normal file
@ -0,0 +1,24 @@
|
||||
from typing import Any, Dict
|
||||
import yaml
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
|
||||
|
||||
@Client.on_message(command('help'))
|
||||
async def helper(_: Client, msg: Message):
|
||||
"""指令用法提示。格式:-help <cmd|None>"""
|
||||
cmd = msg.text.replace('-help', '', 1).strip()
|
||||
cmd_data: Dict[str, Any] = yaml.full_load(open('./data/command.yml', 'rb'))
|
||||
if not cmd:
|
||||
tmp = '、'.join(f"`{k}`" for k in cmd_data.keys())
|
||||
text = f"📢 **指令列表:**\n{tmp}\n\n**发送** `-help <cmd>` **查看某指令的详细用法**"
|
||||
|
||||
elif not cmd_data.get(cmd):
|
||||
text = f'❓ `{cmd}` Command Not Found'
|
||||
|
||||
else:
|
||||
text = f"格式:`{cmd_data.get(cmd).get('format')}`\n" \
|
||||
f"用法:`{cmd_data.get(cmd).get('usage')}`"
|
||||
|
||||
await msg.edit_text(text, parse_mode='md')
|
23
plugins/info.py
Normal file
23
plugins/info.py
Normal file
@ -0,0 +1,23 @@
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import get_fullname
|
||||
|
||||
|
||||
@Client.on_message(command("id"))
|
||||
async def get_id(_: Client, msg: Message):
|
||||
"""直接使用或者回复目标消息,从而获取各种IDs"""
|
||||
text = f"Message ID: `{msg.message_id}`\n\n" \
|
||||
f"Chat Title: `{msg.chat.title}`\n" \
|
||||
f"Chat Type: `{msg.chat.type}`\n" \
|
||||
f"Chat ID: `{msg.chat.id}`"
|
||||
|
||||
if msg.reply_to_message:
|
||||
user = msg.reply_to_message.from_user
|
||||
text = f"Repiled Message ID: `{msg.reply_to_message.message_id}`\n\n" \
|
||||
f"User Nick: `{get_fullname(user)}`\n"\
|
||||
f"User Name: `@{user.username}`\n" \
|
||||
f"User ID: `{user.id}`\n\n" \
|
||||
f"{text}"
|
||||
|
||||
await msg.edit_text(text)
|
54
plugins/kick.py
Normal file
54
plugins/kick.py
Normal file
@ -0,0 +1,54 @@
|
||||
import asyncio
|
||||
from inspect import Parameter
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait, RPCError
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import delete_this, get_cmd_error, kick_one
|
||||
|
||||
|
||||
@Client.on_message(command('sb'))
|
||||
async def sb(cli: Client, msg: Message):
|
||||
"""回复一条消息,将在所有共同且拥有管理踢人权限的群组中踢出目标消息的主人"""
|
||||
cmd, *_ = Parameter.get(msg)
|
||||
reply_to_message = msg.reply_to_message
|
||||
if not reply_to_message or msg.chat.type in ['bot', 'private']:
|
||||
await msg.edit_text(get_cmd_error(cmd))
|
||||
return
|
||||
|
||||
counter, target = 0, reply_to_message.from_user
|
||||
common_groups = await target.get_common_chats()
|
||||
logger.info(
|
||||
f"Start to kick <{target.first_name}{target.last_name} <{target.id}>")
|
||||
for chat in common_groups:
|
||||
try:
|
||||
if await kick_one(cli, chat.id, target.id):
|
||||
counter = counter + 1
|
||||
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
if await kick_one(cli, chat.id, target.id):
|
||||
counter = counter + 1
|
||||
logger.success(
|
||||
f"Kick this user out of <{chat.tile} {chat.id}>"
|
||||
)
|
||||
|
||||
except RPCError as e:
|
||||
logger.warning(
|
||||
f"No admin rights in this group <{chat.title} {chat.id}>")
|
||||
logger.warning(e)
|
||||
|
||||
# delete this user all messages
|
||||
await cli.delete_user_history(msg.chat.id, target.id)
|
||||
|
||||
# Inform
|
||||
text = f"😂 Kick {target.mention(style='md')} in {counter} common groups."
|
||||
await msg.edit_text(text)
|
||||
await asyncio.sleep(10)
|
||||
await delete_this(msg)
|
||||
# log
|
||||
logger.success(f"{cmd} | {text}")
|
||||
await logger.complete()
|
63
plugins/note.py
Normal file
63
plugins/note.py
Normal file
@ -0,0 +1,63 @@
|
||||
import asyncio
|
||||
|
||||
from core import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import BadRequest, FloodWait
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import STORE_NOTES_DATA
|
||||
from tools.helpers import Parameters, get_cmd_error
|
||||
from tools.storage import SimpleStore
|
||||
|
||||
|
||||
@Client.on_message(command('note'))
|
||||
async def note(_: Client, msg: Message):
|
||||
"""
|
||||
用法一:-note <save|del> <序号>
|
||||
用法二:-note <序号|list|clear>
|
||||
作用:发送已保存的笔记
|
||||
"""
|
||||
cmd, opts = Parameters.get_more(msg)
|
||||
if not (1 <= len(opts) <= 2):
|
||||
await msg.edit_text(get_cmd_error(cmd))
|
||||
return
|
||||
|
||||
replied_msg = msg.reply_to_message
|
||||
async with SimpleStore() as store:
|
||||
notes_data = store.get_data(STORE_NOTES_DATA)
|
||||
if len(opts) == 2 and opts[0] == 'save' and replied_msg:
|
||||
if replied_msg:
|
||||
notes_data[opts[1]] = replied_msg.text or replied_msg.caption
|
||||
text = "😊 Notes saved successfully."
|
||||
else:
|
||||
text = get_cmd_error(cmd)
|
||||
elif len(opts) == 2 and opts[0] == 'del':
|
||||
if notes_data.pop(opts[1], None):
|
||||
text = "😊 Notes deleted successfully."
|
||||
else:
|
||||
text = "❓ Can't find the note to delete."
|
||||
elif len(opts) == 1:
|
||||
option = opts[0]
|
||||
if option == 'list':
|
||||
tmp = '\n'.join(
|
||||
f'```{k} | {v[0:30]} ...```' for k, v in notes_data.items())
|
||||
text = f"已保存的笔记:\n{tmp}"
|
||||
elif option == 'clear':
|
||||
notes_data.clear()
|
||||
text = "✅ All saved notes have been deleted."
|
||||
else:
|
||||
res = notes_data.get(option)
|
||||
text = res if res else f"😱 No saved notes found for {option}"
|
||||
else:
|
||||
text = get_cmd_error(cmd)
|
||||
|
||||
try:
|
||||
await msg.edit_text(text)
|
||||
except BadRequest as e:
|
||||
logger.error(e) # 存在消息过长的问题,应拆分发送。(就不拆 😊)
|
||||
except FloodWait as e:
|
||||
logger.warning(e)
|
||||
await asyncio.sleep(e.x)
|
||||
await msg.edit_text(text)
|
||||
finally:
|
||||
await logger.complete()
|
53
plugins/other.py
Normal file
53
plugins/other.py
Normal file
@ -0,0 +1,53 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from core import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait, RPCError
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import delete_this, escape_markdown
|
||||
from tools.sessions import session
|
||||
|
||||
|
||||
@Client.on_message(command(['biss', 'diss', 'tg']))
|
||||
async def other(_: Client, msg: Message):
|
||||
"""喷人/舔狗"""
|
||||
if bool(re.match(r"-(d|b)iss", msg.text)):
|
||||
symbol = '💢 '
|
||||
api = 'https://zuan.shabi.workers.dev/'
|
||||
elif bool(re.match(r"-tg", msg.text)):
|
||||
symbol = '👅 '
|
||||
api = 'http://ovooa.com/API/tgrj/api.php'
|
||||
|
||||
await msg.edit_text(f"{symbol}It's preparating.")
|
||||
|
||||
for _ in range(10):
|
||||
try:
|
||||
resp = await session.get(api, timeout=5.5)
|
||||
if resp.status == 200:
|
||||
text = escape_markdown(await resp.text())
|
||||
else:
|
||||
resp.raise_for_status()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
words = f"{msg.reply_to_message.from_user.mention(style='md')} {text}" \
|
||||
if msg.reply_to_message else text
|
||||
try:
|
||||
await msg.edit_text(words, parse_mode='md')
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await msg.edit_text(words, parse_mode='md')
|
||||
except RPCError as e:
|
||||
logger.error(e)
|
||||
|
||||
await logger.complete()
|
||||
return
|
||||
|
||||
# Failed to get api text
|
||||
await delete_this(msg)
|
||||
res = await msg.edit_text('😤 Rest for a while.')
|
||||
await asyncio.sleep(3)
|
||||
await delete_this(res)
|
29
plugins/pingdc.py
Normal file
29
plugins/pingdc.py
Normal file
@ -0,0 +1,29 @@
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import execute
|
||||
|
||||
|
||||
@Client.on_message(command('pingdc'))
|
||||
async def pingdc(_: Client, msg: Message):
|
||||
"""到各个DC区的延时"""
|
||||
DCs = {
|
||||
1: "149.154.175.50",
|
||||
2: "149.154.167.51",
|
||||
3: "149.154.175.100",
|
||||
4: "149.154.167.91",
|
||||
5: "91.108.56.130"
|
||||
}
|
||||
data = []
|
||||
for dc in range(1, 6):
|
||||
result = await execute(f"ping -c 1 {DCs[dc]} | awk -F '/' " + "'END {print $5}'")
|
||||
output = result.get('output')
|
||||
data.append(output.replace('\n', '') if output else '-1')
|
||||
|
||||
await msg.edit_text(
|
||||
f"🇺🇸 DC1(迈阿密): `{data[0]}`\n"
|
||||
f"🇳🇱 DC2(阿姆斯特丹): `{data[1]}`\n"
|
||||
f"🇺🇸 DC3(迈阿密): `{data[2]}`\n"
|
||||
f"🇳🇱 DC4(阿姆斯特丹): `{data[3]}`\n"
|
||||
f"🇸🇬 DC5(新加坡): `{data[4]}`", "md"
|
||||
)
|
50
plugins/rate.py
Normal file
50
plugins/rate.py
Normal file
@ -0,0 +1,50 @@
|
||||
from core import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import RATE_API
|
||||
from tools.helpers import Parameters
|
||||
from tools.sessions import session
|
||||
|
||||
|
||||
@Client.on_message(command('ex'))
|
||||
async def rate(_: Client, msg: Message):
|
||||
"""查询当天货币汇率,格式:-ex <float> <FROM> <TO>"""
|
||||
cmd, args = Parameters.get_more(msg)
|
||||
if len(args) != 3:
|
||||
failure = f"❗️ Usage like `{cmd} 1 usd cny`, it will be exchanged from usd to cny."
|
||||
await msg.edit_text(failure)
|
||||
return
|
||||
|
||||
try:
|
||||
num = abs(float(args[0]))
|
||||
except ValueError:
|
||||
await msg.edit_text("❗️ Not the correct number.")
|
||||
return
|
||||
else:
|
||||
__from = args[1].lower()
|
||||
__to = args[2].lower()
|
||||
|
||||
for _ in range(10):
|
||||
async with session.get(
|
||||
f'{RATE_API}/{__from}/{__to}.json', timeout=5.5
|
||||
) as resp:
|
||||
try:
|
||||
if resp.status == 200:
|
||||
data = await resp.json()
|
||||
result = float(data.get(__to)) * num
|
||||
success = f"```{__from.upper()} : {__to.upper()} = {num} : {result:.5f}```"
|
||||
await msg.edit_text(success)
|
||||
logger.success(success)
|
||||
return
|
||||
else:
|
||||
resp.raise_for_status()
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
finally:
|
||||
await logger.complete()
|
||||
|
||||
failure = "❗️ Network error or wrong currency symbol. Please try again."
|
||||
await msg.edit_text(failure)
|
||||
return
|
42
plugins/shell.py
Normal file
42
plugins/shell.py
Normal file
@ -0,0 +1,42 @@
|
||||
import asyncio
|
||||
from getpass import getuser
|
||||
from io import BytesIO
|
||||
from platform import node
|
||||
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import Parameters, basher, delete_this, get_cmd_error
|
||||
|
||||
|
||||
@Client.on_message(command("sh"))
|
||||
async def shell(_: Client, msg: Message):
|
||||
"""执行shell脚本"""
|
||||
cmd, _input = Parameters.get(msg)
|
||||
if not _input:
|
||||
await msg.edit_text(get_cmd_error(cmd))
|
||||
return
|
||||
|
||||
try:
|
||||
res = await basher(_input, timeout=30)
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
await msg.edit_text('❗️ Connection Timeout')
|
||||
return
|
||||
|
||||
_output: str = res.get('output') if not res.get('error') else res.get('error')
|
||||
header = f"**{getuser()}@{node()}**\n"
|
||||
all_bytes = len(header.encode() + _input.encode() + _output.encode())
|
||||
if all_bytes >= 2048:
|
||||
await delete_this(msg)
|
||||
await msg.reply_document(
|
||||
document=BytesIO(_output.encode()),
|
||||
caption=f"{header}> # `{_input}`",
|
||||
file_name="output.log",
|
||||
parse_mode='md'
|
||||
)
|
||||
return
|
||||
|
||||
await msg.edit_text(
|
||||
f"{header}> # `{_input}`\n```{_output.strip()}```",
|
||||
parse_mode='md'
|
||||
)
|
63
plugins/speedtest.py
Normal file
63
plugins/speedtest.py
Normal file
@ -0,0 +1,63 @@
|
||||
import asyncio
|
||||
import re
|
||||
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import SPEEDTEST_RUN
|
||||
from tools.helpers import Parameters, delete_this, get_cmd_error, show_error
|
||||
from tools.speedtests import Speedtester
|
||||
|
||||
|
||||
@Client.on_message(command('speedtest'))
|
||||
async def speedtest(_: Client, msg: Message):
|
||||
"""服务器测速,用法:-speedtest <节点ID|list|update>"""
|
||||
cmd, opt = Parameters.get(msg)
|
||||
|
||||
await msg.edit_text("⚡️ Speedtest is running.")
|
||||
async with Speedtester() as tester:
|
||||
if opt == 'update':
|
||||
try:
|
||||
update_res = await tester.init_for_speedtest('update')
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
await msg.edit_text("⚠️ Update Timeout")
|
||||
except Exception as e:
|
||||
await show_error(msg, e)
|
||||
else:
|
||||
# TODO:有个未知错误
|
||||
await msg.edit_text(f"Result\n```{update_res}```")
|
||||
return
|
||||
elif opt == 'list':
|
||||
try:
|
||||
text = await tester.list_servers_ids(f"{SPEEDTEST_RUN} -L")
|
||||
await msg.edit_text(text, parse_mode='md')
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
await msg.edit_text("⚠️ Speedtest Timeout")
|
||||
return
|
||||
elif bool(re.match(r'[0-9]+', opt)) or not opt:
|
||||
try:
|
||||
text, link = await tester.running(
|
||||
f"""{SPEEDTEST_RUN}{'' if not opt else f' -s {opt}'}"""
|
||||
)
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
await msg.edit_text("⚠️ Speedtest Timeout")
|
||||
return
|
||||
else:
|
||||
await msg.edit_text(get_cmd_error(cmd))
|
||||
return
|
||||
|
||||
if not link:
|
||||
await msg.edit_text(text)
|
||||
return
|
||||
|
||||
# send speed report
|
||||
try:
|
||||
await msg.reply_photo(photo=link, caption=text, parse_mode='md')
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await msg.reply_photo(photo=link, caption=text, parse_mode='md')
|
||||
except Exception as e:
|
||||
await show_error(msg, e)
|
||||
# delete cmd history
|
||||
await delete_this(msg)
|
157
plugins/sticker.py
Normal file
157
plugins/sticker.py
Normal file
@ -0,0 +1,157 @@
|
||||
import asyncio
|
||||
from time import time
|
||||
|
||||
from core import command
|
||||
from loguru import logger
|
||||
from pyrogram import Client, filters
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import STICKER_BOT, STICKER_ERROR_LIST
|
||||
from tools.helpers import Parameters, check_if_package_existed, get_default_pkg
|
||||
from tools.stickers import StickerAdder, sticker_cond, sticker_locker
|
||||
from tools.storage import SimpleStore
|
||||
|
||||
|
||||
@Client.on_message(filters.incoming & filters.user(STICKER_BOT), group=-1)
|
||||
async def sticker_event(cli: Client, msg: Message):
|
||||
async with sticker_cond.get_response():
|
||||
if msg.text not in STICKER_ERROR_LIST:
|
||||
sticker_cond.notify()
|
||||
logger.success(f"Receive @Stickers response | {msg.text}")
|
||||
else:
|
||||
async with SimpleStore() as store:
|
||||
me = await cli.get_me()
|
||||
pkg_title, pkg_name = get_default_pkg(me)
|
||||
store.data['sticker_error'] = msg.text
|
||||
store.data['sticker_set_title'] = pkg_title
|
||||
store.data['sticker_set_name'] = pkg_name
|
||||
logger.error(f"Receive @Stickers error | {msg.text}")
|
||||
await logger.complete()
|
||||
|
||||
|
||||
@Client.on_message(command('s'))
|
||||
async def sticker(cli: Client, msg: Message):
|
||||
"""
|
||||
用法一:-s <emoji|无> 回复一条消息
|
||||
用法二:-s <sticker_set_title> <sticker_set_name> 切换默认贴纸包标题和名字
|
||||
作用:偷为静态贴纸(对象:贴纸/图片/图片文件)
|
||||
"""
|
||||
_, args = Parameters.get_more(msg)
|
||||
if not msg.reply_to_message:
|
||||
# 处理参数
|
||||
if len(args) != 2:
|
||||
pkg_title, pkg_name = get_default_pkg(msg.from_user)
|
||||
await msg.edit('✅ Reset sticker title and name to default..')
|
||||
else:
|
||||
pkg_title, pkg_name = args
|
||||
if len(pkg_title.encode()) >= 168:
|
||||
await msg.edit_text('❗️ Too long sticker set title.')
|
||||
return
|
||||
elif len(pkg_title.encode()) >= 58:
|
||||
await msg.edit_text('❗️ Too long sticker set name.')
|
||||
return
|
||||
await msg.edit('✅ Customize sticker title and name successfully.')
|
||||
|
||||
async with SimpleStore() as store:
|
||||
store.data['sticker_set_title'] = pkg_title
|
||||
store.data['sticker_set_name'] = pkg_name
|
||||
return
|
||||
|
||||
async with SimpleStore() as store:
|
||||
pkg_title = store.data.get('sticker_set_title')
|
||||
pkg_name = store.data.get('sticker_set_name')
|
||||
if not pkg_title or not pkg_name:
|
||||
await msg.edit_text(
|
||||
"⚠️ The default sticker title and name are empty, "
|
||||
"please use `-s` reset!"
|
||||
)
|
||||
return
|
||||
|
||||
# 尝试检查贴纸包是否存在
|
||||
try:
|
||||
pkg_existed = await check_if_package_existed(pkg_name)
|
||||
except Exception as e:
|
||||
# 无法判定是否贴纸包存在
|
||||
logger.error(e)
|
||||
await msg.edit_text('⚠️ Network Error | Please try again later ...')
|
||||
return
|
||||
|
||||
# 开始前的检查
|
||||
await msg.edit_text('👆 Working on adding stickers ...')
|
||||
await cli.unblock_user(STICKER_BOT)
|
||||
# 开始偷贴纸
|
||||
async with sticker_locker.get_lock():
|
||||
try:
|
||||
await sticker_helper(
|
||||
cli=cli,
|
||||
msg=msg,
|
||||
pkg_title=pkg_title,
|
||||
pkg_name=pkg_name,
|
||||
pkg_existed=pkg_existed,
|
||||
)
|
||||
except asyncio.exceptions.TimeoutError:
|
||||
async with SimpleStore() as store:
|
||||
sticker_error = store.data.get('sticker_error')
|
||||
store.data.pop('sticker_error', None)
|
||||
await msg.edit_text(f"❌ Error\n```{sticker_error}```")
|
||||
except TypeError:
|
||||
await msg.edit_text("😭 Not static image, now stopped ...")
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
await msg.edit_text("😭 Failed to add stickers, now stopped ...")
|
||||
finally:
|
||||
await logger.complete()
|
||||
|
||||
|
||||
async def sticker_helper(
|
||||
cli: Client,
|
||||
msg: Message,
|
||||
pkg_title: str,
|
||||
pkg_name: str,
|
||||
pkg_existed: bool,
|
||||
):
|
||||
replied = msg.reply_to_message
|
||||
if not (replied.sticker or replied.photo or (
|
||||
replied.document and
|
||||
'image' in replied.document.mime_type
|
||||
)):
|
||||
raise TypeError("It's not photo")
|
||||
|
||||
start, adder = time(), StickerAdder(cli, msg)
|
||||
# ---------------- 目标消息为:贴纸 ----------------
|
||||
success = f"👍 Finished in <time> and Click [here](https://t.me/addstickers/{pkg_name}) for details."
|
||||
# Up to 3 attempts
|
||||
for attempts in range(3):
|
||||
if pkg_existed:
|
||||
# counter == 6
|
||||
await adder.do_cancel()
|
||||
await adder.send_message('/addsticker')
|
||||
await adder.send_message(pkg_name)
|
||||
if await adder.upload_photo():
|
||||
continue
|
||||
await adder.send_emoji()
|
||||
await adder.send_message('/done')
|
||||
|
||||
else:
|
||||
# counter == 8
|
||||
await adder.do_cancel()
|
||||
await adder.send_message('/newpack')
|
||||
await adder.send_message(pkg_title)
|
||||
if await adder.upload_photo():
|
||||
continue
|
||||
await adder.send_emoji()
|
||||
await adder.send_message('/publish')
|
||||
await adder.send_message('/skip')
|
||||
await adder.send_message(pkg_name)
|
||||
|
||||
if adder.is_finished(pkg_existed):
|
||||
success = success.replace('<time>', f'{time()-start:.3f}', 1)
|
||||
await adder.done(success, parse_mode='md')
|
||||
return
|
||||
else:
|
||||
adder.send_retries(attempts)
|
||||
|
||||
failure = "😭 Failed to add stickers, now stopped ..."
|
||||
await adder.done(failure, 'md')
|
||||
logger.warning(failure)
|
||||
await logger.complete()
|
||||
return
|
14
plugins/sysinfo.py
Normal file
14
plugins/sysinfo.py
Normal file
@ -0,0 +1,14 @@
|
||||
from core import command
|
||||
from pyrogram import Client
|
||||
from pyrogram.types import Message
|
||||
from tools.helpers import basher
|
||||
|
||||
|
||||
@Client.on_message(command("sysinfo"))
|
||||
async def sysinfo(_: Client, msg: Message):
|
||||
"""查询系统信息"""
|
||||
res = await basher("neofetch --config none --stdout")
|
||||
if not res.get('error'):
|
||||
await msg.edit_text(f"```{res.get('output')}```")
|
||||
else:
|
||||
await msg.edit_text(f"```{res.get('error')}```")
|
97
plugins/trace.py
Normal file
97
plugins/trace.py
Normal file
@ -0,0 +1,97 @@
|
||||
import asyncio
|
||||
|
||||
from core import command
|
||||
from core.custom import is_traced
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import BadRequest, FloodWait, RPCError
|
||||
from pyrogram.types import Message
|
||||
from tools.constants import REACTIONS, STORE_TRACE_DATA
|
||||
from tools.helpers import Parameters, delete_this
|
||||
from tools.storage import SimpleStore
|
||||
|
||||
|
||||
@Client.on_message(is_traced(), group=-4)
|
||||
async def trace_event(cli: Client, msg: Message):
|
||||
user = msg.from_user
|
||||
async with SimpleStore(auto_flush=False) as store:
|
||||
try:
|
||||
emoji = store.get_data(STORE_TRACE_DATA).get(user.id)
|
||||
await cli.send_reaction(
|
||||
msg.chat.id, msg.message_id, emoji
|
||||
)
|
||||
except BadRequest:
|
||||
failure = f"Group named <{msg.chat.title}> can't use {emoji} to react."
|
||||
store.data[STORE_TRACE_DATA].pop(user.id, None)
|
||||
store.flush()
|
||||
await cli.send_message('me', failure)
|
||||
except RPCError as e:
|
||||
logger.error(e)
|
||||
|
||||
|
||||
@Client.on_message(command('trace'))
|
||||
async def trace(cli: Client, msg: Message):
|
||||
"""群组中追着丢emoji
|
||||
指令:-trace
|
||||
用法:-trace <emoji|clear|list> 用于回复一条消息
|
||||
"""
|
||||
cmd, opt = Parameters.get(msg)
|
||||
replied_msg = msg.reply_to_message
|
||||
|
||||
if not opt and not replied_msg:
|
||||
await msg.edit_text(f'❗️ Use `{cmd}` to reply a message.')
|
||||
return
|
||||
|
||||
if opt != 'clear' and opt != 'list':
|
||||
emoji = '💩' if opt not in REACTIONS else opt
|
||||
user = replied_msg.from_user
|
||||
try:
|
||||
await cli.send_reaction(
|
||||
msg.chat.id,
|
||||
replied_msg.message_id,
|
||||
emoji
|
||||
)
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
await msg.edit_text(f"❗️ Can't use {emoji} in this chat.")
|
||||
return
|
||||
|
||||
async with SimpleStore() as store:
|
||||
trace_data = store.get_data(STORE_TRACE_DATA)
|
||||
if opt != 'clear' and opt != 'list':
|
||||
# 追踪列表中没有,则添加
|
||||
if not trace_data.get(user.id):
|
||||
trace_data[user.id] = emoji
|
||||
text = f"✅ 添加 {user.mention(style='md')} 到trace列表"
|
||||
logger.success(text)
|
||||
# 追踪列表有,则删除
|
||||
elif trace_data.pop(user.id, False):
|
||||
text = f"✅ 将 {user.mention(style='md')} 从trace列表移除"
|
||||
logger.success(text)
|
||||
# 删除失败??
|
||||
else:
|
||||
text = f"❌ 竟然将 {user.mention(style='md')} 从trace列表移除失败!!!"
|
||||
logger.warning(text)
|
||||
elif opt == 'clear':
|
||||
trace_data.clear()
|
||||
text = "✅ 已清空trace名单"
|
||||
elif opt == 'list':
|
||||
tmp = '\n'.join(f"`{k}` | {v}" for k, v in trace_data.items())
|
||||
text = f"📢 trace名单:\n{tmp}"
|
||||
|
||||
try:
|
||||
await msg.edit_text(text, parse_mode='md')
|
||||
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await msg.edit_text(text, parse_mode='md')
|
||||
|
||||
except RPCError as e:
|
||||
logger.error(e)
|
||||
await msg.edit_text(f"Network error | `{e}`")
|
||||
|
||||
finally:
|
||||
if opt != 'clear' and opt != 'list':
|
||||
await asyncio.sleep(3)
|
||||
await delete_this(msg)
|
||||
await logger.complete()
|
BIN
requirements.txt
Normal file
BIN
requirements.txt
Normal file
Binary file not shown.
0
tools/__init__.py
Normal file
0
tools/__init__.py
Normal file
55
tools/constants.py
Normal file
55
tools/constants.py
Normal file
@ -0,0 +1,55 @@
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
# ------------- rate --------------
|
||||
RATE_API: str = 'https://cdn.jsdelivr.net/gh/fawazahmed0/currency-api@1/latest/currencies'
|
||||
HTTP_HEADERS: Dict[str, str] = {
|
||||
"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"
|
||||
}
|
||||
|
||||
# ------------- speedtest --------------
|
||||
SPEEDTEST_PATH_FILE: str = './data/speedtest'
|
||||
SPEEDTEST_CLI: str = "https://install.speedtest.net/app/cli/ookla-speedtest-1.1.1-linux-<arch>.tgz"
|
||||
INSTALL_SPEEDTEST: str = f"wget -qO- {SPEEDTEST_CLI} | tar zx -C ./data speedtest"
|
||||
SPEEDTEST_RUN: str = f'{SPEEDTEST_PATH_FILE} --accept-license --accept-gdpr -f json'
|
||||
|
||||
# ------------- sticker --------------
|
||||
STICKER_BOT: int = 429000
|
||||
STICKER_IMG: str = './data/img/tmp.png'
|
||||
STICKER_DESCRIP: str = b'A Telegram user has created the Sticker\xc2\xa0Set.'.decode('utf-8')
|
||||
GT_120_STICKERS: str = "Whoa! That's probably enough stickers for one set, " \
|
||||
"give it a break. A set can't have more than 120 stickers at the moment."
|
||||
UNACCEPTABLE_SET_NAME: str = 'Sorry, this short name is unacceptable.'
|
||||
TAKEN_SET_NAME: str = 'Sorry, this short name is already taken.'
|
||||
INVALID_SET_NAME: str = 'Invalid set selected.'
|
||||
STICKER_ERROR_LIST: List[str] = [
|
||||
GT_120_STICKERS,
|
||||
UNACCEPTABLE_SET_NAME,
|
||||
TAKEN_SET_NAME,
|
||||
INVALID_SET_NAME,
|
||||
]
|
||||
|
||||
# ------------- cc & trace --------------
|
||||
REACTIONS: list[str] = ['👍', '👎', '❤️', '🔥', '🥰', '👏',
|
||||
'😁', '🤔', '🤯', '😱', '🤬', '😢',
|
||||
'🎉', '🤩', '🤮', '💩']
|
||||
CC_MAX_TIMES: int = 233
|
||||
|
||||
# ------------- ghost --------------
|
||||
GHOST_INTERVAL: float = 1.5
|
||||
|
||||
# ------------- other --------------
|
||||
TG_GROUP: str = 'group'
|
||||
TG_SUPERGROUP: str = 'supergroup'
|
||||
TG_CHANNEL: str = 'channel'
|
||||
TG_BOT: str = 'bot'
|
||||
TG_PRIVATE: str = 'private'
|
||||
TG_GROUPS: List[str] = ['group', 'supergroup']
|
||||
|
||||
# ------------- Store -------------
|
||||
STORE_CC_DATA: str = 'data:cc'
|
||||
STORE_NOTES_DATA: str = 'data:notes'
|
||||
STORE_TRACE_DATA: str = 'data:trace'
|
||||
STORE_GHOST_DATA: str = 'data:ghost'
|
||||
STORE_GHOST_CACHE: str = 'cache:ghost'
|
20
tools/ghosts.py
Normal file
20
tools/ghosts.py
Normal file
@ -0,0 +1,20 @@
|
||||
from time import time
|
||||
from typing import Union
|
||||
|
||||
from .constants import GHOST_INTERVAL, STORE_GHOST_CACHE, STORE_GHOST_DATA
|
||||
from .storage import SimpleStore
|
||||
|
||||
|
||||
async def get_ghost_to_read(cid: Union[int, str]) -> bool:
|
||||
"""是否自动标记为已读"""
|
||||
async with SimpleStore() as store:
|
||||
ghost_cache = store.get_data(STORE_GHOST_CACHE)
|
||||
ghost_list = store.get_data(STORE_GHOST_DATA)
|
||||
if cid in ghost_list.keys() and (
|
||||
not ghost_cache.get(cid) or
|
||||
time() - ghost_cache.get(cid) > GHOST_INTERVAL
|
||||
):
|
||||
ghost_cache[cid] = time()
|
||||
return True
|
||||
|
||||
return False
|
200
tools/helpers.py
Normal file
200
tools/helpers.py
Normal file
@ -0,0 +1,200 @@
|
||||
import asyncio
|
||||
import re
|
||||
from typing import Any, Dict, List, Tuple, Union
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
from loguru import logger
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait, RPCError
|
||||
from pyrogram.types import Message, User
|
||||
|
||||
from .constants import STICKER_DESCRIP
|
||||
from .sessions import session
|
||||
|
||||
|
||||
class Parameters:
|
||||
@classmethod
|
||||
def get(cls, msg: Message) -> Tuple[str]:
|
||||
"""返回所需的指令的单个参数,类型为`str`"""
|
||||
text = msg.text.split(' ', 1)
|
||||
if len(text) > 1:
|
||||
return text[0], ''.join(text[1:]).strip()
|
||||
else:
|
||||
return text[0], ''
|
||||
|
||||
@classmethod
|
||||
def get_int(cls, msg: Message, max_num: int = 30) -> Tuple[str, int]:
|
||||
"""返回所需的指令的单个参数,类型为`int`"""
|
||||
cmd, arg = cls.get(msg)
|
||||
if not arg or not bool(re.match(r"[0-9]+$", arg)):
|
||||
return cmd, 1
|
||||
num = int(arg) if 1 <= int(arg) <= max_num else 1
|
||||
return cmd, num
|
||||
|
||||
@classmethod
|
||||
def get_more(cls, msg: Message) -> Tuple[str, List[str]]:
|
||||
cmd, text = cls.get(msg)
|
||||
return cmd, [x.strip() for x in text.split(' ') if x]
|
||||
|
||||
|
||||
def get_iterlimit(num: int) -> int:
|
||||
"""
|
||||
Args:
|
||||
num (int): 实际删除消息数
|
||||
|
||||
Returns:
|
||||
int: 迭代历史消息限制数
|
||||
"""
|
||||
|
||||
return num * 3 if num * 3 < 1500 else 1500
|
||||
|
||||
|
||||
def get_dc_text(name: str, dc_id: int) -> str:
|
||||
text = f"{name} 的数据中心为:`DC{dc_id}`\n该数据中心位于:"
|
||||
|
||||
if dc_id == 1 or dc_id == 3:
|
||||
return f"{text}`美国佛罗里达州迈阿密`"
|
||||
|
||||
elif dc_id == 2 or dc_id == 4:
|
||||
return f"{text}`荷兰北荷兰省阿姆斯特丹`"
|
||||
|
||||
elif dc_id == 5:
|
||||
return f"{text}`新加坡`"
|
||||
|
||||
else:
|
||||
return "❗️ 无法获取该用户/群组的数据中心 ..."
|
||||
|
||||
|
||||
def get_fullname(user: User) -> str:
|
||||
if user:
|
||||
if user.last_name:
|
||||
return f"{user.first_name} {user.last_name}"
|
||||
return user.first_name
|
||||
|
||||
else:
|
||||
return "Anonymous"
|
||||
|
||||
|
||||
def get_default_pkg(user: User) -> Tuple[str]:
|
||||
if user.username:
|
||||
return f"@{user.username} 的贴纸包(1)", f"{user.username}_1"
|
||||
|
||||
return f"@{user.first_name} 的贴纸包(1)", f"tmp_{user.id}_1"
|
||||
|
||||
|
||||
def is_deleted_id(msg: Message) -> bool:
|
||||
return bool(msg.message_id > 1 and msg.from_user and msg.from_user.is_self)
|
||||
|
||||
|
||||
def get_cmd_error(cmd: str) -> str:
|
||||
return f"Use `-help {cmd.replace('-', '', 1)}` to view detailed usage."
|
||||
|
||||
|
||||
async def check_if_package_existed(pkg_name: str) -> bool:
|
||||
"""检测贴纸包是否存在
|
||||
|
||||
Args:
|
||||
pkg_name (`str`): 贴纸包名字
|
||||
|
||||
Raises:
|
||||
ValueError: 无法检测贴纸包是否存在
|
||||
|
||||
Returns:
|
||||
bool: `True`为贴纸包存在,`False`为贴纸包不存在
|
||||
"""
|
||||
async with session.get(
|
||||
f'https://t.me/addstickers/{pkg_name}', timeout=9.9,
|
||||
) as resp:
|
||||
if resp.status == 200:
|
||||
soup = BeautifulSoup(await resp.text(), 'lxml')
|
||||
target = soup.find(
|
||||
'div', class_='tgme_page_description').text.strip()
|
||||
return not bool(STICKER_DESCRIP == target)
|
||||
else:
|
||||
resp.raise_for_status()
|
||||
|
||||
raise ValueError("Can't check if sticker package is existed.")
|
||||
|
||||
|
||||
async def emoji_sender(
|
||||
cli: Client,
|
||||
chat_id: Union[int, str],
|
||||
msg_id: int,
|
||||
emoji: str = '',
|
||||
) -> bool:
|
||||
try:
|
||||
await cli.send_reaction(chat_id, msg_id, emoji)
|
||||
except FloodWait as e:
|
||||
raise e
|
||||
except RPCError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
async def delete_this(msg: Message) -> None:
|
||||
try:
|
||||
await msg.delete()
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
await logger.complete()
|
||||
|
||||
|
||||
async def basher(cmd: str, timeout: int = 10) -> Dict[str, Any]:
|
||||
return await asyncio.wait_for(execute(cmd), timeout=timeout)
|
||||
|
||||
|
||||
async def execute(command: str) -> Dict[str, Any]:
|
||||
executor = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
try:
|
||||
stdout, stderr = await executor.communicate()
|
||||
except Exception as e:
|
||||
return {'output': '', 'error': str(e)}
|
||||
else:
|
||||
return {
|
||||
'output': stdout.decode('utf-8', 'ignore'),
|
||||
'error': stderr.decode('utf-8', 'ignore')
|
||||
}
|
||||
|
||||
|
||||
async def show_error(msg: Message, e: Any) -> None:
|
||||
await msg.edit_text(f"⚠️ Error\n```{e}```")
|
||||
|
||||
|
||||
async def kick_one(cli: Client, cid: Union[int, str], uid: Union[int, str]):
|
||||
me = await cli.get_chat_member(cid, 'me')
|
||||
if me.can_restrict_members and await cli.ban_chat_member(cid, uid):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def escape_markdown(text: str, version: int = 1, entity_type: str = None) -> str:
|
||||
"""
|
||||
Helper function to escape telegram markup symbols.
|
||||
Args:
|
||||
text (:obj:`str`): The text.
|
||||
version (:obj:`int` | :obj:`str`): Use to specify the version of telegrams Markdown.
|
||||
Either ``1`` or ``2``. Defaults to ``1``.
|
||||
entity_type (:obj:`str`, optional): For the entity types ``PRE``, ``CODE`` and the link
|
||||
part of ``TEXT_LINKS``, only certain characters need to be escaped in ``MarkdownV2``.
|
||||
See the official API documentation for details. Only valid in combination with
|
||||
``version=2``, will be ignored else.
|
||||
"""
|
||||
if int(version) == 1:
|
||||
escape_chars = r'_*`['
|
||||
elif int(version) == 2:
|
||||
if entity_type in ['pre', 'code']:
|
||||
escape_chars = r'\`'
|
||||
elif entity_type == 'text_link':
|
||||
escape_chars = r'\)'
|
||||
else:
|
||||
escape_chars = r'_*[]()~`>#+-=|{}.!'
|
||||
else:
|
||||
raise ValueError('Markdown version must be either 1 or 2!')
|
||||
|
||||
return re.sub(f'([{re.escape(escape_chars)}])', r'\\\1', text)
|
26
tools/initializer.py
Normal file
26
tools/initializer.py
Normal file
@ -0,0 +1,26 @@
|
||||
from loguru import logger
|
||||
import sys
|
||||
|
||||
|
||||
def init_logger():
|
||||
# log config
|
||||
logger.add(
|
||||
sys.stderr,
|
||||
format="{time} {level} {message}",
|
||||
filter="my_module",
|
||||
level="INFO",
|
||||
enqueue=True
|
||||
)
|
||||
logger.add(
|
||||
'./data/log/debug.app.log',
|
||||
filter=lambda record: record["level"].name == "DEBUG",
|
||||
level="DEBUG",
|
||||
enqueue=True,
|
||||
retention="21 days"
|
||||
)
|
||||
logger.add(
|
||||
'./data/log/info.app.log',
|
||||
filter=lambda record: record["level"].name != "DEBUG",
|
||||
level="INFO",
|
||||
enqueue=True
|
||||
)
|
6
tools/sessions.py
Normal file
6
tools/sessions.py
Normal file
@ -0,0 +1,6 @@
|
||||
import aiohttp
|
||||
|
||||
session = aiohttp.ClientSession(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"
|
||||
})
|
252
tools/speedtests.py
Normal file
252
tools/speedtests.py
Normal file
@ -0,0 +1,252 @@
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
from datetime import datetime
|
||||
from os import path
|
||||
from time import time
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
from loguru import logger
|
||||
|
||||
from tools.constants import INSTALL_SPEEDTEST, SPEEDTEST_PATH_FILE
|
||||
|
||||
from .helpers import basher
|
||||
|
||||
|
||||
class Speedtester:
|
||||
def __init__(self) -> None:
|
||||
pass
|
||||
|
||||
async def __aenter__(self):
|
||||
self._timer = time()
|
||||
logger.info("Speedtest start ...")
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
logger.info(
|
||||
f"Speedtest over and takes {time()-self._timer:.5f} seconds.")
|
||||
|
||||
async def running(self, cmd: str) -> Tuple[str]:
|
||||
"""开始执行speedtest
|
||||
|
||||
Args:
|
||||
cmd (str, optional): speedtest的完整指令,需要返回json格式.
|
||||
Defaults to 'speedtest-cli --share --json'.
|
||||
|
||||
Returns:
|
||||
Tuple[str]: 第一个值是文本/错误,第二个是图片link
|
||||
"""
|
||||
await self.init_for_speedtest()
|
||||
# 超时报错
|
||||
res = await basher(cmd, timeout=60)
|
||||
logger.info(f"Speedtest Execution | {res}")
|
||||
|
||||
try:
|
||||
# output result
|
||||
self.__output: Dict[str, Any] = json.loads(res.get('output'))
|
||||
self.__server: Dict[str, Any] = self.__output.get('server')
|
||||
except Exception:
|
||||
return f"⚠️ Error\n```{res.get('error')}```", ''
|
||||
else:
|
||||
text = "**Speedtest**\n" \
|
||||
f"Server: {self.get_server()}\n" \
|
||||
f"Sponsor: {self.get_sponsor()}\n" \
|
||||
f"Upload: {self.get_speed('upload')}\n" \
|
||||
f"Download: {self.get_speed('download')}\n" \
|
||||
f"jitter: {self.get_ping('jitter')}\n" \
|
||||
f"Latency: {self.get_ping('latency')}\n" \
|
||||
f"Time: {self.get_time()}"
|
||||
return text, f"{self.__output.get('result').get('url')}.png"
|
||||
|
||||
async def list_servers_ids(self, cmd: str) -> str:
|
||||
await self.init_for_speedtest()
|
||||
res = await basher(cmd, timeout=10)
|
||||
logger.info(f"Speedtest Execution | {res}")
|
||||
if not res.get('error'):
|
||||
try:
|
||||
self.__output: Dict[str, Any] = json.loads(res.get('output'))
|
||||
except (TypeError, AttributeError, json.decoder.JSONDecodeError):
|
||||
return "⚠️ Unable to get ids of servers"
|
||||
else:
|
||||
tmp = '\n'.join(
|
||||
f"`{k.get('id')}` **|** {k.get('name')} **|** {k.get('location')} {k.get('country')}"
|
||||
for k in self.__output.get('servers')
|
||||
)
|
||||
return f"**Speedtest节点列表:**\n{tmp}"
|
||||
return f"⚠️ Error\n```{res.get('error')}```"
|
||||
|
||||
def get_server(self) -> str:
|
||||
location = self.__server.get('location')
|
||||
country = self.__server.get('country')
|
||||
return f"`{location} {country}`"
|
||||
|
||||
def get_sponsor(self) -> str:
|
||||
return f"`{self.__server.get('name')}`"
|
||||
|
||||
def get_speed(self, opt: str) -> str:
|
||||
"""
|
||||
Args:
|
||||
opt (str): upload or download
|
||||
|
||||
Returns:
|
||||
str: Convert to bits
|
||||
"""
|
||||
def convert(bits) -> str:
|
||||
"""Unit conversion"""
|
||||
power = 1000
|
||||
n = 0
|
||||
units = {
|
||||
0: 'bps',
|
||||
1: 'Kbps',
|
||||
2: 'Mbps',
|
||||
3: 'Gbps',
|
||||
4: 'Tbps'
|
||||
}
|
||||
while bits > power:
|
||||
bits = bits / power
|
||||
n = n + 1
|
||||
return f"{bits:.3f} {units.get(n)}"
|
||||
return f"`{convert(self.__output.get(opt).get('bandwidth')*8)}`"
|
||||
|
||||
def get_ping(self, opt: str) -> str:
|
||||
return f"`{self.__output.get('ping').get(opt):.3f}`"
|
||||
|
||||
def get_time(self) -> str:
|
||||
return f"`{datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')}`"
|
||||
|
||||
async def init_for_speedtest(self, opt: str = 'install') -> Optional[str]:
|
||||
exists_file = path.exists(SPEEDTEST_PATH_FILE)
|
||||
arch = platform.uname().machine
|
||||
|
||||
if arch not in ["x86_64", "aarch64"]:
|
||||
text = f"Unsupported System Architecture: {arch}"
|
||||
logger.warning(text)
|
||||
return text
|
||||
|
||||
elif opt == 'install':
|
||||
if not exists_file:
|
||||
await self.__download_file(arch)
|
||||
logger.success("First install speedtest")
|
||||
return
|
||||
|
||||
elif opt == 'update':
|
||||
os.remove(SPEEDTEST_PATH_FILE)
|
||||
await self.__download_file(arch)
|
||||
if path.exists(SPEEDTEST_PATH_FILE):
|
||||
text = "✅ Update speedtest successfully."
|
||||
logger.success(text)
|
||||
return text
|
||||
return "❌ Failed to update speedtest!"
|
||||
|
||||
else:
|
||||
raise ValueError(f'Wrong speedtest option {opt}!')
|
||||
|
||||
async def __download_file(self, arch: str) -> None:
|
||||
await basher(
|
||||
INSTALL_SPEEDTEST.replace('<arch>', arch, 1),
|
||||
timeout=30
|
||||
)
|
||||
|
||||
|
||||
# import asyncio
|
||||
# import json
|
||||
# from datetime import datetime
|
||||
# from time import time
|
||||
# from typing import Any, Dict, Tuple
|
||||
|
||||
# from loguru import logger
|
||||
|
||||
# from .helpers import execute
|
||||
|
||||
|
||||
# class Speedtester:
|
||||
# def __init__(self) -> None:
|
||||
# pass
|
||||
|
||||
# async def __aenter__(self):
|
||||
# self._timer = time()
|
||||
# logger.info(f"Speedtest start in {self._timer}")
|
||||
# return self
|
||||
|
||||
# async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
# logger.info(
|
||||
# f"Speedtest over and takes {time()-self._timer:.5f} seconds.")
|
||||
|
||||
# async def running(self, cmd: str) -> Tuple[str]:
|
||||
# """开始执行speedtest
|
||||
|
||||
# Args:
|
||||
# cmd (str, optional): speedtest的完整指令,需要返回json格式.
|
||||
# Defaults to 'speedtest-cli --share --json'.
|
||||
|
||||
# Returns:
|
||||
# Tuple[str]: 第一个值是文本/错误,第二个是图片link
|
||||
# """
|
||||
# # 超时报错
|
||||
# res = await asyncio.wait_for(execute(cmd), timeout=60)
|
||||
# logger.info(f"Speedtest Execution | {res}")
|
||||
# await logger.complete()
|
||||
|
||||
# if not res.get('error'):
|
||||
# if 'list' in cmd:
|
||||
# return res.get('output'), ''
|
||||
|
||||
# try:
|
||||
# # output result
|
||||
# self.__output: Dict[str, Any] = json.loads(res.get('output'))
|
||||
# self.__server: Dict[str, Any] = self.__output.get('server')
|
||||
# except (TypeError, AttributeError, json.decoder.JSONDecodeError):
|
||||
# return "⚠️ Unable to get detailed data.", ''
|
||||
|
||||
# else:
|
||||
# text = "**Speedtest**\n" \
|
||||
# f"Server: {self.get_server()}\n" \
|
||||
# f"Sponsor: {self.get_sponsor()}\n" \
|
||||
# f"Upload: {self.get_speed('upload')}\n" \
|
||||
# f"Download: {self.get_speed('download')}\n" \
|
||||
# f"Latency: {self.get_latency()}\n" \
|
||||
# f"Time: {self.get_time()}"
|
||||
# return text, self.__output.get('share')
|
||||
|
||||
# return f"⚠️ Error | {res.get('error')}", ''
|
||||
|
||||
# def get_server(self) -> str:
|
||||
# country = self.__server.get('country')
|
||||
# cc = self.__server.get('cc')
|
||||
# return f"`{country} - {cc}`"
|
||||
|
||||
# def get_sponsor(self) -> str:
|
||||
# return f"`{self.__server.get('sponsor')}`"
|
||||
|
||||
# def get_speed(self, option: str) -> str:
|
||||
# """
|
||||
# Args:
|
||||
# option (str): upload or download
|
||||
|
||||
# Returns:
|
||||
# str: Convert to bits
|
||||
# """
|
||||
# return f"`{self.convert(self.__output.get(option))}`"
|
||||
|
||||
# def get_latency(self) -> str:
|
||||
# return f"`{self.__server.get('latency')}`"
|
||||
|
||||
# def get_time(self) -> str:
|
||||
# return f"`{datetime.strftime(datetime.now(), '%Y-%m-%d %H:%M:%S')}`"
|
||||
|
||||
# @staticmethod
|
||||
# def convert(bits) -> str:
|
||||
# """Unit conversion"""
|
||||
# power = 1024
|
||||
# n = 0
|
||||
# units = {
|
||||
# 0: 'bps',
|
||||
# 1: 'Kbps',
|
||||
# 2: 'Mbps',
|
||||
# 3: 'Gbps',
|
||||
# 4: 'Tbps'
|
||||
# }
|
||||
# while bits > power:
|
||||
# bits = bits / power
|
||||
# n = n + 1
|
||||
# return f"{bits:.3f} {units.get(n)}"
|
218
tools/stickers.py
Normal file
218
tools/stickers.py
Normal file
@ -0,0 +1,218 @@
|
||||
import asyncio
|
||||
from math import floor
|
||||
from typing import Optional
|
||||
|
||||
import emoji
|
||||
from loguru import logger
|
||||
from PIL import Image, UnidentifiedImageError
|
||||
from pyrogram import Client
|
||||
from pyrogram.errors import FloodWait, RPCError
|
||||
from pyrogram.types import Message
|
||||
|
||||
from .constants import STICKER_BOT, STICKER_IMG
|
||||
from .helpers import Parameters, delete_this
|
||||
|
||||
|
||||
class StickerLocker:
|
||||
"""贴纸指令🔒"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._lock = asyncio.Lock()
|
||||
|
||||
def get_lock(self) -> None:
|
||||
return self._lock
|
||||
|
||||
|
||||
sticker_locker = StickerLocker()
|
||||
|
||||
|
||||
class StickerEvent:
|
||||
"""贴纸对话事件"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cond = asyncio.Condition()
|
||||
|
||||
def get_response(self) -> asyncio.Condition:
|
||||
return self._cond
|
||||
|
||||
def wait(self):
|
||||
return self._cond.wait()
|
||||
|
||||
def notify(self):
|
||||
return self._cond.notify()
|
||||
|
||||
|
||||
sticker_cond = StickerEvent()
|
||||
|
||||
|
||||
class StickerAdder:
|
||||
"""
|
||||
新增贴纸的指令`-s`实现详细步骤:
|
||||
1、解禁机器人(`@Stickers`)
|
||||
2、发送/cancel
|
||||
3、检测目标消息是否为贴纸
|
||||
是:
|
||||
3-1、转发贴纸到@Stickerss
|
||||
3-2、获取并发送emoji
|
||||
3-3、发送/done
|
||||
3-4、结束指令
|
||||
4、若不是,则检测是否为图片或图片格式的文件
|
||||
1、转化图片
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, cli: Client, msg: Message) -> None:
|
||||
self._cli = cli
|
||||
self._msg = msg
|
||||
self._bot_id = STICKER_BOT
|
||||
self._count = 0
|
||||
|
||||
def is_finished(self, pkg_existed: bool) -> bool:
|
||||
return (pkg_existed and self._count == 6) or \
|
||||
(not pkg_existed and self._count == 8)
|
||||
|
||||
async def do_cancel(self) -> None:
|
||||
"""取消原指令残留效果"""
|
||||
self._count = 0
|
||||
await self.send_message('/cancel')
|
||||
|
||||
async def send_message(self, text: str) -> None:
|
||||
"""发送指令(或`emoji`)给贴纸"""
|
||||
|
||||
try:
|
||||
await self._cli.send_message(
|
||||
self._bot_id, text,
|
||||
disable_notification=True)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await self._cli.send_message(
|
||||
self._bot_id, text,
|
||||
disable_notification=True)
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
else:
|
||||
await self.__wait_for()
|
||||
|
||||
async def send_emoji(self) -> None:
|
||||
if self._msg.reply_to_message.sticker:
|
||||
an_emoji = self._msg.reply_to_message.sticker.emoji
|
||||
else:
|
||||
_, arg = Parameters.get(self._msg)
|
||||
if emoji.is_emoji(arg):
|
||||
an_emoji = arg
|
||||
else:
|
||||
an_emoji = '⚡️'
|
||||
await self.send_message(an_emoji)
|
||||
|
||||
async def send_retries(self, n: int) -> None:
|
||||
try:
|
||||
retry_text = f"⚠️ Retrying {n+1} times ..."
|
||||
await self._msg.edit_text(retry_text)
|
||||
logger.warning(retry_text)
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
finally:
|
||||
await logger.complete()
|
||||
|
||||
async def upload_photo(self) -> Optional[bool]:
|
||||
"""下载图片/图片文件,修剪后发送至@Stickers"""
|
||||
img = await self._msg.reply_to_message.download(STICKER_IMG)
|
||||
if not img:
|
||||
return True
|
||||
try:
|
||||
resize_image(img)
|
||||
except UnidentifiedImageError as e:
|
||||
logger.warning(e)
|
||||
return True
|
||||
|
||||
try:
|
||||
await self._cli.send_document(
|
||||
self._bot_id, document=img)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await self._cli.send_document(
|
||||
self._bot_id, document=img)
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
else:
|
||||
await self.__wait_for()
|
||||
|
||||
async def edit_text(self, text, parse_mode: Optional[str] = None) -> None:
|
||||
"""编辑消息"""
|
||||
try:
|
||||
await self._msg.edit_text(text, parse_mode=parse_mode)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await self._msg.edit_text(text, parse_mode=parse_mode)
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
|
||||
async def done(self, text: str, parse_mode: Optional[str] = None) -> None:
|
||||
try:
|
||||
await self.edit_text(text, parse_mode=parse_mode)
|
||||
await asyncio.sleep(3.5)
|
||||
await delete_this(self._msg)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await self.edit_text(text, parse_mode=parse_mode)
|
||||
await asyncio.sleep(3.5)
|
||||
await delete_this(self._msg)
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
|
||||
async def mark_as_read(self) -> None:
|
||||
"""自动已读机器人的消息"""
|
||||
try:
|
||||
await self._cli.read_history(self._bot_id)
|
||||
except FloodWait as e:
|
||||
await asyncio.sleep(e.x)
|
||||
await self._cli.read_history(self._bot_id)
|
||||
except RPCError as e:
|
||||
logger.warning(e)
|
||||
|
||||
async def __wait_for(self) -> None:
|
||||
"""等待贴纸机器人(`@Stickers`)的回应"""
|
||||
async with sticker_cond.get_response():
|
||||
await asyncio.wait_for(sticker_cond.wait(), timeout=5)
|
||||
logger.debug(
|
||||
f"Counter of response from @Stickers is {self._count}"
|
||||
)
|
||||
self._count = self._count + 1
|
||||
await self.mark_as_read()
|
||||
|
||||
|
||||
def resize_image(photo: str):
|
||||
with Image.open(photo) as img:
|
||||
maxsize = (512, 512)
|
||||
if img.width < 512 or img.height < 512:
|
||||
w = img.width
|
||||
h = img.height
|
||||
if w > h:
|
||||
scale = 512 / w
|
||||
size1new = 512
|
||||
size2new = h * scale
|
||||
else:
|
||||
scale = 512 / h
|
||||
size1new = w * scale
|
||||
size2new = 512
|
||||
size_new = (floor(size1new), floor(size2new))
|
||||
img = img.resize(size_new)
|
||||
else:
|
||||
img.thumbnail(maxsize)
|
||||
img.save(photo, format='png')
|
||||
return
|
||||
|
||||
|
||||
def isEmoji(content):
|
||||
if not content:
|
||||
return False
|
||||
if u"\U0001F600" <= content <= u"\U0001F64F":
|
||||
return True
|
||||
elif u"\U0001F300" <= content <= u"\U0001F5FF":
|
||||
return True
|
||||
elif u"\U0001F680" <= content <= u"\U0001F6FF":
|
||||
return True
|
||||
elif u"\U0001F1E0" <= content <= u"\U0001F1FF":
|
||||
return True
|
||||
else:
|
||||
return False
|
76
tools/storage.py
Normal file
76
tools/storage.py
Normal file
@ -0,0 +1,76 @@
|
||||
import asyncio
|
||||
import pickle
|
||||
from os import mkdir, path
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
class SimpleStore:
|
||||
"""简单的存一些东西,凑合用用"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
file_name: str = './data/app.pickle',
|
||||
auto_flush: bool = True
|
||||
) -> None:
|
||||
self.__lock = asyncio.Lock()
|
||||
self.__file_name = file_name
|
||||
self.__auto_flush = auto_flush
|
||||
|
||||
try:
|
||||
self.__store = pickle.load(open(file_name, 'rb'), encoding='utf-8')
|
||||
except EOFError:
|
||||
self.__store = {}
|
||||
except FileNotFoundError:
|
||||
self.__store = {}
|
||||
if not path.exists("./data"):
|
||||
mkdir('./data')
|
||||
pickle.dump({}, open(file_name, 'wb'))
|
||||
except Exception as e:
|
||||
raise e
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.__lock.acquire()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.__auto_flush:
|
||||
self.flush()
|
||||
|
||||
if self.__lock.locked():
|
||||
self.__lock.release()
|
||||
|
||||
def get_lock(self) -> asyncio.Lock:
|
||||
return self.__lock
|
||||
|
||||
@property
|
||||
def data(self) -> Dict[str, Any]:
|
||||
return self.__store
|
||||
|
||||
def get_data(self, key: Any) -> Dict: # , typed: Any
|
||||
if self.__store.get(key):
|
||||
return self.__store[key]
|
||||
else:
|
||||
self.__store[key] = {}
|
||||
return self.__store[key]
|
||||
|
||||
# def update(self, data: Dict):
|
||||
# return self.__store.update(data)
|
||||
|
||||
# def clear(self) -> Dict:
|
||||
# self.__store.clear()
|
||||
|
||||
# def getter(self, key: Any) -> Any:
|
||||
# return self.__store.get(key)
|
||||
|
||||
# def setter(self, key: Any, value: Any) -> None:
|
||||
# self.__store[key] = value
|
||||
|
||||
# def deleter(self, key: Any) -> Optional[Any]:
|
||||
# return self.__store.pop(key, None)
|
||||
|
||||
def flush(self):
|
||||
"""更新数据并持久化到pickle文件"""
|
||||
pickle.dump(self.__store, open(self.__file_name, 'wb'))
|
||||
|
||||
|
||||
# storage = SimpleStorage('./data/app.pickle')
|
Loading…
Reference in New Issue
Block a user