1 Star 0 Fork 0

Rison/ExportWeChatMsgForWindows

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
文件
克隆/下载
decode_chat_message.cpp 18.10 KB
一键复制 编辑 原始数据 按行查看 历史
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
#include <QtCore>
#include <QtSql/QSqlDatabase>
#include <QtSql/QSqlQuery>
#include <QtSql/QSqlRecord>
#include "chat_message_define.h"
#include "chat_personal_define.h"
extern std::string xml2json(const char* xml_str);
namespace helper {
int GetDatabaseQueryCount(QSqlQuery& query);
void ReplaceEmojiMsgToImage(QString& source);
QSize GetImageSize(const QString& path);
bool DecodeWechatImage(QString src_path, QString& dst_path);
bool DownloadWechatImage(const QString& src_path, QString& dst_path);
QByteArray ReadDataFromFile(const QString& path);
bool SaveContent2File(const QByteArray& data, const QString& path, bool enable_log);
} // namespace helper
namespace {
QString findExistsPath(QString path) {
if (QFile::exists(path + ".jpg")) {
return path + ".jpg";
}
if (QFile::exists(path + ".gif")) {
return path + ".gif";
}
if (QFile::exists(path + ".png")) {
return path + ".png";
}
return QString();
};
int findSuffixIndex(QByteArray& extra) {
int index = extra.indexOf(".") + 1; // skip .
for (int count = extra.length(); index < count; index++) {
char c = extra.at(index);
if ((c >= '0' && c <= '9') || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) {
continue;
} else {
index += 1;
break;
}
}
return index;
}
QString decodeEmojis(QString msgContent,
QByteArray extra,
const QString& sourcePath,
const QString& outputPath) {
if (!QFile::exists(sourcePath)) {
QDir().mkdir(sourcePath);
}
//解析出图片md5 和 url
QString key;
QString cdnurl;
if (msgContent.contains("<type>8</type>")) {
//<emoticonmd5>0ab3eacba35f5a0d3db4ce048b4360f7</emoticonmd5>
msgContent = msgContent.mid(msgContent.indexOf("<emoticonmd5>"));
msgContent = msgContent.mid(msgContent.indexOf(">") + 1);
key = msgContent.left(msgContent.indexOf("</"));
} else {
std::string jsonNode = xml2json(msgContent.toStdString().c_str());
QJsonParseError parseJsonErr;
QJsonDocument document =
QJsonDocument::fromJson(QString::fromStdString(jsonNode).toUtf8(), &parseJsonErr);
if (!(parseJsonErr.error == QJsonParseError::NoError)) {
qWarning() << "解析json文件错误: " << jsonNode.c_str();
return "";
}
auto jsonMap = document.object().toVariantMap();
auto value = jsonMap["msg"].toMap()["emoji"].toMap();
//解析出持久化在本地的名称
QString md5 = value["@md5"].toString();
QString androidMd5 = value["@androidmd5"].toString();
cdnurl = value["@cdnurl"].toString().trimmed();
key = androidMd5.isEmpty() ? md5 : androidMd5;
}
if (key.isEmpty()) {
qWarning() << "decode emoji fail, no md5 found";
return "";
}
//先尝试从缓存里面解析出
static QMap<QString, QString> cache;
if (cache.contains(key)) {
return cache.value(key);
}
//接着检查输出目录是否已经存在,存在就无须再处理了
QString maybeImage = findExistsPath(outputPath + key);
if (!maybeImage.isEmpty()) {
cache[key] = maybeImage;
return maybeImage;
}
//尝试从extra里面读取
QString orgImg = sourcePath + key;
QString outImg = outputPath + key;
if (!QFile::exists(orgImg) && extra.contains("Attachment")) {
extra = extra.mid(extra.indexOf("Attachment"));
orgImg = extra.left(findSuffixIndex(extra) - 1);
if (orgImg.contains("_")) {
//这个是小图,需要重新获取
extra = extra.mid(orgImg.length());
extra = extra.mid(extra.indexOf("Attachment"));
QString tmp = extra.left(findSuffixIndex(extra) - 1);
if (!tmp.isEmpty()) {
orgImg = tmp;
}
}
if (!orgImg.isEmpty()) {
//多传了一个CustomEmotions
orgImg = sourcePath.left(sourcePath.lastIndexOf("CustomEmotions/")) + orgImg;
}
//有原图
if (orgImg.endsWith(".gif") && QFile::exists(orgImg)) {
outImg += ".gif";
QFile::copy(orgImg, outImg);
cache[key] = outImg;
return outImg;
}
}
//开始解密
bool ret = false;
if (QFile::exists(orgImg)) {
ret = helper::DecodeWechatImage(orgImg, outImg);
if (ret) {
cache[key] = outImg;
return outImg;
}
}
//已经证实md5是内容的md5
if (!cdnurl.isEmpty()) {
//准备通过连接下载图片
if (helper::DownloadWechatImage(cdnurl, outImg)) {
if (!key.isEmpty()) {
cache[key] = outImg;
}
return outImg;
}
//下载失败的处理
qWarning() << "decode emoji fail, we has url: " << cdnurl << ", md5: " << key;
if (!key.isEmpty()) {
cache[key] = cdnurl;
}
return cdnurl;
} else {
//去重,找出不存在的md5,手动获取表情资源替换掉
// qWarning() << "decode emoji fail, nothing can do, " << msgContent;
static QSet<QString> lostEmoji;
if (!lostEmoji.contains(key)) {
lostEmoji.insert(key);
qWarning() << "decode emoji fail, nothing can do, md5: " << key;
}
}
return QString();
}
QString decodeImages(const QString& svrId,
QByteArray extra,
const QString& sourcePath,
const QString& outputPath,
QString& thumbPath) {
if (!QFile::exists(outputPath)) {
QDir().mkdir(outputPath);
}
//尝试从数据库信息里面解析2个路径
QString orgBigImg, orgSmallImg;
if (extra.count(".dat") >= 2) {
//大图
int startIndex = extra.indexOf("Data") + 5; // 4 is length of Data + /
int endIndex = extra.indexOf(".dat") + 4; // 4 is length of .dat
orgBigImg = extra.mid(startIndex, endIndex - startIndex);
//缩略图
extra = extra.mid(endIndex);
startIndex = extra.indexOf("Data") + 5;
endIndex = extra.indexOf(".dat") + 4;
orgSmallImg = extra.mid(startIndex, endIndex - startIndex);
if (!orgBigImg.isEmpty()) {
orgBigImg = sourcePath + orgBigImg;
}
if (!orgSmallImg.isEmpty()) {
orgSmallImg = sourcePath + orgSmallImg;
}
//有可能顺序会相反,交换一下
if (orgBigImg.contains("Tiny") && !orgSmallImg.isEmpty()) {
orgBigImg.swap(orgSmallImg);
}
}
//没有就尝试挣扎一下,尝试检查几个目录里面是否存在
// if (!QFile::exists(orgBigImg)) {
// QString bigSourceImage = sourcePath + svrId + ".dat";
//}
if (!QFile::exists(orgSmallImg)) {
// 缩略图在 Data/Tiny,结尾可能是_sender 或者_t
static auto findThumbnail = [](QString path) {
if (QFile::exists(path + "_sender.dat")) {
return path + "_sender.dat";
}
if (QFile::exists(path + "_t.dat")) {
return path + "_t.dat";
}
return QString();
};
orgSmallImg = findThumbnail(sourcePath + "/Tiny/" + svrId);
}
//解密大图。如果已经解密过的话可以跳过
bool ret = false;
QString outImg = outputPath + svrId;
QString cacheBigImg = findExistsPath(outImg);
if (!cacheBigImg.isEmpty()) {
// good
} else if (QFile::exists(orgBigImg)) {
ret = helper::DecodeWechatImage(orgBigImg, outImg);
//成功的话,缩略图默认也用这个,因为可能存在缩略图没有的情况
if (ret) {
thumbPath = outImg;
cacheBigImg = outImg;
} else {
qWarning() << "decode image fail, svrId " << svrId;
}
} else if (orgSmallImg.isEmpty()) {
//啥都没有,需要警告一下
qWarning() << "big image not exist, " << orgBigImg;
}
//解密小图
outImg = outputPath + svrId + "_thumb";
QString cacheSmallImg = findExistsPath(outImg);
if (!cacheSmallImg.isEmpty()) {
// good
} else if (QFile::exists(orgSmallImg)) {
ret = helper::DecodeWechatImage(orgSmallImg, outImg);
//成功的话,缩略图默认就用这个
if (ret) {
cacheSmallImg = outImg;
} else {
qWarning() << "decode thumb image fail, svrId " << svrId;
}
} else /*if (orgSmallImg.isEmpty())*/ {
//没有大图也没有小图,需要警告一下
qWarning() << "small image not exist, " << orgSmallImg;
}
//处理一些特殊情况,比如有缩略图但是没大图
if (!cacheSmallImg.isEmpty()) {
thumbPath = cacheSmallImg;
}
if (cacheBigImg.isEmpty()) {
cacheBigImg = cacheSmallImg;
}
return cacheBigImg;
}
int parseVoiceTime(QString& msgContent) {
int voiceTime = 1;
QString prefix = "voicelength=\"";
int index = msgContent.indexOf(prefix);
if (index > -1) {
auto rightStr = msgContent.mid(index + prefix.length());
int length = rightStr.indexOf("\"");
if (length > -1) {
int millSeconds = rightStr.leftRef(length).toInt();
if (millSeconds > 0) {
voiceTime = (millSeconds + 999) / 1000;
}
}
// qWarning() << "从xml解析出语音消息长度为 " << voiceTime << "s";
}
return voiceTime;
}
QString decodeVideos(const QString& svrId,
QByteArray extra,
const QString& sourcePath,
const QString& outputPath,
QString& thumbPath) {
if (!QFile::exists(outputPath)) {
QDir().mkdir(outputPath);
}
QString orgThumb, orgVideo;
if (extra.count("Video") >= 2) {
//缩略图
int startIndex = extra.indexOf("Video") + 6; // 6 is length of Data + /
extra = extra.mid(startIndex);
orgThumb = extra.left(extra.indexOf(".") + 4); // 4 is .mp4 or .jpg
//视频
extra = extra.mid(orgThumb.length());
startIndex = extra.indexOf("Video") + 6;
extra = extra.mid(startIndex);
orgVideo = extra.left(extra.indexOf(".") + 4);
if (!orgThumb.isEmpty()) {
orgThumb = sourcePath + orgThumb;
}
if (!orgVideo.isEmpty()) {
orgVideo = sourcePath + orgVideo;
}
//有可能顺序会相反,交换一下
if (orgThumb.contains(".mp4") && !orgVideo.isEmpty()) {
orgThumb.swap(orgVideo);
}
}
QString md5 = QCryptographicHash::hash(svrId.toUtf8(), QCryptographicHash::Md5).toHex();
QString outputPrefix = outputPath + md5;
//挣扎一下
if (orgThumb.isEmpty()) {
orgThumb = sourcePath + md5 + ".jpg";
}
if (orgVideo.isEmpty()) {
orgVideo = sourcePath + md5 + ".mp4";
}
//拷贝缩略图
QString cachedThumb = outputPrefix + ".jpg";
if (QFile::exists(cachedThumb)) {
thumbPath = cachedThumb;
} else if (QFile::exists(orgThumb)) {
QFile::copy(orgThumb, cachedThumb);
thumbPath = cachedThumb;
} else {
qWarning() << "video thumbnail not exist, svrId " << svrId;
}
//拷贝视频
QString cachedVideo = outputPrefix + ".mp4";
if (QFile::exists(cachedVideo)) {
// thumbPath = cachedThumb;
} else if (QFile::exists(orgVideo)) {
QFile::copy(orgVideo, cachedVideo);
} else {
cachedVideo = "";
qWarning() << "video mp4 not exist, svrId " << svrId;
}
return cachedVideo;
}
QString decodeFiles(QByteArray extra, const QString& sourcePath, const QString& outputPath) {
if (!QFile::exists(outputPath)) {
QDir().mkdir(outputPath);
}
QString orgFile;
if (extra.contains(":")) {
//例如存在下载路径,旧版
extra = extra.mid(extra.indexOf(":") - 1);
orgFile = extra.left(findSuffixIndex(extra) - 1);
} else if (extra.contains("Attachment")) {
//例如只有相对路径的情况,新版
extra = extra.mid(extra.indexOf("Attachment"));
orgFile = extra.left(findSuffixIndex(extra) - 1);
orgFile = sourcePath + orgFile;
}
if (orgFile.isEmpty()) {
return "";
}
orgFile.replace("\\", "/");
QString outputFile = outputPath;
outputFile += orgFile.midRef(orgFile.lastIndexOf("/") + 1);
if (QFile::exists(outputFile)) {
return outputFile;
} else if (QFile::exists(orgFile)) {
QFile::copy(orgFile, outputFile);
// qWarning() << "copy file: " << orgFile << " to " << outputFile;
return outputFile;
} else {
qWarning() << "parse file path: " << orgFile << ", but not exists";
}
return QString();
}
} // namespace
namespace wechat {
//支持的是2016年前的微信聊天数据库
void DecodeChatMessage() {
QString sourcePath = TARGET_SOURCE_PATH;
QString outputPath = TARGET_OUTPUT_PATH;
// https://blog.csdn.net/lms1008611/article/details/81271712
QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE");
db.setDatabaseName(sourcePath + R"(Msg\dec_ChatMsg.db)");
if (!db.open()) {
qWarning() << "open database failed";
return;
}
//查询指定用户的聊天数据,并按照本地数据库id从小到大排序
QSqlQuery query(db);
query.prepare("SELECT * FROM ChatMsg WHERE strTalker='" TARGET_USER "' ORDER BY localId");
query.exec();
//统计数目
int result_count = helper::GetDatabaseQueryCount(query);
qDebug() << "find " << result_count << " messages";
//获得结果
QJsonArray jsonArray;
while (query.next()) {
auto localId = query.value("localId").toInt();
auto type = query.value("type").toInt();
auto isSender = query.value("IsSender").toInt() > 0;
auto msgSvrID = query.value("MsgSvrID").toString();
auto createTime = query.value("CreateTime").toLongLong();
auto msgContent = query.value("strContent").toString();
auto bytesExtra = query.value("bytesExtra").toByteArray();
// qDebug() << "type:"<< type << "str: " << msgContent;
QJsonObject item;
item["m_uiMesLocalID"] = localId;
item["m_uiMesSvrID"] = msgSvrID;
item["m_uiCreateTime"] = createTime;
item["m_uiMessageType"] = type;
item["m_nsFromUsr"] = isSender ? "" : TARGET_USER;
item["m_nsToUsr"] = isSender ? TARGET_USER : "";
item["m_nsContent"] = msgContent;
if (type == ChatType_Text) {
//普通消息
helper::ReplaceEmojiMsgToImage(msgContent);
item["m_nsContent"] = msgContent;
} else if (type == ChatType_Image) {
// 图片消息
QString thumbPath;
QString imagePath = decodeImages(
msgSvrID, bytesExtra, sourcePath + "Data/", outputPath + "images/", thumbPath);
if (imagePath.isEmpty()) {
item["m_nsContent"] = "";
item["m_nsThumbnail"] = "";
item["m_nsOriginal"] = msgContent;
// 占位的大小
item["m_nsWidth"] = 180;
item["m_nsHeight"] = 180;
} else {
item["m_nsContent"] = imagePath.mid(outputPath.length());
item["m_nsThumbnail"] = thumbPath.mid(outputPath.length());
item["m_nsOriginal"] = "";
auto&& size = helper::GetImageSize(imagePath);
item["m_nsWidth"] = size.width();
item["m_nsHeight"] = size.height();
}
} else if (type == ChatType_Emotion) {
// emoji表情
QString emojiPath = decodeEmojis(
msgContent, bytesExtra, sourcePath + "CustomEmotions/", outputPath + "emotions/");
if (emojiPath.isEmpty()) {
item["m_nsContent"] = "";
item["m_nsOriginal"] = msgContent;
} else {
item["m_nsContent"] = emojiPath.mid(outputPath.length());
item["m_nsOriginal"] = ""; //已经解析出来就可以丢弃了
}
} else if (type == ChatType_Voice) {
//语音消息
//暂时不清楚旧版语音消息的格式如何,因为以前的PC版没有存放
item["m_uiVoiceTime"] = parseVoiceTime(msgContent);
item["m_nsContent"] = "";
item["m_nsOriginal"] = msgContent;
} else if (type == ChatType_Video || type == ChatType_ShortVideo) {
//视频,key为msgSvrID的md5
//短视频,可以从extra里面解密到信息
QString thumbPath;
QString videoPath = decodeVideos(
msgSvrID, bytesExtra, sourcePath + "Video/", outputPath + "Videos/", thumbPath);
if (videoPath.isEmpty()) {
item["m_nsContent"] = "";
item["m_nsThumbnail"] = "";
item["m_nsOriginal"] = msgContent;
} else {
item["m_nsContent"] = videoPath.mid(outputPath.length());
item["m_nsThumbnail"] = thumbPath.mid(outputPath.length());
item["m_nsOriginal"] = "";
}
} else if (type == ChatType_Extend) {
//扩展消息
msgContent.replace("&#x20;", " ");
msgContent.replace("&#x0A;", "\n");
msgContent.replace("&#x0D;", "\r");
//去掉xml之间的特殊换行符
if (msgContent.startsWith("<msg>")) {
msgContent.replace(QRegExp(">\\s+<"), "><");
}
if (msgContent.endsWith("</msg>\n")) {
msgContent.chop(1);
}
item["m_nsContent"] = msgContent;
if (msgContent.contains("<type>6</type>")) {
auto filePath = decodeFiles(bytesExtra, sourcePath, outputPath + "attachment/");
item["m_nsFilePath"] = filePath.mid(outputPath.length());
item["m_nsFileExist"] = !filePath.isEmpty();
} else if (msgContent.contains("<type>8</type>")) {
//表情消息,尝试解析
QString emojiPath = decodeEmojis(
msgContent, bytesExtra, sourcePath + "CustomEmotions/", outputPath + "emotions/");
if (!emojiPath.isEmpty()) {
item["m_uiMessageType"] = 47; //重新转换为表情类型
item["m_nsContent"] = emojiPath.mid(outputPath.length());
item["m_nsOriginal"] = "";
}
}
// else if (!msgContent.contains("<type>5</type>") && !msgContent.contains("<type>3</type>"))
// {
// qWarning() << msgContent;
//}
}
jsonArray.push_back(item);
}
//格式化json
QString content = QJsonDocument(jsonArray).toJson(QJsonDocument::Indented);
//读取模板信息并写入到新的文件
QString bytes = helper::ReadDataFromFile(outputPath + "js/message_tmp.js");
helper::SaveContent2File(bytes.arg(content).toUtf8(), outputPath + "js/message.js", true);
}
} // namespace wechat
Loading...
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
C++
1
https://gitee.com/rison13/ExportWeChatMsgForWindows.git
git@gitee.com:rison13/ExportWeChatMsgForWindows.git
rison13
ExportWeChatMsgForWindows
ExportWeChatMsgForWindows
master

搜索帮助