wechat_tools / compressedContentViewer.py
huggingface112's picture
remove wechatlog dependency
4acd90c
import panel as pn
import lz4.block
import xml.sax.saxutils
import html
import io
import xml.dom.minidom
import blackboxprotobuf
# from wechatlog.adaptors.pywxdumpAdapter import decode_bytes_extra
# Initialize Panel with Bootstrap design
pn.extension(design="bootstrap", sizing_mode="stretch_width")
# Social media links and icons
ICON_URLS = {
"brand-github": "https://github.com/yourusername/compressed-content-viewer",
"message-circle": "https://discourse.holoviz.org/",
}
def decode_bytes_extra(data):
"""
Decode BytesExtra data using pywxdump's decode_bytes_extra function.
"""
def get_BytesExtra(BytesExtra):
BytesExtra_message_type = {
"1": {
"type": "message",
"message_typedef": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "int",
"name": ""
}
},
"name": "1"
},
"3": {
"type": "message",
"message_typedef": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "str",
"name": ""
}
},
"name": "3",
"alt_typedefs": {
"1": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {},
"name": ""
}
},
"2": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"13": {
"type": "fixed32",
"name": ""
},
"12": {
"type": "fixed32",
"name": ""
}
},
"name": ""
}
},
"3": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"15": {
"type": "fixed64",
"name": ""
}
},
"name": ""
}
},
"4": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"15": {
"type": "int",
"name": ""
},
"14": {
"type": "fixed32",
"name": ""
}
},
"name": ""
}
},
"5": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"12": {
"type": "fixed32",
"name": ""
},
"7": {
"type": "fixed64",
"name": ""
},
"6": {
"type": "fixed64",
"name": ""
}
},
"name": ""
}
},
"6": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"7": {
"type": "fixed64",
"name": ""
},
"6": {
"type": "fixed32",
"name": ""
}
},
"name": ""
}
},
"7": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"12": {
"type": "fixed64",
"name": ""
}
},
"name": ""
}
},
"8": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"6": {
"type": "fixed64",
"name": ""
},
"12": {
"type": "fixed32",
"name": ""
}
},
"name": ""
}
},
"9": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"15": {
"type": "int",
"name": ""
},
"12": {
"type": "fixed64",
"name": ""
},
"6": {
"type": "int",
"name": ""
}
},
"name": ""
}
},
"10": {
"1": {
"type": "int",
"name": ""
},
"2": {
"type": "message",
"message_typedef": {
"6": {
"type": "fixed32",
"name": ""
},
"12": {
"type": "fixed64",
"name": ""
}
},
"name": ""
}
},
}
}
}
if BytesExtra is None or not isinstance(BytesExtra, bytes):
return None
try:
deserialize_data, message_type = blackboxprotobuf.decode_message(BytesExtra, BytesExtra_message_type)
return deserialize_data
except Exception as e:
return None
return get_BytesExtra(data)
def process_content(data):
"""
Process both CompressContent and BytesExtra content.
First tries to decompress (for CompressContent), if fails treats as BytesExtra.
:param data: bytes data to process
:return: tuple (decoded_string, content_type, error_message)
"""
if data is None or not isinstance(data, bytes):
return "", "unknown", "Invalid input: data must be bytes"
# First try to decompress (assuming it's CompressContent)
try:
dst = lz4.block.decompress(data, uncompressed_size=len(data) << 10)
decoded_string = dst.decode().replace("\x00", "") # Remove any null characters
return decoded_string, "CompressContent", None
except Exception: # If decompression fails, try BytesExtra
try:
# Try to decode as BytesExtra
decoded_dict = decode_bytes_extra(data)
if isinstance(decoded_dict, dict):
# Convert dictionary to a formatted string representation
decoded_string = "{\n"
for key, value in decoded_dict.items():
decoded_string += f" {key}: {value}\n"
decoded_string += "}"
return decoded_string, "BytesExtra", None
else:
return "", "unknown", "BytesExtra decoding did not return a dictionary"
except Exception as decode_error:
return "", "unknown", f"Failed to process content: {str(decode_error)}"
def unescape_xml(text):
# First handle standard XML entities
text = xml.sax.saxutils.unescape(text)
# Then handle HTML entities that might be present
text = html.unescape(text)
return text
def format_xml(xml_string):
try:
# Parse the XML string
dom = xml.dom.minidom.parseString(xml_string)
# Get the pretty-printed XML
return dom.toprettyxml(indent=" ")
except Exception as e:
# If parsing fails, return the original string with the error
return f"XML formatting error: {str(e)}\n\nOriginal XML:\n{xml_string}"
# Create the UI with modern styling
file_input = pn.widgets.FileInput(
accept='.bin',
height=50,
sizing_mode="stretch_width"
)
content_type_indicator = pn.widgets.StaticText(
value="",
align="center"
)
status = pn.widgets.StaticText(
value="##### πŸ“ Upload a .bin file to view its content",
align="center"
)
# Use a TextAreaInput with modern styling
result_area = pn.widgets.TextAreaInput(
value="",
height=400,
sizing_mode="stretch_width",
disabled=True,
stylesheets=['body { font-family: monospace; }']
)
# Styled checkbox
unescape_checkbox = pn.widgets.Checkbox(
name="Unescape XML entities",
value=True,
align="center",
margin=(10, 10)
)
def get_xml_content():
if processed_xml:
return io.StringIO(processed_xml)
return None
# Modern styled download button
download_button = pn.widgets.FileDownload(
callback=get_xml_content,
filename="content.xml",
button_type="primary",
label="πŸ“₯ Download XML",
disabled=True,
align="center",
height=35
)
# Global variable to store the processed XML
processed_xml = None
def process_file(event):
global processed_xml
if event.new is not None:
status.value = "##### βš™οΈ Processing file..."
content_type_indicator.value = ""
try:
# Process the content (either CompressContent or BytesExtra)
content, content_type, error = process_content(event.new)
if error:
status.value = f"##### ❌ {error}"
result_area.value = ""
content_type_indicator.value = ""
download_button.disabled = True
processed_xml = None
return
# Update content type indicator with icon
icon = "πŸ—œοΈ" if content_type == "CompressContent" else "πŸ“„"
content_type_indicator.value = f"##### {icon} Detected type: {content_type}"
if content:
if content_type == "CompressContent":
try:
# Unescape XML entities if checkbox is checked
if unescape_checkbox.value:
content = unescape_xml(content)
# Format XML with proper indentation
pretty_content = format_xml(content)
result_area.value = pretty_content
processed_xml = pretty_content
download_button.filename = f"{file_input.filename.rsplit('.', 1)[0]}_xml.xml"
except Exception as xml_error:
result_area.value = content
processed_xml = content
status.value = f"##### ⚠️ Content extracted, but XML formatting failed: {str(xml_error)}"
else: # BytesExtra
result_area.value = content
processed_xml = content
download_button.filename = f"{file_input.filename.rsplit('.', 1)[0]}_dict.txt"
status.value = "##### βœ… Content processed successfully!"
download_button.disabled = False
else:
status.value = "##### ❌ Processing failed"
result_area.value = ""
download_button.disabled = True
processed_xml = None
except Exception as e:
status.value = f"##### ❌ Error: {str(e)}"
result_area.value = ""
content_type_indicator.value = ""
download_button.disabled = True
processed_xml = None
file_input.param.watch(process_file, 'value')
def toggle_unescape(event):
if file_input.value is not None:
process_file(pn.param.ParamEvent(obj=file_input, name='value', old=None, new=file_input.value))
unescape_checkbox.param.watch(toggle_unescape, 'value')
# Add footer with social links
footer_row = pn.Row(pn.Spacer(), align="center")
for icon, url in ICON_URLS.items():
href_button = pn.widgets.Button(icon=icon, width=35, height=35)
href_button.js_on_click(code=f"window.open('{url}')")
footer_row.append(href_button)
footer_row.append(pn.Spacer())
# Create main content
main = pn.WidgetBox(
pn.Column(
pn.pane.Markdown("##### πŸ“ Drop your .bin file here or click to upload"),
file_input,
content_type_indicator,
pn.Row(
unescape_checkbox,
download_button,
align="center"
),
status,
pn.pane.Markdown("##### πŸ“„ Content Preview"),
result_area,
footer_row,
sizing_mode="stretch_width"
)
)
# Create the template
title = "Content Viewer"
template = pn.template.BootstrapTemplate(
title=title,
main=main,
main_max_width="min(80%, 1000px)",
header_background="#F08080",
)
template.servable(title=title)