Spaces:
Paused
Paused
File size: 8,800 Bytes
2e79f82 65651b1 c71e4ac 65651b1 c71e4ac 2e79f82 65651b1 2e79f82 c71e4ac 2e79f82 65651b1 2e79f82 65651b1 2e79f82 65651b1 2e79f82 c71e4ac 2e79f82 c71e4ac 65651b1 c71e4ac 65651b1 2e79f82 65651b1 c71e4ac 2e79f82 65651b1 c71e4ac 2e79f82 65651b1 2e79f82 65651b1 2e79f82 | 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 | """/shift: copy posts of one channel into another channel - text, formatting, media, albums, buttons and replies
- and register the copies in My posts. The source is only read, never changed or deleted.
It is the /repost copy loop pointed at two channels. Links from a shifted post to another post of the source channel
follow the content: when that post was shifted too, the link points at its copy in the destination.
Three optional extras are remembered with the shift (job marks), so that continuing a stopped shift keeps them:
-c afterwards every link to a post of the source, in ANY connected channel, is pointed at the copy
(crosslinks.relink_after_shift; the source itself is only read);
-all afterwards the destination gets the source's name, description and profile photo
(profile_copy.copy_profile);
--no-media the copies get no files: every post is copied as its text, with its formatting and buttons (mark "nomedia";
repost_engine.without_files). A post that is only a file has nothing to copy and is left out.
When Telegram hides the first new copies (read back right after posting) the shift stops by itself - more copies would
only be more posts nobody can see - and goes on only when the owner presses Continue (mark "hiddenok").
A source that Telegram restricts (a copyright strike: "This channel is unavailable due to copyright infringement", its
posts marked or shown as a notice) is copied from what My posts saved of its posts - every post that has a saved copy,
however intact Telegram's version looks (repost_engine.copy_source). A post without a saved copy is copied as Telegram
hands it out if that is the post itself, and skipped if it is only the notice. A source that can't be read at all (a
banned or private channel) is copied from My posts alone: mark "fromdb", set when the plan was made that way.
"""
from __future__ import annotations
import logging
from types import SimpleNamespace
from typing import Any, Awaitable, Callable, Optional
from .crosslinks import CrossResult, relink_after_shift, shift_links
from .profile_copy import copy_profile
from .repost_engine import (
STOP_IF_HIDDEN,
CopyJob,
DeleteResult,
RepostOptions,
RepostPlan,
RepostResult,
copy_loop,
delete_pending,
plan_from_saved,
relink_copies,
saved_lookup,
saved_units,
)
from .restrictions import probe_channel
from .tgutil import peer_of
log = logging.getLogger(__name__)
def source_of(shift) -> SimpleNamespace:
"""The source channel as stored with the shift (it need not be registered in the bot)."""
return SimpleNamespace(
id=shift.src_channel_id, access_hash=shift.src_access_hash, username=shift.src_username, title=shift.src_title
)
async def plan_from_my_posts(
db, src, first_id=None, last_id=None, unreadable=None, no_media: bool = False
) -> Optional[RepostPlan]:
"""The plan of a shift made from My posts alone, for a source that can't be read; None when My posts has nothing
of that channel (in that range)."""
plan = plan_from_saved(
src, await db.sent_posts_from(src.id, first_id or 1), RepostOptions(), first_id=first_id, last_id=last_id,
unreadable=unreadable, no_media=no_media,
)
return None if plan.error else plan
async def run_shift(
client,
db,
shift,
dst,
user_id: int,
*,
delay: float = 1.2,
settle_pause: float = 0.7,
progress: Optional[Callable[[RepostResult], Awaitable[None]]] = None,
should_stop: Optional[Callable[[], bool]] = None,
) -> RepostResult:
"""Copy the posts shift.first_id..shift.last_id of the source into `dst` (a registered channel), in order.
Safe to run again to continue: posts that already have a copy are skipped."""
src = source_of(shift)
idmap = dict(await db.shift_pairs(shift.id, alive_only=False))
links = shift_links(src, dst, idmap)
marks = await db.marks_of(shift.id)
from_db = "fromdb" in marks # the source can't be read: its posts come from My posts alone
restricted = None
if not from_db: # does Telegram restrict the source channel itself (a copyright strike)? Then My posts is used
try:
_top, restricted, _err = await probe_channel(client, peer_of(src))
except Exception: # unknown is treated as "not restricted": marks on single posts and the read-back still guard
log.warning("could not ask Telegram whether the source channel is restricted", exc_info=True)
job = CopyJob(
src_peer=peer_of(src),
dst_peer=peer_of(dst),
opts=RepostOptions(),
links=links,
first_id=shift.first_id,
last_id=shift.last_id,
done=await db.shift_done_ids(shift.id),
idmap=idmap,
record=lambda rows: db.record_shift(shift.id, dst.id, rows, user_id),
saved=saved_lookup(db, src.id), # posts Telegram holds back (a copyright strike) are copied from My posts
use_saved=True,
units=(lambda: saved_units(db, src.id, shift.first_id, shift.last_id)) if from_db else None,
from_db=from_db,
restricted=restricted,
no_media="nomedia" in marks,
stop_if_hidden=0 if "hiddenok" in marks else STOP_IF_HIDDEN, # the owner said to go on although Telegram hides them
)
res = await copy_loop(client, job, delay=delay, progress=progress, should_stop=should_stop, settle_pause=settle_pause)
if not res.stopped and not res.aborted:
res.phase = "finish"
if progress:
await progress(res)
try:
pairs = await db.shift_pairs(shift.id)
res.final = await relink_copies(
client, db, dst, dict(pairs), [n for _, n in pairs], links=links, delay=delay,
settle_pause=settle_pause, should_stop=should_stop,
)
except Exception as e: # the copies are fine; only the links between them were not updated
log.exception("pointing the links of a shift at the copies failed")
res.final_error = type(e).__name__
marks = await db.marks_of(shift.id)
if "profile" in marks and "profile_done" not in marks:
await shift_profile(client, db, shift, dst, user_id, res, progress)
if "relink" in marks and not (res.final is not None and res.final.stopped):
res.phase = "links"
res.cross = CrossResult()
async def watch(_cross) -> None:
if progress:
await progress(res)
if progress:
await progress(res)
try:
await relink_after_shift(
client, db, shift, src, dst, delay=delay, settle_pause=settle_pause, progress=watch,
should_stop=should_stop, result=res.cross,
)
except Exception as e: # the copies are fine; only the links in the other channels were not updated
log.exception("pointing the links of the other channels at the shifted posts failed")
res.cross_error = type(e).__name__
return res
async def shift_profile(client, db, shift, dst, user_id: int, res: RepostResult, progress=None) -> None:
"""-all: the destination takes over the source's name, description and photo (the outcome goes into `res`)."""
res.phase = "profile"
if progress:
await progress(res)
try:
res.profile = await copy_profile(client, source_of(shift), dst)
if res.profile.title: # the destination has a new name: keep the bot's own list right
await db.save_channel(dst.id, dst.access_hash, res.profile.title, dst.username, user_id)
if res.profile.ok:
await db.set_mark(shift.id, "profile_done")
except Exception as e: # the copies are fine; only the channel's name / description / photo were not copied
log.exception("copying the channel profile failed")
res.profile_error = type(e).__name__
async def delete_shift_copies(
client,
db,
dst,
shift,
*,
delay: float = 0.5,
progress: Optional[Callable[[DeleteResult], Awaitable[None]]] = None,
fallback: Optional[Callable[[list], Awaitable[Any]]] = None,
) -> DeleteResult:
"""Undo: remove the copies of this shift from the destination channel (the source is not touched)."""
todo = await db.shift_pending_new(shift.id, 1_000_000)
async def mark(ids: list) -> None:
await db.mark_shift_deleted(shift.id, dst.id, ids)
res = await delete_pending(client, peer_of(dst), todo, mark, delay=delay, progress=progress, fallback=fallback)
res.remaining = (await db.shift_counts(shift.id))["new_left"]
res.blocked = res.remaining > 0 and not res.error
return res
|