From 836694df48bbff4c076d04c54f9d5f0844386b75 Mon Sep 17 00:00:00 2001 From: iwumingz Date: Wed, 6 Apr 2022 22:39:27 +0800 Subject: [PATCH] Release --- .github/workflows/docker-image.yml | 82 ++++++++++ .gitignore | 8 + Dockerfile | 16 ++ README.md | 15 +- core/__init__.py | 13 ++ core/custom.py | 37 +++++ data/command.yml | 82 ++++++++++ install.sh | 154 ++++++++++++++++++ main.py | 6 + plugins/__init__.py | 0 plugins/calculate.py | 29 ++++ plugins/cc.py | 107 ++++++++++++ plugins/dc.py | 15 ++ plugins/dme.py | 66 ++++++++ plugins/forward.py | 54 +++++++ plugins/ghost.py | 58 +++++++ plugins/help.py | 24 +++ plugins/info.py | 23 +++ plugins/kick.py | 54 +++++++ plugins/note.py | 63 ++++++++ plugins/other.py | 53 ++++++ plugins/pingdc.py | 29 ++++ plugins/rate.py | 50 ++++++ plugins/shell.py | 42 +++++ plugins/speedtest.py | 63 ++++++++ plugins/sticker.py | 157 ++++++++++++++++++ plugins/sysinfo.py | 14 ++ plugins/trace.py | 97 +++++++++++ requirements.txt | Bin 0 -> 780 bytes tools/__init__.py | 0 tools/constants.py | 55 +++++++ tools/ghosts.py | 20 +++ tools/helpers.py | 200 +++++++++++++++++++++++ tools/initializer.py | 26 +++ tools/sessions.py | 6 + tools/speedtests.py | 252 +++++++++++++++++++++++++++++ tools/stickers.py | 218 +++++++++++++++++++++++++ tools/storage.py | 76 +++++++++ 38 files changed, 2263 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docker-image.yml create mode 100644 Dockerfile create mode 100644 core/__init__.py create mode 100644 core/custom.py create mode 100644 data/command.yml create mode 100644 install.sh create mode 100644 main.py create mode 100644 plugins/__init__.py create mode 100644 plugins/calculate.py create mode 100644 plugins/cc.py create mode 100644 plugins/dc.py create mode 100644 plugins/dme.py create mode 100644 plugins/forward.py create mode 100644 plugins/ghost.py create mode 100644 plugins/help.py create mode 100644 plugins/info.py create mode 100644 plugins/kick.py create mode 100644 plugins/note.py create mode 100644 plugins/other.py create mode 100644 plugins/pingdc.py create mode 100644 plugins/rate.py create mode 100644 plugins/shell.py create mode 100644 plugins/speedtest.py create mode 100644 plugins/sticker.py create mode 100644 plugins/sysinfo.py create mode 100644 plugins/trace.py create mode 100644 requirements.txt create mode 100644 tools/__init__.py create mode 100644 tools/constants.py create mode 100644 tools/ghosts.py create mode 100644 tools/helpers.py create mode 100644 tools/initializer.py create mode 100644 tools/sessions.py create mode 100644 tools/speedtests.py create mode 100644 tools/stickers.py create mode 100644 tools/storage.py diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..6152852 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index b6e4761..7858701 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,11 @@ dmypy.json # Pyre type checker .pyre/ + + +data/app* +data/config* +data/log* +data/img* +*.debug.sh +READ.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..12972ae --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 3751bed..a3c6273 100644 --- a/README.md +++ b/README.md @@ -1 +1,14 @@ -# sycgram \ No newline at end of file +# sycgram + +## 安装与更新 + +```shell +# 如果选择的是安装,则安装成功后,可先使用Ctrl+P,然后使用Ctrl+Q挂到后台运行 +bash <(curl -fsL "https://raw.githubusercontent.com/iwumingz/sycgram/main/install.sh") +``` + +## 注意事项 + +- 脚本仅适用于Ubuntu/Debian +- 按个人需求随缘更,仅用于学习用途。 +- 如果号码等输入错误了,重新安装即可 diff --git a/core/__init__.py b/core/__init__.py new file mode 100644 index 0000000..81a1d10 --- /dev/null +++ b/core/__init__.py @@ -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', +) diff --git a/core/custom.py b/core/custom.py new file mode 100644 index 0000000..3e80df4 --- /dev/null +++ b/core/custom.py @@ -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) diff --git a/data/command.yml b/data/command.yml new file mode 100644 index 0000000..1d78418 --- /dev/null +++ b/data/command.yml @@ -0,0 +1,82 @@ +help: + format: -help + usage: 指令列表 + +note: + format: -note <序号> 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 + usage: 直接使用。开启ghost的对话会被自动标记为已读 + +id: + format: -id + usage: 回复一条消息或直接使用,查看对话及消息的ID + +sb: + format: -sb + usage: 回复一条消息,将在所有共同且拥有管理踢人权限的群组中踢出目标消息的主人 + +dc: + format: -dc + usage: 回复一条消息,或者直接使用。查看目标消息或当前对话的DC区 + +pingdc: + format: -pingdc + usage: 测试与各个DC的延时 + +ex: + format: -ex <数字> + usage: 汇率转换 + +speedtest: + format: -speedtest <无|节点ID|list|update> + usage: 服务器本地网络测速 + +s: + format: -s <无|emoji> or -s + usage: + 收集回复的贴纸/图片/图片文件消息。直接使用时,可以设置默认贴纸包标题&名字; + 回复使用时,可以指定emoji,不指定则使用默认emoji + +trace: + format: -trace + usage: 回复一条消息,当目标消息的主人发消息时,自动丢。默认:💩 + +cc: + format: -cc <数量> or -cc + usage: 回复使用:遍历该消息的主人发过的消息并丢<数量>个给Ta;直接使用: + 指令为默认emoji。数量范围:1 ~ 233,Emoji默认为:💩 + +cal: + format: -cal <四则运算式> + usage: 直接使用。默认除法精确到小数点后4位 + +sh: + format: -sh + usage: 直接使用 + +sysinfo: + format: -sysinfo + usage: 直接使用,查看系统信息 + +diss: + format: -diss or -biss + usage: 喷子语录 + +tg: + format: -biss + usage: 舔狗语录 diff --git a/install.sh b/install.sh new file mode 100644 index 0000000..7017778 --- /dev/null +++ b/install.sh @@ -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 < or -cc + usage: + 回复使用:遍历该消息的主人发过的消息并丢<数量>个给Ta;直接使用: + 指令为默认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) diff --git a/plugins/dc.py b/plugins/dc.py new file mode 100644 index 0000000..3ce47d4 --- /dev/null +++ b/plugins/dc.py @@ -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)) diff --git a/plugins/dme.py b/plugins/dme.py new file mode 100644 index 0000000..3aec746 --- /dev/null +++ b/plugins/dme.py @@ -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() diff --git a/plugins/forward.py b/plugins/forward.py new file mode 100644 index 0000000..30ca1f1 --- /dev/null +++ b/plugins/forward.py @@ -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) diff --git a/plugins/ghost.py b/plugins/ghost.py new file mode 100644 index 0000000..23ca332 --- /dev/null +++ b/plugins/ghost.py @@ -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) diff --git a/plugins/help.py b/plugins/help.py new file mode 100644 index 0000000..32f07d7 --- /dev/null +++ b/plugins/help.py @@ -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 = 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 ` **查看某指令的详细用法**" + + 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') diff --git a/plugins/info.py b/plugins/info.py new file mode 100644 index 0000000..6727527 --- /dev/null +++ b/plugins/info.py @@ -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) diff --git a/plugins/kick.py b/plugins/kick.py new file mode 100644 index 0000000..ea8fce1 --- /dev/null +++ b/plugins/kick.py @@ -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() diff --git a/plugins/note.py b/plugins/note.py new file mode 100644 index 0000000..036e640 --- /dev/null +++ b/plugins/note.py @@ -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 <序号> + 用法二:-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() diff --git a/plugins/other.py b/plugins/other.py new file mode 100644 index 0000000..2b04626 --- /dev/null +++ b/plugins/other.py @@ -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) diff --git a/plugins/pingdc.py b/plugins/pingdc.py new file mode 100644 index 0000000..6f610b2 --- /dev/null +++ b/plugins/pingdc.py @@ -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" + ) diff --git a/plugins/rate.py b/plugins/rate.py new file mode 100644 index 0000000..985676e --- /dev/null +++ b/plugins/rate.py @@ -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 """ + 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 diff --git a/plugins/shell.py b/plugins/shell.py new file mode 100644 index 0000000..690a99f --- /dev/null +++ b/plugins/shell.py @@ -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' + ) diff --git a/plugins/speedtest.py b/plugins/speedtest.py new file mode 100644 index 0000000..91fb1f8 --- /dev/null +++ b/plugins/speedtest.py @@ -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) diff --git a/plugins/sticker.py b/plugins/sticker.py new file mode 100644 index 0000000..2cfd83b --- /dev/null +++ b/plugins/sticker.py @@ -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 回复一条消息 + 用法二:-s 切换默认贴纸包标题和名字 + 作用:偷为静态贴纸(对象:贴纸/图片/图片文件) + """ + _, 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