github-actions[bot] commited on
Commit
4f159e6
·
1 Parent(s): 109b8ca

Automated sync from GitHub

Browse files
Files changed (3) hide show
  1. app/handlers/replace.py +23 -3
  2. tests/harness.py +7 -3
  3. tests/test_units.py +26 -2
app/handlers/replace.py CHANGED
@@ -13,6 +13,7 @@ from telethon import Button, errors
13
  from ..common import Ctx, UserError, apply_edit, cmd, edit_callback_message, guard, on_cb, purge_pending, require_owner, say, show
14
  from ..linkswap import normalize_username
15
  from ..replace_engine import ApplyResult, ChannelScan, ScanOptions, apply_channel, scan_channel, undo_batch
 
16
  from ..tgutil import classify_media, esc, explain_rpc, get_rights, pack_file_id, ser_entities
17
  from ..channelref import parse_channel_ref
18
 
@@ -98,7 +99,8 @@ def parse_post_link(value: str):
98
  if u.netloc.lower() not in {"t.me", "www.t.me", "telegram.me", "www.telegram.me"}:
99
  return None
100
  if bits and bits[0] == "c" and len(bits) == 3 and bits[1].isdigit() and bits[2].isdigit():
101
- return -1000000000000 - int(bits[1]), int(bits[2])
 
102
  if bits and bits[0] == "s":
103
  bits = bits[1:]
104
  if len(bits) == 2 and bits[1].isdigit() and re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{3,}", bits[0]):
@@ -228,8 +230,26 @@ def register(ctx: Ctx) -> None:
228
  if channel is None:
229
  raise UserError("That post link is not from a registered channel. Check /channels.")
230
  post = await db.find_by_message(channel.id, message_id)
