@@ -175,6 +175,11 @@ def test_gif(self):
175175
176176 def test_sticker (self ):
177177 assert viewer ._media_type_from_path ("foo/Sticker/abc.webp" ) == "sticker"
178+ assert viewer ._media_type_from_path ("foo/stickers/abc.webp" ) == "sticker"
179+ assert (
180+ viewer ._media_type_from_path ("Groups/Chat/2026/STK-20260101-WA0001.webp" ) == "sticker"
181+ )
182+ assert viewer ._media_type_from_path ("Groups/Chat/2026/STK-20260101-WA0001.was" ) == "sticker"
178183
179184 def test_document (self ):
180185 assert viewer ._media_type_from_path ("foo/document.pdf" ) == "document"
@@ -7057,3 +7062,160 @@ def test_frontend_assets_include_msg_edited(self):
70577062 css_content = css_path .read_text (encoding = "utf-8" )
70587063 assert "msg-edited" in js_content
70597064 assert "msg-edited" in css_content
7065+
7066+
7067+ class TestStickerSupport :
7068+ def test_ios_sticker_classification_and_gallery (self , tmp_path ):
7069+ archive_path_db = tmp_path / ".wa_media_archiver.db"
7070+ archive_conn = make_archive_db (archive_path_db )
7071+ archive_conn .execute ("INSERT INTO contacts (number, folder) VALUES ('44123456', 'Alice')" )
7072+ archive_conn .execute (
7073+ "INSERT INTO archive_copies (original_path, archive_path) VALUES ('Message/Media/123/abc.webp', 'Contacts/Alice/2026/abc.webp')"
7074+ )
7075+ archive_conn .commit ()
7076+ archive_conn .close ()
7077+
7078+ wa_conn = sqlite3 .connect (str (tmp_path / "ChatStorage.sqlite" ))
7079+ wa_conn .executescript ("""
7080+ CREATE TABLE ZWACHATSESSION (
7081+ Z_PK INTEGER PRIMARY KEY,
7082+ ZCONTACTJID TEXT,
7083+ ZGROUPINFO INTEGER,
7084+ ZPARTNERNAME TEXT,
7085+ ZHIDDEN INTEGER DEFAULT 0
7086+ );
7087+ CREATE TABLE ZWAMEDIAITEM (
7088+ Z_PK INTEGER PRIMARY KEY,
7089+ ZMEDIALOCALPATH TEXT,
7090+ ZTITLE TEXT,
7091+ ZVCARDNAME TEXT,
7092+ ZVCARDSTRING TEXT
7093+ );
7094+ CREATE TABLE ZWAGROUPMEMBER (
7095+ Z_PK INTEGER PRIMARY KEY,
7096+ ZMEMBERJID TEXT,
7097+ ZCONTACTNAME TEXT
7098+ );
7099+ CREATE TABLE ZWAPROFILEPUSHNAME (
7100+ ZJID TEXT,
7101+ ZPUSHNAME TEXT
7102+ );
7103+ CREATE TABLE ZWAMESSAGE (
7104+ Z_PK INTEGER PRIMARY KEY,
7105+ ZCHATSESSION INTEGER,
7106+ ZISFROMME INTEGER,
7107+ ZMESSAGEDATE REAL,
7108+ ZMESSAGETYPE INTEGER,
7109+ ZTEXT TEXT,
7110+ ZMEDIAITEM INTEGER,
7111+ ZFROMJID TEXT,
7112+ ZSTANZAID TEXT,
7113+ ZPARENTMESSAGE INTEGER,
7114+ ZPUSHNAME TEXT,
7115+ ZGROUPMEMBER INTEGER
7116+ );
7117+ """ )
7118+ wa_conn .execute (
7119+ "INSERT INTO ZWACHATSESSION (Z_PK, ZCONTACTJID, ZGROUPINFO, ZPARTNERNAME) VALUES (1, '44123456@s.whatsapp.net', NULL, 'Alice')"
7120+ )
7121+ wa_conn .execute (
7122+ "INSERT INTO ZWAMEDIAITEM (Z_PK, ZMEDIALOCALPATH, ZTITLE) VALUES (1, 'Media/123/abc.webp', NULL)"
7123+ )
7124+ wa_conn .execute (
7125+ "INSERT INTO ZWAMESSAGE (Z_PK, ZCHATSESSION, ZISFROMME, ZMESSAGEDATE, ZMESSAGETYPE, ZTEXT, ZMEDIAITEM) VALUES (1, 1, 0, 700000000.0, 15, NULL, 1)"
7126+ )
7127+ wa_conn .commit ()
7128+ wa_conn .close ()
7129+
7130+ app = viewer .create_app (tmp_path , rescan = False )
7131+ with app .test_client () as client :
7132+ # 1. /api/messages classifies as sticker
7133+ resp = client .get ("/api/messages?chat_id=1&chat_type=contact" )
7134+ assert resp .status_code == 200
7135+ msgs = resp .get_json ()
7136+ assert len (msgs ) == 1
7137+ assert msgs [0 ]["media_type" ] == "sticker"
7138+ assert msgs [0 ]["archive_path" ] == "Contacts/Alice/2026/abc.webp"
7139+
7140+ # 2. /api/chats classifies last_msg_type as sticker
7141+ chats_resp = client .get ("/api/chats" )
7142+ assert chats_resp .status_code == 200
7143+ chats = chats_resp .get_json ()
7144+ assert len (chats ) == 1
7145+ assert chats [0 ]["last_msg_type" ] == "sticker"
7146+
7147+ # 3. /api/media with types=sticker returns the sticker
7148+ stickers_resp = client .get ("/api/media?chat_id=1&chat_type=contact&types=sticker" )
7149+ assert stickers_resp .status_code == 200
7150+ stickers = stickers_resp .get_json ()
7151+ assert len (stickers ) == 1
7152+ assert stickers [0 ]["media_type" ] == "sticker"
7153+
7154+ # 4. /api/media with types=image does NOT return the sticker
7155+ images_resp = client .get ("/api/media?chat_id=1&chat_type=contact&types=image" )
7156+ assert images_resp .status_code == 200
7157+ images = images_resp .get_json ()
7158+ assert len (images ) == 0
7159+
7160+ # 5. /api/media/count counts it as sticker
7161+ count_resp = client .get ("/api/media/count?chat_id=1&chat_type=contact" )
7162+ assert count_resp .status_code == 200
7163+ counts = count_resp .get_json ()
7164+ assert counts ["by_type" ]["sticker" ]["count" ] == 1
7165+ assert counts ["by_type" ]["sticker" ]["missing" ] == 0
7166+
7167+ def test_android_sticker_classification_and_gallery (self , tmp_path ):
7168+ wa_path = tmp_path / "msgstore.db"
7169+ archive_path_db = tmp_path / ".wa_media_archiver.db"
7170+ wa_conn = make_android_db (wa_path )
7171+ archive_conn = make_archive_db (archive_path_db )
7172+ seed_android_db (wa_conn , archive_conn )
7173+
7174+ wa_conn .execute (
7175+ "INSERT INTO message (_id, chat_row_id, from_me, sender_jid_row_id, timestamp, text_data, message_type) "
7176+ "VALUES (55, 10, 0, 1, 1700000055000, NULL, 20)"
7177+ )
7178+ wa_conn .execute (
7179+ "INSERT INTO message_media (message_row_id, file_path, media_name) "
7180+ "VALUES (55, 'Media/WhatsApp Stickers/STK-2026.webp', 'STK-2026.webp')"
7181+ )
7182+ wa_conn .commit ()
7183+
7184+ archive_conn .execute (
7185+ "INSERT INTO archive_copies (original_path, archive_path) VALUES ('Media/WhatsApp Stickers/STK-2026.webp', 'Contacts/Alice/2026/STK-2026.webp')"
7186+ )
7187+ archive_conn .commit ()
7188+ wa_conn .close ()
7189+ archive_conn .close ()
7190+
7191+ app = viewer .create_app (tmp_path , rescan = False )
7192+ with app .test_client () as client :
7193+ # 1. /api/messages classifies as sticker
7194+ resp = client .get ("/api/messages?chat_id=123456789&chat_type=contact" )
7195+ assert resp .status_code == 200
7196+ msgs = [m for m in resp .get_json () if m ["msg_id" ] == 55 ]
7197+ assert len (msgs ) == 1
7198+ assert msgs [0 ]["media_type" ] == "sticker"
7199+ assert msgs [0 ]["archive_path" ] == "Contacts/Alice/2026/STK-2026.webp"
7200+
7201+ # 2. /api/media with types=sticker returns the sticker
7202+ stickers_resp = client .get (
7203+ "/api/media?chat_id=123456789&chat_type=contact&types=sticker"
7204+ )
7205+ assert stickers_resp .status_code == 200
7206+ stickers = stickers_resp .get_json ()
7207+ assert len (stickers ) == 1
7208+ assert stickers [0 ]["media_type" ] == "sticker"
7209+
7210+ # 3. /api/media with types=image does NOT return the sticker
7211+ images_resp = client .get ("/api/media?chat_id=123456789&chat_type=contact&types=image" )
7212+ assert images_resp .status_code == 200
7213+ images = [img for img in images_resp .get_json () if img ["msg_id" ] == 55 ]
7214+ assert len (images ) == 0
7215+
7216+ # 4. /api/media/count counts it as sticker
7217+ count_resp = client .get ("/api/media/count?chat_id=123456789&chat_type=contact" )
7218+ assert count_resp .status_code == 200
7219+ counts = count_resp .get_json ()
7220+ assert counts ["by_type" ]["sticker" ]["count" ] == 1
7221+ assert counts ["by_type" ]["sticker" ]["missing" ] == 0
0 commit comments