fix: a Discord source takes only messages with an image attached, from the posters it names, and knows its channel's name (#4481)
CI and images / lint (push) Successful in 4s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 21s
CI and images / extension-test (push) Successful in 20s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Successful in 2m24s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 7s
CI and images / build-web (push) Successful in 1m42s
CI and images / smoke-web (push) Successful in 59s
CI and images / promote (push) Successful in 2s
CI and images / lint (push) Successful in 4s
CI and images / extension-version (push) Successful in 3s
CI and images / frontend-build (push) Successful in 21s
CI and images / extension-test (push) Successful in 20s
CI and images / backend-lint-and-test (push) Successful in 34s
CI and images / integration (push) Successful in 2m24s
CI and images / sign-extension (push) Successful in 3s
CI and images / build-agent (push) Successful in 7s
CI and images / build-web (push) Successful in 1m42s
CI and images / smoke-web (push) Successful in 59s
CI and images / promote (push) Successful in 2s
- A message with no image or video attachment is chat: extract_media returns nothing for it, so nothing downloads and no post record is written. A message that passes keeps every file, numbered as gallery-dl numbers them. - `discord_authors` in a source's config limits the walk to those posters (id, username or display name); the edit dialog has a field for it and no longer drops config keys it has no field for. - source.display_name (migration 0115), refreshed by every Discord walk, shows on Subscriptions in place of the two-id URL; a Discord post card names its channel from the record's `channel`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -153,9 +153,11 @@ def test_an_embeds_identity_ignores_its_signature():
|
||||
def embed(sig):
|
||||
return {"type": "image", "image": {"proxy_url": f"https://media/p/x.png?ex={sig}"}}
|
||||
|
||||
one = DiscordClient.extract_media(_msg(9, embeds=[embed("a")]))[0].media_id
|
||||
two = DiscordClient.extract_media(_msg(9, embeds=[embed("b")]))[0].media_id
|
||||
assert one == two and len(one) <= 33
|
||||
art = [{"url": "https://cdn/a/1.png"}]
|
||||
one = DiscordClient.extract_media(_msg(9, attachments=art, embeds=[embed("a")]))[1]
|
||||
two = DiscordClient.extract_media(_msg(9, attachments=art, embeds=[embed("b")]))[1]
|
||||
assert one.kind == two.kind == "embed"
|
||||
assert one.media_id == two.media_id and len(one.media_id) <= 33
|
||||
|
||||
|
||||
def test_post_seams():
|
||||
@@ -165,11 +167,52 @@ def test_post_seams():
|
||||
|
||||
|
||||
def test_a_text_only_message_is_not_a_post():
|
||||
"""gallery-dl never made one: chat lines would bury the drops."""
|
||||
"""Chat lines would bury the drops."""
|
||||
assert DiscordClient.post_record_key(_msg(6, content="brb")) is None
|
||||
assert DiscordClient.post_meta(_msg(1))["date"].startswith("2026-09-20")
|
||||
|
||||
|
||||
def test_a_message_without_an_attached_image_is_chat():
|
||||
"""Operator 2026-09-28: only content with an image attached. A lone
|
||||
archive, a link preview or a Tenor GIF takes nothing and records nothing."""
|
||||
chat = [
|
||||
_msg(1, content="the stash", attachments=[
|
||||
{"url": "https://cdn/a/Links_Stash.rar", "content_type": "application/x-rar"},
|
||||
]),
|
||||
_msg(2, content="look", embeds=[
|
||||
{"type": "image", "image": {"proxy_url": "https://media/p/x.png"}},
|
||||
]),
|
||||
_msg(3, content="lol", embeds=[
|
||||
{"type": "gifv", "video": {"proxy_url": "https://media/p/t.mp4"}},
|
||||
]),
|
||||
_msg(4, content="wip", attachments=[{"url": "https://cdn/a/wip.psd"}]),
|
||||
]
|
||||
for m in chat:
|
||||
assert DiscordClient.extract_media(m) == []
|
||||
assert DiscordClient.post_record_key(m) is None
|
||||
|
||||
|
||||
def test_an_attached_image_takes_every_file_numbered_as_gallery_dl_does():
|
||||
"""The gate decides WHETHER a message is taken, never which of its files:
|
||||
the rar beside the image keeps its number, so on-disk names still match."""
|
||||
m = _msg(7, attachments=[
|
||||
{"url": "https://cdn/a/pack.rar"},
|
||||
{"url": "https://cdn/a/noext", "content_type": "image/png"},
|
||||
])
|
||||
items = DiscordClient.extract_media(m)
|
||||
assert [(i.num, i.filename) for i in items] == [(1, "pack"), (2, "noext")]
|
||||
assert DiscordClient.post_record_key(m) == ("message:7", "7")
|
||||
video = _msg(8, attachments=[{"url": "https://cdn/a/clip.MP4?ex=1"}])
|
||||
assert len(DiscordClient.extract_media(video)) == 1
|
||||
|
||||
|
||||
def test_a_forwarded_image_counts_as_attached():
|
||||
m = _msg(9, message_snapshots=[
|
||||
{"message": {"type": 0, "attachments": [{"url": "https://cdn/a/fwd.png"}]}},
|
||||
])
|
||||
assert DiscordClient.post_record_key(m) == ("message:9", "9")
|
||||
|
||||
|
||||
# -- the walk --------------------------------------------------------------------
|
||||
|
||||
def test_a_channel_pages_newest_first_and_skips_system_messages():
|
||||
@@ -206,6 +249,50 @@ def test_messages_carry_server_and_channel_metadata():
|
||||
assert meta["parent"] == "Art"
|
||||
|
||||
|
||||
def _one_channel(messages):
|
||||
return {
|
||||
"/guilds/1": _ok({"id": "1", "name": "Todding's Server"}),
|
||||
"/guilds/1/channels": _ok([
|
||||
{"id": "4", "type": 4, "name": "Todding"},
|
||||
{"id": "2", "type": 0, "name": "banana-land", "parent_id": "4"},
|
||||
]),
|
||||
("/channels/2/messages", None): _ok(messages),
|
||||
("/channels/2/threads/search", 0): _ok({"threads": []}),
|
||||
}
|
||||
|
||||
|
||||
def test_only_from_takes_the_creators_messages_and_no_one_elses():
|
||||
"""Operator 2026-09-28: in the creator's server, another member posting a
|
||||
meme is not the creator's art. Matched by id, username or display name."""
|
||||
creator = {"id": "7", "username": "todding", "global_name": "Todding"}
|
||||
other = {"id": "8", "username": "jakeboii", "global_name": "Jake Boii"}
|
||||
msgs = [_msg(3, author=creator), _msg(2, author=other), _msg(1, author=creator)]
|
||||
url = "https://discord.com/channels/1/2"
|
||||
|
||||
everyone = _client(_one_channel([dict(m) for m in msgs]))
|
||||
assert [mid for mid, _ in _ids(everyone, url)] == ["3", "2", "1"]
|
||||
for who in (["Todding"], ["TODDING "], ["7"]):
|
||||
client = _client(_one_channel([dict(m) for m in msgs]))
|
||||
client.only_from(who)
|
||||
assert [mid for mid, _ in _ids(client, url)] == ["3", "1"]
|
||||
client = _client(_one_channel([dict(m) for m in msgs]))
|
||||
client.only_from([])
|
||||
assert len(_ids(client, url)) == 3
|
||||
|
||||
|
||||
def test_the_source_label_names_the_server_and_channel_the_walk_read():
|
||||
client = _client(_one_channel([]))
|
||||
url = "https://discord.com/channels/1/2"
|
||||
assert client.source_label(url) is None # nothing walked yet
|
||||
list(client.iter_posts(url))
|
||||
assert client.source_label(url) == "Todding's Server · #banana-land"
|
||||
assert client.source_label("https://discord.com/channels/1") == "Todding's Server"
|
||||
client._channels["9"] = {"channel": "wip", "is_thread": True, "parent": "banana-land"}
|
||||
assert client.source_label("https://discord.com/channels/1/9") == (
|
||||
"Todding's Server · #banana-land › wip"
|
||||
)
|
||||
|
||||
|
||||
def test_a_server_walks_text_then_threads_newest_created_first_and_skips_private():
|
||||
routes = {
|
||||
"/guilds/1": _ok({"id": "1", "name": "S"}),
|
||||
|
||||
@@ -283,6 +283,27 @@ async def test_scroll_item_shape_minimal(db):
|
||||
assert "description_full" not in item
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_a_discord_post_names_its_channel(db):
|
||||
"""#4481: the card says which channel a message came from. Read from the
|
||||
record's `channel`; never on another platform, even with the same key."""
|
||||
artist = await _seed_artist(db, "todding-ch")
|
||||
dsrc = await _seed_source(db, artist.id, "discord", "https://discord.com/channels/1/2")
|
||||
psrc = await _seed_source(db, artist.id, "patreon", "https://p/todding-ch")
|
||||
now = datetime.now(UTC)
|
||||
d = await _seed_post(db, dsrc.id, external_id="D1", post_date=now)
|
||||
d.raw_metadata = {"category": "discord", "channel": "banana-land"}
|
||||
blank = await _seed_post(db, dsrc.id, external_id="D2", post_date=now)
|
||||
blank.raw_metadata = {"category": "discord", "channel": " "}
|
||||
p = await _seed_post(db, psrc.id, external_id="P1", post_date=now)
|
||||
p.raw_metadata = {"channel": "not-discord"}
|
||||
await db.commit()
|
||||
|
||||
page = await PostFeedService(db).scroll(cursor=None, limit=10, artist_id=artist.id)
|
||||
by_id = {it["external_post_id"]: it["channel"] for it in page["items"]}
|
||||
assert by_id == {"D1": "banana-land", "D2": None, "P1": None}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_scroll_surfaces_translation_fields(db):
|
||||
# #143: a translated post exposes the translated title/description + source
|
||||
|
||||
Reference in New Issue
Block a user