231
- if post is None or post.status != "sent":
232
- raise UserError("That published post is not in My posts.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
233
  caption = reply.raw_text or ""
234
  await apply_edit(
235
  ctx, post, channel, replace_media=True, media_kind=kind, media_file_id=pack_file_id(reply),
 
13
  from ..common import Ctx, UserError, apply_edit, cmd, edit_callback_message, guard, on_cb, purge_pending, require_owner, say, show
14
  from ..linkswap import normalize_username
15
  from ..replace_engine import ApplyResult, ChannelScan, ScanOptions, apply_channel, scan_channel, undo_batch
16
+ from ..sync_engine import SyncOptions, sync_ids
17
  from ..tgutil import classify_media, esc, explain_rpc, get_rights, pack_file_id, ser_entities
18
  from ..channelref import parse_channel_ref
19
 
 
99
  if u.netloc.lower() not in {"t.me", "www.t.me", "telegram.me", "www.telegram.me"}:
100
  return None
101
  if bits and bits[0] == "c" and len(bits) == 3 and bits[1].isdigit() and bits[2].isdigit():
102
+ # Telegram's t.me/c URL uses the same positive channel ID stored by Telethon and in our database.
103
+ return int(bits[1]), int(bits[2])
104
  if bits and bits[0] == "s":
105
  bits = bits[1:]
106
  if len(bits) == 2 and bits[1].isdigit() and re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{3,}", bits[0]):
 
230
  if channel is None:
231
  raise UserError("That post link is not from a registered channel. Check /channels.")
232
  post = await db.find_by_message(channel.id, message_id)
233
+ if post is None:
234
+ report = await sync_ids(
235
+ ctx.client,
236
+ db,
237
+ channel,
238
+ [message_id],
239
+ SyncOptions(history=False, grace=0, delete_services=False, ignore=ctx.sync_ignore),
240
+ adopt_ids={message_id},
241
+ )
242
+ post = await db.find_by_message(channel.id, message_id)
243
+ if post is None:
244
+ if report.error:
245
+ raise UserError(f"I couldn't read that post: {report.error}")
246
+ if report.restricted:
247
+ raise UserError("Telegram is currently holding that post back, so I can't replace its media.")
248
+ if report.unsupported:
249
+ raise UserError("That post can't be saved in My posts, so its media can't be replaced here.")
250
+ raise UserError("That post no longer exists in the channel, or I can't add it to My posts.")
251
+ if post.status != "sent":
252
+ raise UserError("That post is not published yet.")
253
  caption = reply.raw_text or ""
254
  await apply_edit(
255
  ctx, post, channel, replace_media=True, media_kind=kind, media_file_id=pack_file_id(reply),
tests/harness.py CHANGED
@@ -29,7 +29,7 @@ class FakeSent:
29
 
30
 
31
  class FakeEvent:
32
- def __init__(self, uid, out, text="", data=None, message=None):
33
  self.sender_id, self.out = uid, out
34
  self.is_private = True
35
  self.is_channel = False
@@ -40,6 +40,7 @@ class FakeEvent:
40
  raw_text=text, entities=[], media=None, grouped_id=None, forward=None
41
  )
42
  self.pattern_match = None
 
43
  self._is_callback = data is not None
44
  if data is not None:
45
  self.data = data.encode()
@@ -57,6 +58,9 @@ class FakeEvent:
57
  async def get_input_chat(self):
58
  return types.InputPeerUser(self.sender_id, 0)
59
 
 
 
 
60
 
61
  class Outbox:
62
  def __init__(self):
@@ -189,8 +193,8 @@ class App:
189
  await self.db.init()
190
  register_all(self.ctx)
191
 
192
- async def text(self, s, message=None):
193
- ev = FakeEvent(self.uid, self.out, s, message=message)
194
  for builder, cb in self.tg.handlers:
195
  if not isinstance(builder, events.NewMessage):
196
  continue
 
29
 
30
 
31
  class FakeEvent:
32
+ def __init__(self, uid, out, text="", data=None, message=None, reply=None):
33
  self.sender_id, self.out = uid, out
34
  self.is_private = True
35
  self.is_channel = False
 
40
  raw_text=text, entities=[], media=None, grouped_id=None, forward=None
41
  )
42
  self.pattern_match = None
43
+ self._reply = reply
44
  self._is_callback = data is not None
45
  if data is not None:
46
  self.data = data.encode()
 
58
  async def get_input_chat(self):
59
  return types.InputPeerUser(self.sender_id, 0)
60
 
61
+ async def get_reply_message(self):
62
+ return self._reply
63
+
64
 
65
  class Outbox:
66
  def __init__(self):
 
193
  await self.db.init()
194
  register_all(self.ctx)
195
 
196
+ async def text(self, s, message=None, reply=None):
197
+ ev = FakeEvent(self.uid, self.out, s, message=message, reply=reply)
198
  for builder, cb in self.tg.handlers:
199
  if not isinstance(builder, events.NewMessage):
200
  continue
tests/test_units.py CHANGED
@@ -11,7 +11,7 @@ from app.common import UserError
11
  from app.db import normalize_db_url
12
  from app.channelref import parse_channel_ref
13
  from app.handlers.panel import extract_content
14
- from app.handlers.replace import parse_replace_args
15
  from app.linkswap import apply_span_replacements, classify_url, normalize_username, swap_url
16
  from app.suggest import advance_plain, build_suggestion
17
  from app.tgutil import (
@@ -27,7 +27,7 @@ from app.tgutil import (
27
  utf16_len,
28
  )
29
 
30
- from .fakes import cb_btn, make_msg, markup, url_btn
31
  from .harness import App
32
 
33
  NOW = datetime.datetime.now(datetime.timezone.utc)
@@ -166,6 +166,30 @@ def test_replace_args_and_refs():
166
  assert normalize_username("https://t.me/foo_bar/") == "foo_bar" and normalize_username("@x y") is None
167
  assert parse_channel_ref("@animech") == "animech" and parse_channel_ref("https://t.me/c/123456/7") == 123456
168
  assert parse_channel_ref("-1001234567890") == 1234567890 and parse_channel_ref("???") is None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
 
171
  def test_url_helpers():
 
11
  from app.db import normalize_db_url
12
  from app.channelref import parse_channel_ref
13
  from app.handlers.panel import extract_content
14
+ from app.handlers.replace import parse_post_link, parse_replace_args
15
  from app.linkswap import apply_span_replacements, classify_url, normalize_username, swap_url
16
  from app.suggest import advance_plain, build_suggestion
17
  from app.tgutil import (
 
27
  utf16_len,
28
  )
29
 
30
+ from .fakes import FakeChannelClient, cb_btn, make_msg, markup, photo_media, url_btn
31
  from .harness import App
32
 
33
  NOW = datetime.datetime.now(datetime.timezone.utc)
 
166
  assert normalize_username("https://t.me/foo_bar/") == "foo_bar" and normalize_username("@x y") is None
167
  assert parse_channel_ref("@animech") == "animech" and parse_channel_ref("https://t.me/c/123456/7") == 123456
168
  assert parse_channel_ref("-1001234567890") == 1234567890 and parse_channel_ref("???") is None
169
+ assert parse_post_link("https://t.me/c/1723201445/9084") == (1723201445, 9084)
170
+ assert parse_post_link("https://t.me/animech/9084") == ("animech", 9084)
171
+
172
+
173
+ def test_replace_adopts_a_linked_private_post_then_updates_its_media(tmp_path):
174
+ async def run():
175
+ app = App(tmp_path)
176
+ await app.start()
177
+ channel_id, message_id = 1723201445, 9084
178
+ old = make_msg(message_id, "old caption", media=photo_media(10))
179
+ app.tg.add_channel(FakeChannelClient({message_id: old}, cid=channel_id, title="Database Channel"))
180
+ await app.db.save_channel(channel_id, 5, "Database Channel", None, 1)
181
+ replacement = make_msg(1, "new caption", media=photo_media(20))
182
+
183
+ await app.text(f"/replace https://t.me/c/{channel_id}/{message_id}", reply=replacement)
184
+
185
+ saved = await app.db.find_by_message(channel_id, message_id)
186
+ assert saved is not None and saved.source == "adopted"
187
+ assert saved.text == "new caption" and saved.media_file_id == pack_file_id(replacement)
188
+ assert app.tg.edits[-1][1] == message_id
189
+ assert "Replaced the media" in app.out.last_text
190
+ await app.db.close()
191
+
192
+ asyncio.run(run())
193
 
194
 
195
  def test_url_helpers():