Spaces:
Paused
Paused
File size: 9,683 Bytes
65651b1 61d6e2a 65651b1 53e3df0 848377f de342dc 61d6e2a c53c2ab 2e79f82 848377f 53e3df0 61d6e2a c53c2ab 61d6e2a 2e79f82 848377f 109b8ca 2e79f82 109b8ca 2e79f82 c71e4ac 65651b1 848377f 65651b1 849f335 61d6e2a 849f335 61d6e2a 848377f 53e3df0 61d6e2a 65651b1 61d6e2a de342dc 848377f de342dc 1dfae7a de342dc 1dfae7a de342dc 61d6e2a 65651b1 61d6e2a 65651b1 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 | """/start /help /cancel /export /userbot, plus the routers for free text and inline-button presses (/import is in backup.py)."""
from __future__ import annotations
import io
import json
import logging
from telethon import events
from ..common import Ctx, UserError, cmd, clear_state, discard_temp, guard, say, show
from ..picker import PAGE_CB
from ..richmsg import RICH_HELP, RICH_HELP_HTML
from ..richtext import CHEAT_SHEET
from ..tgutil import esc
log = logging.getLogger(__name__)
HELP = (
"<b>Channel Post Bot</b>\n\n"
"<b>Posts</b>\n"
"/new - create a post (text or media, with buttons)\n"
"/posts - your posts and drafts: edit text, caption, media and buttons, or delete them - a published post is deleted in the channel and in My posts (a button post also has “Mass replace links”)\n"
"/autopost 114 20 - post “Episodes 01 to 20”, “21 to 40” ... up to 114, each with a download button, then fill in the links\n"
"/formatting - bold, italic, links, quotes ...: Telegram's own formatting is kept, or type HTML / Markdown instead\n"
"/rich - rich messages: headings, lists, tables, expandable sections, math, maps, pictures, buttons ... typed as HTML / Markdown, or sent as a Telegram rich message\n"
"/cancel - stop what you're doing\n\n"
"<b>Channels</b>\n"
"/addchannel - register a channel (the bot must be an admin there)\n"
"/channels - list your channels, or take one out (the bot asks whether its saved posts in My posts are deleted too)\n\n"
"<b>Owner tools</b>\n"
"/broadcast [time] - send one message to all channels; with a time (50m, 1h, 5d) it is deleted again from all channels and My posts afterwards\n"
"/replace <post link> - reply to a new media message to replace that post's media and caption (where Telegram refuses the bot, the userbot does it)\n"
"/swap @old @new - swap the username inside t.me links (button links and hyperlinks) in all channels\n"
"/handleswap @old @new - change the username @old written in posts (text, captions, button names) to @new in all channels; links stay\n"
"/undo - undo the last /swap or /handleswap\n"
"/repost @old @new - copy every post of a channel in order (links swapped, replies, pins and links between posts kept), then delete the old posts\n"
"/shift @source @destination [from_id] [to_id] [-c] [-all] [--no-media] - copy posts from one channel into another (buttons kept, listed in My posts); "
"a channel Telegram restricts (copyright strike) is copied from My posts; "
"-c: links to the source's posts in all your channels are changed to the copies; -all: the destination also gets the source's name, description and photo; "
"--no-media: no files, only the text and buttons\n"
"/sync - compare every channel with My posts right now (posts deleted, edited or added by others, the older posts from before the bot was added; service messages are deleted)\n"
"/userbot - status of the helper account that, by itself and without asking, deletes and edits the posts the bot is not allowed to\n"
"/export - download a JSON backup of your channels and posts\n"
"/import - put a backup made by /export back (only adds: nothing that exists is changed or deleted)"
"\n/admins - list admins (owner)\n/addadmin 123456789 - add an admin (owner)\n/removeadmin 123456789 - remove an admin (owner)"
)
def register(ctx: Ctx) -> None:
client, db = ctx.client, ctx.db
@client.on(cmd("admins"))
@guard(ctx, owner=True)
async def h_admins(event):
ids = sorted(ctx.admins | set(ctx.cfg.admins))
await say(event, "Admins: " + (", ".join(map(str, ids)) if ids else "none"))
@client.on(cmd("addadmin"))
@guard(ctx, owner=True)
async def h_addadmin(event):
parts = (event.raw_text or "").split()
if len(parts) != 2 or not parts[1].isdigit() or int(parts[1]) <= 0:
raise UserError("Use /addadmin followed by a numeric Telegram user ID.")
uid = int(parts[1])
if ctx.cfg.is_owner(uid):
raise UserError("That user is already an owner.")
if not await db.add_admin(uid):
await say(event, f"{uid} is already an admin.")
return
ctx.admins.add(uid)
await say(event, f"Added {uid} as an admin.")
@client.on(cmd("removeadmin"))
@guard(ctx, owner=True)
async def h_removeadmin(event):
parts = (event.raw_text or "").split()
if len(parts) != 2 or not parts[1].isdigit() or int(parts[1]) <= 0:
raise UserError("Use /removeadmin followed by a numeric Telegram user ID.")
uid = int(parts[1])
if not await db.remove_admin(uid):
await say(event, f"{uid} is not an admin.")
return
ctx.admins.discard(uid)
await say(event, f"Removed {uid} as an admin.")
@client.on(cmd("start"))
@client.on(cmd("help"))
@guard(ctx)
async def h_help(event):
await say(event, HELP)
@client.on(cmd("formatting"))
@guard(ctx)
async def h_formatting(event):
await say(event, CHEAT_SHEET)
@client.on(cmd("rich"))
@guard(ctx)
async def h_rich(event):
await say(event, RICH_HELP)
await say(event, RICH_HELP_HTML)
@client.on(cmd("cancel"))
@guard(ctx)
async def h_cancel(event):
await discard_temp(ctx, event.sender_id)
clear_state(ctx, event.sender_id)
await say(event, "Cancelled.")
@client.on(cmd("export"))
@guard(ctx, owner=True)
async def h_export(event):
data = await db.export_all()
buf = io.BytesIO(json.dumps(data, ensure_ascii=False, indent=1).encode("utf-8"))
buf.name = "channel-posts-export.json"
await event.respond(
f"📦 Backup: {len(data['channels'])} channel(s), {len(data['posts'])} post(s). "
"To put it back (on this bot or a new one), send /import and then this file.",
file=buf,
)
@client.on(cmd("userbot"))
@guard(ctx, owner=True)
async def h_userbot(event):
ub = ctx.userbot
if ub is None or not ub.enabled:
await say(
event,
"🤖 <b>Userbot: not set up.</b>\n"
"The bot does its work itself first. If Telegram refuses - deleting posts older than 48 hours, or "
"editing a post that belongs to another bot (/replace, text and caption edits) - a helper account, "
"the userbot, takes over by itself, without asking you each time: it joins the channel through an "
"invite link the bot makes, the bot gives it the “Delete messages” or “Edit messages of others” right "
"if it has the “Add new admins” right, and gives the rights back afterwards.\n\n"
"To set it up: on your own computer run <code>python make_userbot_session.py</code> with a "
"<b>separate</b> Telegram account, and put the printed text into the bot's secrets as "
"<code>USERBOT_SESSION</code>. Then send /start to this bot from that account (new media reaches the "
"userbot through its chat with the bot - that is what makes a replace quick). See the README.",
)
return
if not ub.ready:
await ub.connect()
kept = "stays an admin" if ub.keep_admin else "is made admin only while it deletes or edits"
await say(
event,
f"🤖 <b>Userbot:</b> {esc(ub.describe())}\nIt {kept} (USERBOT_KEEP_ADMIN).\n"
"It must have started this bot (/start from that account): new media for an edit goes through its chat "
"with the bot, which is what makes a big file as quick as a small one.",
)
@client.on(
events.NewMessage(incoming=True, func=lambda e: e.is_private and not (e.raw_text or "").startswith("/"))
)
@guard(ctx)
async def h_free_text(event):
st = ctx.state.get(event.sender_id)
if not st:
await say(event, "Use /new to create a post or /posts to manage your posts. /help lists everything.")
return
fn = ctx.text_handlers.get(st["mode"])
if fn is None:
clear_state(ctx, event.sender_id)
await say(event, "That step is no longer active. Start again with /new.")
return
await fn(event, st)
@client.on(events.CallbackQuery())
@guard(ctx)
async def h_callback(event):
try:
await event.answer() # stop the loading spinner right away
except Exception:
pass
data = event.data.decode("utf-8", "replace")
prefix, *parts = data.split(":")
fn = ctx.callbacks.get(prefix)
if fn is None:
await event.respond("This button is no longer active. Use /new or /posts.")
return
await fn(event, parts)
async def cb_noop(event, parts):
await event.answer()
async def cb_cancel(event, parts):
uid = event.sender_id
await discard_temp(ctx, uid)
clear_state(ctx, uid)
await show(event, "Cancelled.")
async def cb_channel_page(event, parts):
"""◀ ▶ under a channel chooser: draw the same chooser again on another page."""
kind, arg, page = parts[0], parts[1], int(parts[2])
draw = ctx.pickers.get(kind)
if draw is None:
raise UserError("This list is no longer active. Start the command again.")
await draw(event, arg, page)
ctx.callbacks["noop"] = cb_noop
ctx.callbacks["cx"] = cb_cancel
ctx.callbacks[PAGE_CB] = cb_channel_page
|