Skip to content

Commit

Permalink
Support 1. auto-combine slwotable, 2. auto-use friend name
Browse files Browse the repository at this point in the history
Signed-off-by: Yiyiyimu <[email protected]>
  • Loading branch information
Yiyiyimu committed Jan 2, 2021
1 parent fd9880a commit 3d16b58
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 85 deletions.
19 changes: 9 additions & 10 deletions GUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,17 @@


def Enter():
dir, qq_self, qq_oppo = e1.get(), e2.get(), e3.get()
dir, qq_self, qq = e1.get(), e2.get(), e3.get()
group = 1 if e4.get() == '私聊' else 2
if (dir == "" or qq_self == "" or qq_oppo == ""):
if (dir == "" or qq_self == "" or qq == ""):
info.set("信息不完整!")
return ()
info.set("开始导出")
try:
QQ_History.main(dir, qq_self, qq_oppo, group)
QQ_History.main(dir, qq_self, qq, group)
except Exception as e:
info.set(repr(e))
return ()
info.set("完成")


def SelectPath():
Expand Down Expand Up @@ -52,21 +51,21 @@ def url():
e2 = ttk.Entry(root)
e2.grid(row=1, column=1, columnspan=3, sticky="ew", pady=3)

ttk.Label(root, text="*QQ号/群号:").grid(row=1, column=0, sticky="e")
ttk.Label(root, text="*QQ号/群号:").grid(row=2, column=0, sticky="e")
e3 = ttk.Entry(root)
e3.grid(row=1, column=1, columnspan=3, sticky="ew", pady=3)
e3.grid(row=2, column=1, columnspan=3, sticky="ew", pady=3)

ttk.Label(root, text="私聊/群聊:").grid(row=6, column=0, sticky="e")
ttk.Label(root, text="私聊/群聊:").grid(row=3, column=0, sticky="e")
e4 = ttk.Combobox(root)
e4['values'] = ('私聊', '群聊')
e4.current(0)
e4.grid(row=6, column=1, columnspan=3, sticky="ew", pady=3)
e4.grid(row=3, column=1, columnspan=3, sticky="ew", pady=3)

root.grid_columnconfigure(2, weight=1)

ttk.Button(root, text="确认", command=Enter).grid(row=7, column=1)
ttk.Button(root, text="确认", command=Enter).grid(row=4, column=1)
l1 = ttk.Label(root, textvariable=info)
l1.grid(row=7, column=1)
l1.grid(row=4, column=1)

tmp = open("tmp.png", "wb+")
tmp.write(base64.b64decode(github_mark))
Expand Down
135 changes: 84 additions & 51 deletions QQ_History.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,15 @@


class QQoutput():
def __init__(self, dir, qq_self, qq_oppo, mode):
self.key = get_key(dir) # 解密用的密钥
db = os.path.join(dir, "db", qq_self + ".db")
self.c = sqlite3.connect(db).cursor()
def __init__(self, dir, qq_self, qq, mode):
self.dir = dir
self.key = self.get_key() # 解密用的密钥
db = os.path.join(dir, "databases", qq_self + ".db")
self.c1 = sqlite3.connect(db).cursor()
db = os.path.join(dir, "databases", "slowtable_" + qq_self + ".db")
self.c2 = sqlite3.connect(db).cursor()
self.qq_self = qq_self
self.qq = qq_oppo
self.qq = qq
self.mode = mode
self.num_to_name = {}

Expand All @@ -22,9 +25,9 @@ def decrypt(self, data):
try:
for i in range(0, len(data)):
msg += bytes([data[i] ^ ord(self.key[i % len(self.key)])])
return msg.decode(encoding="utf-8")
except:
msg = NULL
return msg.decode(encoding="utf-8")
return NULL
elif type(data) == str:
msg = ""
try:
Expand Down Expand Up @@ -54,49 +57,76 @@ def message(self):
num = self.qq.encode("utf-8")
md5num = hashlib.md5(num).hexdigest().upper()
if (self.mode == 1):
execute = "select msgData,senderuin,time from mr_friend_{md5num}_New".format(
cmd = "select msgData,senderuin,time from mr_friend_{md5num}_New".format(
md5num=md5num)
self.get_friends()
else:
execute = "select msgData,senderuin,time from mr_troop_{md5num}_New".format(
cmd = "select msgData,senderuin,time from mr_troop_{md5num}_New".format(
md5num=md5num)
self.get_troop_members()

cursor = self.c.execute(execute)
cursors = self.fill_cursors(cmd)
allmsg = []
for row in cursor:
msgdata = row[0]
if (not msgdata):
continue
uin = row[1]
ltime = time.localtime(row[2])

sendtime = time.strftime("%Y-%m-%d %H:%M:%S", ltime)

amsg = []
amsg.append(sendtime)
amsg.append(self.decrypt(uin))
amsg.append(self.decrypt(msgdata))
allmsg.append(amsg)
for cs in cursors:
for row in cs:
msgdata = row[0]
if (not msgdata):
continue
uin = row[1]
ltime = time.localtime(row[2])
sendtime = time.strftime("%Y-%m-%d %H:%M:%S", ltime)

amsg = []
amsg.append(sendtime)
amsg.append(self.decrypt(uin))
amsg.append(self.decrypt(msgdata))
allmsg.append(amsg)
return allmsg

def get_name(self):
exe = "select uin,remark from Friends"
cursor = self.c.execute(exe)
for row in cursor:
num = self.decrypt(row[0])
name = self.decrypt(row[1])
self.num_to_name[num] = name
def get_friends(self):
cmd = "SELECT uin, remark FROM Friends"
cursors = self.fill_cursors(cmd)
for cs in cursors:
for row in cs:
num = self.decrypt(row[0])
name = self.decrypt(row[1])
self.num_to_name[num] = name

def get_troop_members(self):
cmd = "SELECT troopuin, memberuin, friendnick, troopnick FROM TroopMemberInfo"
cursors = self.fill_cursors(cmd)
for cs in cursors:
for row in cs:
if(self.decrypt(row[0]) != self.qq):
continue
num = self.decrypt(row[1])
name = self.decrypt(row[3]) or self.decrypt(row[2])
self.num_to_name[num] = name

def fill_cursors(self, cmd):
cursors = []
try:
cursors.append(self.c2.execute(cmd))
except:
pass
try:
cursors.append(self.c1.execute(cmd))
except:
pass
return cursors

def output(self):
name1 = "我"
file = str(self.qq) + ".html"
self.get_name()
f2 = open(file, "w", encoding="utf-8")
f2.write(
"<head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" /></head>"
)
allmsg = self.message()
f2.write("<div style='white-space: pre-line'>")
for msg in allmsg:
if not msg[2]:
continue
try:
if (msg[1] == str(self.qq_self)):
f2.write("<p align='right'>")
Expand All @@ -119,16 +149,27 @@ def output(self):
pass
f2.write("</div>")


def get_key(dir):
kc_path = os.path.join(dir, "f", "kc")
kc_file = open(kc_path, "r")
return kc_file.read()


def main(dir, qq_self, qq_oppo, mode):
def get_key(self):
self.unify_path()
kc_path = os.path.join(self.dir, "files", "kc")
kc_file = open(kc_path, "r")
return kc_file.read()

def unify_path(self):
if os.path.isdir(os.path.join(self.dir, "f")):
os.rename(os.path.join(self.dir, "f"),
os.path.join(self.dir, "files"))
if os.path.isdir(os.path.join(self.dir, "db")):
os.rename(os.path.join(self.dir, "db"),
os.path.join(self.dir, "databases"))
if os.path.isfile(os.path.join(self.dir, "files", "kc")) == False:
raise OSError(
"File not found. Please report your directory layout.")


def main(dir, qq_self, qq, mode):
try:
q = QQoutput(dir, qq_self, qq_oppo, mode)
q = QQoutput(dir, qq_self, qq, mode)
q.output()
except Exception as e:
with open('log.txt', 'w') as f:
Expand All @@ -139,13 +180,5 @@ def main(dir, qq_self, qq_oppo, mode):
print(traceback.format_exc())
if (err_info):
raise ValueError("QQ号/私聊群聊选择/db地址/错误")


dir = "C:/Users/30857/Desktop/QQ_History/qq/apps/com.tencent.mobileqq"
qq_self = "308571034"
qq_oppo = "584740257"
#qq = "939840382"
key = "361910168"
#msg = "还是在试表情"
msg = ""
main(dir, qq_self, qq_oppo, 1)
else:
raise BaseException("Error! See log.txt")
39 changes: 15 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,62 +1,53 @@
# QQ聊天记录导出

可执行文件[Github下载链接](https://github.com/Yiyiyimu/QQ_History_Backup/releases/download/v1.41/QQ_History_Backup-v1.41.zip)[百度网盘下载链接](https://pan.baidu.com/s/1FRcqKiYho-DoDU-RC_uRkw)(sqhc) ,可直接运行。
可执行文件[Github下载链接](https://github.com/Yiyiyimu/QQ_History_Backup/releases/download/v2.0/QQ_History_Backup-v2.0.zip)[百度网盘下载链接](https://pan.baidu.com/s/1nbJcP5RVc1ID1IFGsN1-yQw)(i4cv) ,可直接运行。

## 简介

本项目 fork 自大佬的项目[roadwide/qqmessageoutput](https://github.com/roadwide/qqmessageoutput) 在此非常感谢。因为改动较多,不再作为 fork 分支开发

在之前版本的基础上完成了原作者提到的无需密钥解密的方法,添加了QQ表情的一并导出,并制作了GUI方便使用
在之前版本的基础上完成了自动提取密钥解密的方法,自动填入备注/昵称,添加了QQ表情的一并导出,并制作了GUI方便使用

## 获取db文件方法
## 获取聊天记录文件夹方法

如果root了,直接在以下地址就可以找到

```
data\data\com.tencent.mobileqq\databases\你的QQ.db 和 slowtable_你的QQ.db
data\data\com.tencent.mobileqq
```


如果没有root,可以通过手机自带的备份工具备份整个QQ软件,具体方法可以参见

> 怎样导出手机中的QQ聊天记录? - 益新软件的回答 - 知乎
> https://www.zhihu.com/question/28574047/answer/964813560
导出之前建议发给对方一句话(至少六个汉字),后面解密用

## GUI使用方法

![GUI_image](./img/GUI.png)

db文件地址(必填):选择对应的 qq号.db ,如果不全再选择slowtable_qq号.db

对方QQ号(必填)

手机识别码(待自动填入,供slowtable使用):
Android Q及以上(19年以后的系统)限制了id获取权限,无法使用手机识别码(IMEI/MEID)作为聊天记录数据库的密钥,只能通过最后一次聊天记录计算key。在导出slowtable里的内容时默认使用前一步输出的手机识别码作为密钥。

最后一次聊天记录(非slowtable**必填**):
因为测试所用两部手机密钥分别为9位和14位,一个汉字对应三个utf-8码,为了避免更长的密钥推荐使用至少六个汉字符号。可以在导出之前给对方发一句话过去。

我的名字(选填):默认为“我”,填入进行替换

对方名字(选填):默认为对方QQ号,填入进行替换

私聊/群聊
com.tencent.mobileqq:选择备份后的相应文件夹,一般为`apps/com.tencent.mobileqq`

## 输出截图

为了方便离线查看,qq表情gif选择保存在本地,注意移动聊天记录的时候需要同时移动gif文件

![screenshot](./img/screenshot.png)

有bug的话记得附上log.txt里的内容
有bug的话提issue,记得附上log.txt里的内容

## v2.0 更新
- 直接从 `files/kc` 提取明文的密钥,不用再手动输入或解密
- 支持群聊记录导出
- 支持 私聊/群聊 的 备注/昵称 自动填入
- 支持 slowtable 的直接整合

## TODO
- [x] support troop message output
- [x] use com.tencent.mobileqq/f/kc as key
- [ ] decode friend/troop name, to use in result
- [ ] auto-combine db and slow-table
- [x] decode friend/troop name, to use in result
- [x] auto-combine db and slow-table
- [ ] update to new qq emoji
- [ ] add desensitization data to create e2e test
- [ ] add Makefile, to run build/test
- [ ] use pic in mobile folder, to better present result
Binary file modified img/GUI.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 3d16b58

Please sign in to comment.