├── db ├── __init__.py ├── man.py └── models.py ├── exporter ├── html │ ├── __init__.py │ ├── elements.py │ └── expoter.py ├── txt │ ├── __init__.py │ ├── elements.py │ └── exporter.py ├── json │ ├── __init__.py │ └── exporter.py ├── __init__.py └── base_elements.py ├── requirements.txt ├── example.toml ├── template └── template.html ├── README.md ├── emojis.py ├── main.py ├── element.proto ├── element_pb2.py └── LICENSE /db/__init__.py: -------------------------------------------------------------------------------- 1 | from .man import DatabaseManager 2 | -------------------------------------------------------------------------------- /exporter/html/__init__.py: -------------------------------------------------------------------------------- 1 | from .expoter import HtmlExportManager -------------------------------------------------------------------------------- /exporter/txt/__init__.py: -------------------------------------------------------------------------------- 1 | from .exporter import TxtExportManager 2 | -------------------------------------------------------------------------------- /exporter/json/__init__.py: -------------------------------------------------------------------------------- 1 | from .exporter import JsonExportManager 2 | 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Tealina28/QQNT_Export/HEAD/requirements.txt -------------------------------------------------------------------------------- /exporter/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def sanitize_filename(filename): 4 | illegal_chars = r'[<>:"/\\|?*]' 5 | if isinstance(filename,int): 6 | return filename 7 | return re.sub(illegal_chars, '_', filename) -------------------------------------------------------------------------------- /example.toml: -------------------------------------------------------------------------------- 1 | db_path = ".\\databases\\" # 解密后的数据库目录路径 2 | pic_path = ".\\chatpic\\" # chatpic目录路径,留空不导出完整路径 3 | 4 | output_path = "" # 导出的路径,留空默认为数据库上级目录 5 | 6 | c2c_filters = [123456,654321] # 需要输出的私聊消息的QQ号列表,留空导出全部 7 | group_filters = [654321,123456] # 需要输出的群聊消息的群号列表,留空导出全部 8 | 9 | output_format = ["txt", "json"] # 需要导出的文件格式 -------------------------------------------------------------------------------- /template/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ title }} 6 | 7 | 8 | 9 | 10 |

{{ title }}

11 | 12 | {% for message in messages %} 13 |

14 | {{ message.readable_time }} 15 | {{ message.display_identity }}
16 | {{ message.content|safe}}
17 |

18 | {% endfor %} 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # QQNT_Export 2 | 3 | ## 讨论 4 | 5 | - **答疑**:可能会在 [Discussions](https://github.com/Tealina28/QQNT_Export/discussions) 提出开发中的问题,欢迎协助解答。 6 | - **讨论**:技术方案等各类讨论欢迎在 [Discussions](https://github.com/Tealina28/QQNT_Export/discussions) 中发起。 7 | - **协作开发**:如果您有 SQL/Protobuf 相关经验,特别欢迎参与项目改进。 8 | 9 | ## 介绍 10 | 11 | 本项目用于读取并导出**解密后的**QQNT数据库中的聊天记录。 12 | 13 | 解密数据库请使用[qqnt_backup](https://github.com/xCipHanD/qqnt_backup)(Android)或参照[qq-win-db-key](https://github.com/QQBackup/qq-win-db-key)。 14 | 15 | 16 | ## 使用流程 17 | 18 | 有两种使用方式 19 | 20 | 1. 使用二进制文件(Windows)。 21 | 22 | 2. 使用源代码。 23 | 24 | ### 获取二进制文件 25 | 26 | Windows用户可到[Releases](https://github.com/Tealina28/QQNT_Export/releases)中下载二进制文件。 27 | 28 | ### 获取源代码 29 | 30 | 1. 克隆或下载本仓库。 31 | 32 | 2. 确保你拥有[Python 3](https://www.python.org/downloads/)环境,建议使用较新的版本。 33 | 34 | 3. 使用`pip install -r requirements.txt`安装项目依赖。 35 | 36 | ### 使用 37 | 38 | 创建`.toml`文件,并按照仓库中`example.toml`的格式修改配置。使用时传入该`.toml`文件的路径作为唯一参数即可。 39 | 40 | 示例:`python main.py .\example.toml` 41 | 42 | > 对于之前版本,仍可使用`python main.py --help`查看帮助信息。 43 | 44 | 若一切正常,你应该看到在生成了`output`目录,目录中对于每个私聊对象和群聊生成了一个`.txt`或`.json`文件。 45 | 46 | ## 关于 47 | 48 | 本项目基于[GPLv3](https://www.gnu.org/licenses/gpl-3.0.zh-cn.html)开源。 49 | 50 | ## 鸣谢 51 | 52 | 53 | | 对象 | 内容 | 54 | |-------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------| 55 | | [@yllhwa](https://github.com/yllhwa) | 初始代码和Protobuf定义 | 56 | | [QQDecrypt](https://docs.aaqwq.top/) | 数据表部分列含义,Protobuf的消息段部分字段含义。
该网站的设立者[@shenapex](https://github.com/shenapex)为解读数据库和导出聊天记录做了大量的研究工作,向他致敬🫡。 | 57 | | [nt_msg.py](https://github.com/BrokenC1oud/nt_msg.py) | SQLAlchemy模型, DatabaseManager(抄了好多,大佬好强) | 58 | 59 | ## 免责声明 60 | 61 | 本项目仅供学习交流使用,严禁用于任何违反中国大陆法律法规、您所在地区法律法规、QQ软件许可及服务协议的行为,开发者不承担任何相关行为导致的直接或间接责任。 62 | 63 | 本项目不对生成内容的完整性、准确性作任何担保,生成的一切内容不可用于法律取证,您不应当将其用于学习与交流外的任何用途。 64 | -------------------------------------------------------------------------------- /emojis.py: -------------------------------------------------------------------------------- 1 | emojis = {4: '得意', 5: '流泪', 8: '睡', 9: '大哭', 10: '尴尬', 12: '调皮', 14: '微笑', 16: '酷', 21: '可爱', 2 | 23: '傲慢', 24: '饥饿', 25: '困', 26: '惊恐', 27: '流汗', 28: '憨笑', 29: '悠闲', 30: '奋斗', 32: '疑问', 3 | 33: '嘘', 34: '晕', 38: '敲打', 39: '再见', 41: '发抖', 42: '爱情', 43: '跳跳', 49: '拥抱', 53: '蛋糕', 4 | 60: '咖啡', 63: '玫瑰', 66: '爱心', 74: '太阳', 75: '月亮', 76: '赞', 78: '握手', 79: '胜利', 85: '飞吻', 5 | 89: '西瓜', 96: '冷汗', 97: '擦汗', 98: '抠鼻', 99: '鼓掌', 100: '糗大了', 101: '坏笑', 102: '左哼哼', 6 | 103: '右哼哼', 104: '哈欠', 106: '委屈', 109: '左亲亲', 111: '可怜', 116: '示爱', 118: '抱拳', 120: '拳头', 7 | 122: '爱你', 123: 'NO', 124: 'OK', 125: '转圈', 129: '挥手', 144: '喝彩', 147: '棒棒糖', 171: '茶', 8 | 173: '泪奔', 174: '无奈', 175: '卖萌', 176: '小纠结', 179: 'doge', 180: '惊喜', 181: '骚扰', 182: '笑哭', 9 | 183: '我最美', 201: '点赞', 203: '托脸', 212: '托腮', 214: '啵啵', 219: '蹭一蹭', 222: '抱抱', 227: '拍手', 10 | 232: '佛系', 240: '喷脸', 243: '甩头', 246: '加油抱抱', 262: '脑阔疼', 264: '捂脸', 265: '辣眼睛', 11 | 266: '哦哟', 267: '头秃', 268: '问号脸', 269: '暗中观察', 270: 'emm', 271: '吃瓜', 272: '呵呵哒', 12 | 273: '我酸了', 277: '汪汪', 278: '汗', 281: '无眼笑', 282: '敬礼', 284: '面无表情', 285: '摸鱼', 287: '哦', 13 | 289: '睁眼', 290: '敲开心', 293: '摸锦鲤', 294: '期待', 297: '拜谢', 298: '元宝', 299: '牛啊', 305: '右亲亲', 14 | 306: '牛气冲天', 307: '喵喵', 314: '仔细分析', 315: '加油', 318: '崇拜', 319: '比心', 320: '庆祝', 15 | 322: '拒绝', 324: '吃糖', 326: '生气', 9728: '☀ 晴天', 9749: '☕ 咖啡', 9786: '☺ 可爱', 10024: '✨ 闪光', 16 | 10060: '❌ 错误', 10068: '❔ 问号', 127801: '🌹 玫瑰', 127817: '🍉 西瓜', 127822: '🍎 苹果', 127827: '🍓 草莓', 17 | 127836: '🍜 拉面', 127838: '🍞 面包', 127847: '🍧 刨冰', 127866: '🍺 啤酒', 127867: '🍻 干杯', 127881: '🎉 庆祝', 18 | 128027: '🐛 虫', 128046: '🐮 牛', 128051: '🐳 鲸鱼', 128053: '🐵 猴', 128074: '👊 拳头', 128076: '👌 好的', 19 | 128077: '👍 厉害', 128079: '👏 鼓掌', 128089: '👙 内衣', 128102: '👦 男孩', 128104: '👨 爸爸', 128147: '💓 爱心', 20 | 128157: '💝 礼物', 128164: '💤 睡觉', 128166: '💦 水', 128168: '💨 吹气', 128170: '💪 肌肉', 128235: '📫 邮箱', 21 | 128293: '🔥 火', 128513: '😁 呲牙', 128514: '😂 激动', 128516: '😄 高兴', 128522: '😊 嘿嘿', 128524: '😌 羞涩', 22 | 128527: '😏 哼哼', 128530: '😒 不屑', 128531: '😓 汗', 128532: '😔 失落', 128536: '😘 飞吻', 128538: '😚 亲亲', 23 | 128540: '😜 淘气', 128541: '😝 吐舌', 128557: '😭 大哭', 128560: '😰 紧张', 128563: '😳 瞪眼'} 24 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import tomllib 3 | 4 | from sys import argv 5 | from pathlib import Path 6 | 7 | import db 8 | from exporter import base_elements 9 | from exporter.json import JsonExportManager 10 | from exporter.txt import TxtExportManager 11 | from exporter.html import HtmlExportManager 12 | 13 | logging.basicConfig( 14 | level=logging.INFO, # 设置默认日志级别 15 | format='%(asctime)s - %(levelname)s - %(message)s', 16 | handlers=[logging.StreamHandler()] 17 | ) 18 | 19 | exporter_map = { 20 | "txt": TxtExportManager, 21 | "json": JsonExportManager, 22 | "html": HtmlExportManager 23 | } 24 | 25 | 26 | def mk_output_path(path): 27 | c2c_path = path / "c2c" 28 | group_path = path / "group" 29 | 30 | if not c2c_path.exists(): 31 | c2c_path.mkdir(parents=True) 32 | if not group_path.exists(): 33 | group_path.mkdir(parents=True) 34 | 35 | return c2c_path, group_path 36 | 37 | 38 | def run_single(task_type, queries, Exporter, path, dbman): 39 | logging.info(f"开始解析并写入{task_type}消息") 40 | Exporter(dbman,queries, path, task_type).process() 41 | 42 | logging.info(f"成功解析并写入{task_type}消息") 43 | 44 | 45 | def main(): 46 | config_path = Path(argv[1]) 47 | with open(config_path, "rb") as f: 48 | config = tomllib.load(f) 49 | 50 | db_path = Path(config["db_path"]) 51 | 52 | pic_path = Path(config["pic_path"]) 53 | base_elements.pic_path = pic_path 54 | 55 | if not config["output_path"]: 56 | output_path = Path(db_path).parent / "output" 57 | else: 58 | output_path = Path(config["output_path"]) 59 | 60 | c2c_path, group_path = mk_output_path(output_path) 61 | 62 | c2c_filters = config["c2c_filters"] 63 | group_filters = config["group_filters"] 64 | 65 | dbman = db.DatabaseManager(db_path) 66 | 67 | c2c_queries = dbman.c2c_messages(c2c_filters) 68 | group_queries = dbman.group_messages(group_filters) 69 | 70 | for output_type in config["output_format"]: 71 | c2c_exporter = exporter_map[output_type] 72 | group_exporter = exporter_map[output_type] 73 | 74 | run_single("c2c", c2c_queries, c2c_exporter, c2c_path, dbman) 75 | run_single("group", group_queries, group_exporter, group_path, dbman) 76 | 77 | logging.info(f"成功导出{output_type}格式") 78 | 79 | 80 | if __name__ == '__main__': 81 | main() 82 | -------------------------------------------------------------------------------- /exporter/html/elements.py: -------------------------------------------------------------------------------- 1 | from json import loads, JSONDecodeError 2 | 3 | from exporter.base_elements import * 4 | 5 | __all__ = ["Text", 6 | "Image", 7 | "File", 8 | "Voice", 9 | "Video", 10 | "Emoji", 11 | "Notice", 12 | "RedPacket", 13 | "Application", 14 | "Call", 15 | "Feed"] 16 | 17 | class Text(BaseText): 18 | def _get_content(self): 19 | return self.text 20 | 21 | class Image(BaseImage): 22 | def _get_content(self): 23 | 24 | return f"""{self.text} {self.readable_size}""" 25 | 26 | class File(BaseFile): 27 | def _get_content(self): 28 | return f"[文件] {self.file_name} {self.readable_size}" 29 | 30 | class Voice(BaseVoice): 31 | def _get_content(self): 32 | return f"[语音]" + " ".join( 33 | part for part in [ 34 | f"{self.voice_len}″ {self.voice_text}", 35 | self.file_name, 36 | self.readable_size 37 | ] if part 38 | ) 39 | 40 | class Video(BaseVideo): 41 | def _get_content(self): 42 | return "[视频]" + " ".join( 43 | part for part in [ 44 | f"{self.formated_video_len} {self.file_name} {self.readable_size}", 45 | self.path 46 | ] if part 47 | ) 48 | 49 | class Emoji(BaseEmoji): 50 | def _get_content(self): 51 | return f"[表情]{self.text}-{self.emoji_id}" 52 | 53 | class Notice(BaseNotice): 54 | def _get_content(self): 55 | return f"[提示]{self._parse_info()}" 56 | 57 | class RedPacket(BaseRedPacket): 58 | def _get_content(self): 59 | return f"[红包]{self.summary} {self.prompt}" 60 | 61 | class Application(BaseApplication): 62 | def _get_content(self): 63 | if self.raw and self.raw.strip(): 64 | try: 65 | data = loads(self.raw) 66 | prompt = data.get("prompt", "(无提示信息)") # 使用 .get() 避免因缺少 "prompt" 键而引发 KeyError 67 | return f"[应用消息]{prompt}" 68 | except JSONDecodeError: 69 | # 如果 self.raw 不是有效的 JSON,可以在这里处理 70 | return "[应用消息](无效的消息格式)" 71 | return "[应用消息](空消息)" 72 | 73 | class Call(BaseCall): 74 | def _get_content(self): 75 | return f"[通话]{self.status}-{self.text}" 76 | 77 | class Feed(BaseFeed): 78 | def _get_content(self): 79 | return f"""[动态消息]{self.title}
{self.feed_content}
""" 80 | -------------------------------------------------------------------------------- /element.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | message Elements {repeated Element elements = 40800;} 4 | 5 | message Element { 6 | uint64 id = 45001; 7 | uint32 type = 45002; 8 | // 1:文本段,2:图片段,3:文件消息,4:语音消息,5:视频,6:表情段,7:引用段, 9 | // 8:提示消息(中间灰色),9:红包,10:应用消息 10 | // 21:通话段 11 | // 26:动态消息 12 | 13 | 14 | // 文本 15 | string text = 45101; 16 | 17 | 18 | // 图片 19 | // 可引用fileName 20 | // 可引用fileSize 21 | bytes md5HexStr = 45406; // 小写 22 | uint64 original = 45418; // 0 false, 1 true 23 | string originImageMd5 = 45424; 24 | 25 | string imageUrlLow = 45802; 26 | string imageUrlHigh = 45803; 27 | string imageUrlOrigin = 45804; 28 | 29 | string imageFilePath = 45812; 30 | 31 | string imageText = 45815; 32 | 33 | 34 | // 文件 35 | string fileName = 45402; 36 | uint64 fileSize = 45405; 37 | 38 | uint64 fileTimestamp = 45505; // ? 39 | 40 | 41 | // 语音消息 42 | // 可引用 fileName 43 | // 可引用 fileSize 44 | string voiceText =45923; 45 | uint64 voiceLen = 45906; //单位为秒 46 | 47 | // 视频 48 | // 可引用 fileName 49 | // 可引用 fileSize 50 | string videoPath = 46403; 51 | uint64 videoLen = 45410; //单位为秒 52 | uint64 videoWidth = 45411; 53 | uint64 videoHeight = 45412; 54 | uint64 videoWidth2 = 46413; 55 | uint64 videoHeight2 = 46414; 56 | 57 | // 表情消息 58 | // 1: QQ 系统表情,2: emoji 表情 59 | // https://bot.q.qq.com/wiki/develop/api-v2/openapi/emoji/model.html 60 | uint32 emojiId = 47601; 61 | string emojiText = 47602; 62 | 63 | 64 | // 引用 65 | string senderUid = 40020; 66 | string interlocutorUid = 40021; 67 | 68 | uint32 senderNum = 47403; 69 | uint32 quotedTimestamp = 47404; 70 | uint32 interlocutorNum = 47411; 71 | 72 | Element quotedElement = 47423; 73 | 74 | 75 | // 提示消息 76 | string noticeInfo = 48214; 77 | string noticeInfo2 = 48271; // ? 78 | 79 | 80 | // 红包 81 | RedPacket redPacket = 48403; 82 | 83 | 84 | // 应用消息 85 | string applicationMessage = 47901; 86 | 87 | 88 | // 通话消息 89 | string callStatus = 48153; 90 | string callText = 48157; 91 | 92 | // 动态消息 93 | FeedMessage feedTitle = 48175; 94 | FeedMessage feedContent = 48176; 95 | 96 | string feedUrl = 48180; 97 | string feedLogoUrl = 48181; 98 | uint32 feedPublisherNum = 48182; 99 | 100 | string feedJumpInfo = 48183; 101 | string feedPublisherUid = 48188; 102 | } 103 | 104 | message FeedMessage {string text = 48178;} 105 | 106 | message RedPacket { 107 | string greeting = 48443; 108 | string prompt = 48444; 109 | string redPacketType = 48445; 110 | string summary = 48448; 111 | } -------------------------------------------------------------------------------- /exporter/txt/elements.py: -------------------------------------------------------------------------------- 1 | from exporter.base_elements import * 2 | 3 | from json import loads, JSONDecodeError 4 | 5 | __all__ = [ 6 | "Text", 7 | "Image", 8 | "File", 9 | "Voice", 10 | "Video", 11 | "Emoji", 12 | "Notice", 13 | "RedPacket", 14 | "Application", 15 | "Call", 16 | "Feed", 17 | ] 18 | 19 | class Text(BaseText): 20 | def _get_content(self): 21 | return "[文本]", self.text 22 | 23 | 24 | class Image(BaseImage): 25 | def _get_content(self): 26 | return "[图片]", "\n".join( 27 | part for part in [ 28 | f"{self.text}{self.cache_path} {self.readable_size}", 29 | self.file_path, 30 | self.file_url 31 | ] if part 32 | ) 33 | 34 | 35 | class File(BaseFile): 36 | def _get_content(self): 37 | return "[文件]", f"{self.file_name} {self.readable_size}" 38 | 39 | 40 | class Voice(BaseVoice): 41 | def _get_content(self): 42 | return "[语音]", "\n".join( 43 | part for part in [ 44 | f"{self.voice_len}″ {self.voice_text}", 45 | self.file_name, 46 | self.readable_size 47 | ] if part 48 | ) 49 | 50 | 51 | class Video(BaseVideo): 52 | def _get_content(self): 53 | return "[视频]", "\n".join( 54 | part for part in [ 55 | f"{self.formated_video_len} {self.file_name} {self.readable_size}", 56 | self.path 57 | ] if part 58 | ) 59 | 60 | 61 | class Emoji(BaseEmoji): 62 | def _get_content(self): 63 | return "[表情]", f"{self.text}-{self.emoji_id}" 64 | 65 | 66 | class Notice(BaseNotice): 67 | def _get_content(self): 68 | return "[提示]", self._parse_info() 69 | 70 | class RedPacket(BaseRedPacket): 71 | def _get_content(self): 72 | return "[红包]", f"{self.summary} {self.prompt}" 73 | 74 | 75 | class Application(BaseApplication): 76 | def _get_content(self): 77 | if self.raw and self.raw.strip(): 78 | try: 79 | data = loads(self.raw) 80 | prompt = data.get("prompt", "(无提示信息)") # 使用 .get() 避免因缺少 "prompt" 键而引发 KeyError 81 | return f"[应用消息]{prompt}" 82 | except JSONDecodeError: 83 | # 如果 self.raw 不是有效的 JSON,可以在这里处理 84 | return "[应用消息](无效的消息格式)" 85 | return "[应用消息](空消息)" 86 | 87 | 88 | class Call(BaseCall): 89 | def _get_content(self): 90 | return "[通话]", f"{self.status}-{self.text}" 91 | 92 | 93 | class Feed(BaseFeed): 94 | def _get_content(self): 95 | return "[动态消息]", "\n".join( 96 | part for part in [ 97 | self.title, 98 | self.feed_content, 99 | self.url 100 | ] if part 101 | ) 102 | -------------------------------------------------------------------------------- /db/man.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker 5 | from sqlalchemy.orm.query import Query 6 | 7 | __all__ = ["DatabaseManager"] 8 | 9 | 10 | class DatabaseManager: 11 | _models = defaultdict(defaultdict) # {db_id: {table_name: model}} 12 | _engines = {} # {db_id: engine} 13 | _binds = {} # {model: engine} 14 | 15 | @classmethod 16 | def register_model(cls, db_id: str) -> callable: 17 | def wrapper(model): 18 | cls._models[db_id][model.__tablename__] = model 19 | return model 20 | 21 | return wrapper 22 | 23 | def __new__(cls, db_path): 24 | for db_filename in cls._models.keys(): 25 | db_file = db_path / f"{db_filename}.db" 26 | if db_file.exists(): 27 | engine = create_engine(f"sqlite:///{db_file}") 28 | cls._engines[db_filename] = engine 29 | 30 | # 将该数据库下的所有模型绑定到对应的引擎 31 | for model in cls._models[db_filename].values(): 32 | cls._binds[model] = engine 33 | 34 | # 重新配置 session factory 35 | cls._session_factory = sessionmaker(binds=cls._binds) 36 | cls._session_factory.configure(binds=cls._binds) 37 | cls.session = cls._session_factory() 38 | 39 | return super(DatabaseManager, cls).__new__(cls) 40 | 41 | def __init__(self, db_path): 42 | pass 43 | 44 | def num_to_uid(self, num: int) -> str: 45 | model = self._models["nt_msg"]["nt_uid_mapping_table"] 46 | return self.session.query(model).filter_by(qq_num = num).first().uid 47 | 48 | def c2c_messages(self, filters): 49 | model = self._models["nt_msg"]["c2c_msg_table"] 50 | query = self.session.query(model) 51 | if filters: 52 | uids = [self.num_to_uid(num) for num in filters] 53 | else: 54 | uids = [row[0] for row in self.session.query(model.interlocutor_uid).distinct().all()] 55 | 56 | queries = {uid: query.filter_by(interlocutor_uid = uid).order_by(model.time) for uid in uids} 57 | 58 | return queries 59 | 60 | 61 | def group_messages(self, filters): 62 | model = self._models["nt_msg"]["group_msg_table"] 63 | query = self.session.query(model) 64 | if not filters: 65 | filters = [row[0] for row in self.session.query(model.mixed_group_num).distinct().all()] 66 | queries = {num: query.filter_by(mixed_group_num = num).order_by(model.time) for num in filters} 67 | return queries 68 | 69 | def profile_info(self, uid): 70 | model = self._models["profile_info"]["profile_info_v6"] 71 | return self.session.query(model).filter_by(uid = uid).first() 72 | 73 | def group_info(self, group_num): 74 | model = self._models["group_info"]["group_list"] 75 | return self.session.query(model) \ 76 | .filter_by(group_number = group_num) \ 77 | .first() 78 | 79 | from .models import * -------------------------------------------------------------------------------- /element_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # NO CHECKED-IN PROTOBUF GENCODE 4 | # source: element.proto 5 | # Protobuf Python Version: 6.31.1 6 | """Generated protocol buffer code.""" 7 | from google.protobuf import descriptor as _descriptor 8 | from google.protobuf import descriptor_pool as _descriptor_pool 9 | from google.protobuf import runtime_version as _runtime_version 10 | from google.protobuf import symbol_database as _symbol_database 11 | from google.protobuf.internal import builder as _builder 12 | _runtime_version.ValidateProtobufRuntimeVersion( 13 | _runtime_version.Domain.PUBLIC, 14 | 6, 15 | 31, 16 | 1, 17 | '', 18 | 'element.proto' 19 | ) 20 | # @@protoc_insertion_point(imports) 21 | 22 | _sym_db = _symbol_database.Default() 23 | 24 | 25 | 26 | 27 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\relement.proto\"(\n\x08\x45lements\x12\x1c\n\x08\x65lements\x18\xe0\xbe\x02 \x03(\x0b\x32\x08.Element\"\xfe\x07\n\x07\x45lement\x12\x0c\n\x02id\x18\xc9\xdf\x02 \x01(\x04\x12\x0e\n\x04type\x18\xca\xdf\x02 \x01(\r\x12\x0e\n\x04text\x18\xad\xe0\x02 \x01(\t\x12\x13\n\tmd5HexStr\x18\xde\xe2\x02 \x01(\x0c\x12\x12\n\x08original\x18\xea\xe2\x02 \x01(\x04\x12\x18\n\x0eoriginImageMd5\x18\xf0\xe2\x02 \x01(\t\x12\x15\n\x0bimageUrlLow\x18\xea\xe5\x02 \x01(\t\x12\x16\n\x0cimageUrlHigh\x18\xeb\xe5\x02 \x01(\t\x12\x18\n\x0eimageUrlOrigin\x18\xec\xe5\x02 \x01(\t\x12\x17\n\rimageFilePath\x18\xf4\xe5\x02 \x01(\t\x12\x13\n\timageText\x18\xf7\xe5\x02 \x01(\t\x12\x12\n\x08\x66ileName\x18\xda\xe2\x02 \x01(\t\x12\x12\n\x08\x66ileSize\x18\xdd\xe2\x02 \x01(\x04\x12\x17\n\rfileTimestamp\x18\xc1\xe3\x02 \x01(\x04\x12\x13\n\tvoiceText\x18\xe3\xe6\x02 \x01(\t\x12\x12\n\x08voiceLen\x18\xd2\xe6\x02 \x01(\x04\x12\x13\n\tvideoPath\x18\xc3\xea\x02 \x01(\t\x12\x12\n\x08videoLen\x18\xe2\xe2\x02 \x01(\x04\x12\x14\n\nvideoWidth\x18\xe3\xe2\x02 \x01(\x04\x12\x15\n\x0bvideoHeight\x18\xe4\xe2\x02 \x01(\x04\x12\x15\n\x0bvideoWidth2\x18\xcd\xea\x02 \x01(\x04\x12\x16\n\x0cvideoHeight2\x18\xce\xea\x02 \x01(\x04\x12\x11\n\x07\x65mojiId\x18\xf1\xf3\x02 \x01(\r\x12\x13\n\temojiText\x18\xf2\xf3\x02 \x01(\t\x12\x13\n\tsenderUid\x18\xd4\xb8\x02 \x01(\t\x12\x19\n\x0finterlocutorUid\x18\xd5\xb8\x02 \x01(\t\x12\x13\n\tsenderNum\x18\xab\xf2\x02 \x01(\r\x12\x19\n\x0fquotedTimestamp\x18\xac\xf2\x02 \x01(\r\x12\x19\n\x0finterlocutorNum\x18\xb3\xf2\x02 \x01(\r\x12!\n\rquotedElement\x18\xbf\xf2\x02 \x01(\x0b\x32\x08.Element\x12\x14\n\nnoticeInfo\x18\xd6\xf8\x02 \x01(\t\x12\x15\n\x0bnoticeInfo2\x18\x8f\xf9\x02 \x01(\t\x12\x1f\n\tredPacket\x18\x93\xfa\x02 \x01(\x0b\x32\n.RedPacket\x12\x1c\n\x12\x61pplicationMessage\x18\x9d\xf6\x02 \x01(\t\x12\x14\n\ncallStatus\x18\x99\xf8\x02 \x01(\t\x12\x12\n\x08\x63\x61llText\x18\x9d\xf8\x02 \x01(\t\x12!\n\tfeedTitle\x18\xaf\xf8\x02 \x01(\x0b\x32\x0c.FeedMessage\x12#\n\x0b\x66\x65\x65\x64\x43ontent\x18\xb0\xf8\x02 \x01(\x0b\x32\x0c.FeedMessage\x12\x11\n\x07\x66\x65\x65\x64Url\x18\xb4\xf8\x02 \x01(\t\x12\x15\n\x0b\x66\x65\x65\x64LogoUrl\x18\xb5\xf8\x02 \x01(\t\x12\x1a\n\x10\x66\x65\x65\x64PublisherNum\x18\xb6\xf8\x02 \x01(\r\x12\x16\n\x0c\x66\x65\x65\x64JumpInfo\x18\xb7\xf8\x02 \x01(\t\x12\x1a\n\x10\x66\x65\x65\x64PublisherUid\x18\xbc\xf8\x02 \x01(\t\"\x1d\n\x0b\x46\x65\x65\x64Message\x12\x0e\n\x04text\x18\xb2\xf8\x02 \x01(\t\"]\n\tRedPacket\x12\x12\n\x08greeting\x18\xbb\xfa\x02 \x01(\t\x12\x10\n\x06prompt\x18\xbc\xfa\x02 \x01(\t\x12\x17\n\rredPacketType\x18\xbd\xfa\x02 \x01(\t\x12\x11\n\x07summary\x18\xc0\xfa\x02 \x01(\tb\x06proto3') 28 | 29 | _globals = globals() 30 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 31 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'element_pb2', _globals) 32 | if not _descriptor._USE_C_DESCRIPTORS: 33 | DESCRIPTOR._loaded_options = None 34 | _globals['_ELEMENTS']._serialized_start=17 35 | _globals['_ELEMENTS']._serialized_end=57 36 | _globals['_ELEMENT']._serialized_start=60 37 | _globals['_ELEMENT']._serialized_end=1082 38 | _globals['_FEEDMESSAGE']._serialized_start=1084 39 | _globals['_FEEDMESSAGE']._serialized_end=1113 40 | _globals['_REDPACKET']._serialized_start=1115 41 | _globals['_REDPACKET']._serialized_end=1208 42 | # @@protoc_insertion_point(module_scope) 43 | -------------------------------------------------------------------------------- /exporter/json/exporter.py: -------------------------------------------------------------------------------- 1 | from atexit import register 2 | from collections import defaultdict 3 | from datetime import datetime 4 | from pathlib import Path 5 | from json import dump 6 | from tqdm import tqdm 7 | 8 | from exporter.txt.elements import * 9 | from ..__init__ import sanitize_filename 10 | 11 | __all__ = ["JsonExportManager"] 12 | 13 | 14 | class JsonExportManager: 15 | def __init__(self, dbman, queries, output_path, task_type): 16 | self.export_queue: dict[Path, list] = defaultdict(list) 17 | self.dbman = dbman 18 | self.queries = queries 19 | self.output_path = output_path 20 | self.task_type = task_type 21 | register(self.save) 22 | 23 | def process(self): 24 | Exporter = C2cJsonExporter if self.task_type == "c2c" else GroupJsonExporter 25 | for interlocutor_uid, query in self.queries.items(): 26 | if self.task_type == "c2c": 27 | profile_info = self.dbman.profile_info(interlocutor_uid) 28 | if profile_info: 29 | filename = profile_info.remark or profile_info.nickname 30 | elif query.first().mapping: 31 | filename = query.first().mapping.qq_num 32 | else: 33 | filename = query.first().interlocutor_num 34 | else: 35 | group_info = self.dbman.group_info(query.first().mixed_group_num) 36 | if group_info: 37 | filename = group_info.remark or group_info.name 38 | else: 39 | filename = query.first().mixed_group_num 40 | 41 | json_path = self.output_path / f"{sanitize_filename(filename)}.json" 42 | for message in tqdm(query.all()): 43 | exporter = Exporter(message) 44 | self.export_queue[json_path].append(exporter.content_dict) 45 | 46 | def save(self): 47 | for path in self.export_queue: 48 | with path.open(mode="w+", encoding="utf-8") as f: 49 | dump(self.export_queue[path], f, ensure_ascii=False, indent=2) 50 | 51 | 52 | class BaseExporter: 53 | def __init__(self, message): 54 | self.message = message 55 | self.readable_time = datetime.fromtimestamp(message.time).strftime("%Y-%m-%d %H:%M:%S") 56 | self.contents = [] 57 | self.elements_map = { 58 | 1: Text, 59 | 2: Image, 60 | 3: File, 61 | 4: Voice, 62 | 5: Video, 63 | 6: Emoji, 64 | 8: Notice, 65 | 9: RedPacket, 66 | 10: Application, 67 | 21: Call, 68 | 26: Feed 69 | } 70 | self.content_dict = self._content_dict() 71 | 72 | def _extract(self): 73 | for element in self.message.elements.elements: 74 | self.contents.append(self._extract_single(element)) 75 | 76 | def _extract_single(self, element): 77 | if element.type in self.elements_map: 78 | return self.elements_map[element.type](element).content 79 | elif element.type == 7: 80 | result = self._extract_single(element.quotedElement) 81 | if result[0]: 82 | result = ("[被引用的消息]" + result[0], result[1]) 83 | else: 84 | result = ("[被引用的消息]", result[1]) 85 | return result 86 | return None, None 87 | 88 | def _content_dict(self): 89 | pass 90 | 91 | 92 | class C2cJsonExporter(BaseExporter): 93 | def __init__(self, message): 94 | super().__init__(message) 95 | 96 | def _content_dict(self): 97 | self._extract() 98 | if self.message.sender_flag == 0: 99 | direction = "收" 100 | elif self.message.sender_flag in (1, 2): 101 | direction = "发" 102 | elif self.message.sender_flag == 8: 103 | direction = "转发" 104 | else: 105 | direction = "未知" 106 | 107 | return { 108 | "time": self.readable_time, 109 | "direction": direction, 110 | "contents": self.contents, 111 | } 112 | 113 | 114 | class GroupJsonExporter(BaseExporter): 115 | def __init__(self, message): 116 | super().__init__(message) 117 | 118 | def _content_dict(self): 119 | self._extract() 120 | if self.message.group_name_card: 121 | display_identity = self.message.group_name_card 122 | elif self.message.nickname: 123 | display_identity = self.message.nickname 124 | elif self.message.sender_profile: 125 | display_identity = self.message.sender_profile.group_name_card or self.message.sender_profile.nickname or self.message.sender_profile.qq_num 126 | else: 127 | display_identity = self.message.sender_num 128 | 129 | return { 130 | "time": self.readable_time, 131 | "sender": display_identity, 132 | "sender_qq": self.message.sender_num, 133 | "contents": self.contents, 134 | } 135 | -------------------------------------------------------------------------------- /exporter/txt/exporter.py: -------------------------------------------------------------------------------- 1 | from atexit import register 2 | from collections import defaultdict 3 | from datetime import datetime 4 | from pathlib import Path 5 | from tqdm import tqdm 6 | 7 | from .elements import * 8 | from ..__init__ import sanitize_filename 9 | 10 | __all__ = ["TxtExportManager"] 11 | 12 | 13 | class TxtExportManager: 14 | def __init__(self,dbman,queries, output_path, task_type): 15 | 16 | self.export_queue: dict[Path:list] = defaultdict(list) 17 | self.dbman = dbman 18 | self.queries = queries 19 | self.output_path = output_path 20 | self.task_type = task_type 21 | register(self.save) 22 | 23 | def process(self): 24 | Exporter = C2cTxtExporter if self.task_type == "c2c" else GroupTxtExporter 25 | for interlocutor_uid,query in self.queries.items(): 26 | if self.task_type == "c2c": 27 | profile_info = self.dbman.profile_info(interlocutor_uid) 28 | if profile_info: 29 | filename = profile_info.remark or profile_info.nickname 30 | elif query.first().mapping: 31 | filename = query.first().mapping.qq_num 32 | else: 33 | filename = query.first().interlocutor_num 34 | else: 35 | group_info = self.dbman.group_info(query.first().mixed_group_num) 36 | if group_info: 37 | filename = group_info.remark or group_info.name 38 | else: 39 | filename = query.first().mixed_group_num 40 | 41 | txt_path = self.output_path / f"{sanitize_filename(filename)}.txt" 42 | for message in tqdm(query.all()): 43 | exporter = Exporter(message) 44 | self.export_queue[txt_path].append(exporter.content_str) 45 | 46 | def save(self): 47 | for path in self.export_queue: 48 | with path.open(mode="w+", encoding="utf-8") as f: 49 | for content in self.export_queue[path]: 50 | f.write(content) 51 | 52 | class BaseExporter: 53 | def __init__(self, message): 54 | self.message = message 55 | self.readable_time = datetime.fromtimestamp(message.time).strftime("%Y-%m-%d %H:%M:%S") 56 | self.contents = [] 57 | self.elements_map = { 58 | 1: Text, 59 | 2: Image, 60 | 3: File, 61 | 4: Voice, 62 | 5: Video, 63 | 6: Emoji, 64 | 8: Notice, 65 | 9: RedPacket, 66 | 10: Application, 67 | 21: Call, 68 | 26: Feed 69 | } 70 | self.content_str = self._content_str() 71 | 72 | def _extract(self): 73 | for element in self.message.elements.elements: 74 | self.contents.append(self._extract_single(element)) 75 | 76 | def _extract_single(self, element): 77 | if element.type in self.elements_map: 78 | return self.elements_map[element.type](element).content 79 | elif element.type == 7: 80 | result = self._extract_single(element.quotedElement) 81 | if result[0]: 82 | result = ("[被引用的消息]" + result[0], result[1]) 83 | else: 84 | result = ("[被引用的消息]", result[1]) 85 | return result 86 | 87 | return None, None 88 | 89 | def _content_str(self): 90 | pass 91 | 92 | class C2cTxtExporter(BaseExporter): 93 | def __init__(self, message): 94 | super().__init__(message) 95 | 96 | 97 | def _content_str(self): 98 | self._extract() 99 | 100 | if self.message.sender_flag == 0: 101 | direction = "收" 102 | elif self.message.sender_flag in (1, 2): 103 | direction = "发" 104 | elif self.message.sender_flag == 8: 105 | direction = "转发" 106 | else: 107 | direction = "未知" 108 | 109 | content_str = f"""{self.readable_time} {direction}\n""" 110 | 111 | for content in self.contents: 112 | content_str += f"{content[0]}\n{content[1]}\n" 113 | 114 | content_str += "\n" 115 | 116 | return content_str 117 | 118 | class GroupTxtExporter(BaseExporter): 119 | def __init__(self, message): 120 | super().__init__(message) 121 | 122 | def _content_str(self): 123 | self._extract() 124 | 125 | if self.message.group_name_card: 126 | display_identity = self.message.group_name_card 127 | elif self.message.nickname: 128 | display_identity = self.message.nickname 129 | elif self.message.sender_profile: 130 | display_identity = self.message.sender_profile.group_name_card or self.message.sender_profile.nickname or self.message.sender_profile.qq_num 131 | else: 132 | display_identity = self.message.sender_num 133 | 134 | content_str = f"""{self.readable_time} {display_identity}\n""" 135 | 136 | for content in self.contents: 137 | content_str += f"{content[0]}\n{content[1]}\n" 138 | 139 | content_str += "\n" 140 | 141 | return content_str -------------------------------------------------------------------------------- /exporter/html/expoter.py: -------------------------------------------------------------------------------- 1 | from atexit import register 2 | from collections import defaultdict 3 | from datetime import datetime 4 | from pathlib import Path 5 | from tqdm import tqdm 6 | 7 | from .elements import * 8 | from ..__init__ import sanitize_filename 9 | 10 | from jinja2 import Environment, FileSystemLoader 11 | 12 | env = Environment(loader=FileSystemLoader('.')) 13 | template = env.get_template('template/template.html') 14 | 15 | class HtmlExportManager: 16 | def __init__(self,dbman,queries, output_path, task_type): 17 | 18 | self.export_queue: dict[Path:list] = defaultdict(list) 19 | self.dbman = dbman 20 | self.queries = queries 21 | self.output_path = output_path 22 | self.task_type = task_type 23 | register(self.save) 24 | 25 | def process(self): 26 | Exporter = C2cHtmlExporter if self.task_type == "c2c" else GroupHtmlExporter 27 | for interlocutor_uid,query in self.queries.items(): 28 | if self.task_type == "c2c": 29 | profile_info = self.dbman.profile_info(interlocutor_uid) 30 | if profile_info: 31 | filename = profile_info.remark or profile_info.nickname 32 | elif query.first().mapping: 33 | filename = query.first().mapping.qq_num 34 | else: 35 | filename = query.first().interlocutor_num 36 | else: 37 | group_info = self.dbman.group_info(query.first().mixed_group_num) 38 | if group_info: 39 | filename = group_info.remark or group_info.name 40 | else: 41 | filename = query.first().mixed_group_num 42 | 43 | html_path = self.output_path / f"{sanitize_filename(filename)}.html" 44 | for message in tqdm(query.all()): 45 | exporter = Exporter(message) 46 | self.export_queue[html_path].append(exporter.content_dict) 47 | 48 | def save(self): 49 | for path in self.export_queue: 50 | html_content = template.render(title = path.stem, messages = self.export_queue[path]) 51 | with path.open(mode="w", encoding="utf-8") as f: 52 | f.write(html_content) 53 | 54 | class BaseExporter: 55 | def __init__(self, message): 56 | self.message = message 57 | self.readable_time = datetime.fromtimestamp(message.time).strftime("%Y-%m-%d %H:%M:%S") 58 | self.contents = [] 59 | self.elements_map = { 60 | 1: Text, 61 | 2: Image, 62 | 3: File, 63 | 4: Voice, 64 | 5: Video, 65 | 6: Emoji, 66 | 8: Notice, 67 | 9: RedPacket, 68 | 10: Application, 69 | 21: Call, 70 | 26: Feed 71 | } 72 | self.content_dict = self._content_dict() 73 | 74 | def _extract(self): 75 | for element in self.message.elements.elements: 76 | self.contents.append(self._extract_single(element)) 77 | 78 | def _extract_single(self, element): 79 | if element.type in self.elements_map: 80 | return self.elements_map[element.type](element).content 81 | elif element.type == 7: 82 | result = self._extract_single(element.quotedElement) 83 | if result: 84 | result = ("[被引用的消息]" + result) 85 | else: 86 | result = ("[被引用的消息]", result) 87 | return result 88 | 89 | return None 90 | 91 | def _content_dict(self): 92 | pass 93 | 94 | class C2cHtmlExporter(BaseExporter): 95 | def __init__(self, message): 96 | super().__init__(message) 97 | 98 | 99 | def _content_dict(self): 100 | self._extract() 101 | 102 | if self.message.sender_flag == 0: 103 | display_identity = "收" 104 | elif self.message.sender_flag in (1, 2): 105 | display_identity = "发" 106 | elif self.message.sender_flag == 8: 107 | display_identity = "转发" 108 | else: 109 | display_identity = "未知" 110 | 111 | content_str = "\n" 112 | 113 | for content in self.contents: 114 | content_str += f"{content}\n" 115 | 116 | content_str += "\n" 117 | 118 | content_dict = {"readable_time": self.readable_time, 119 | "display_identity": display_identity, 120 | "content": content_str} 121 | 122 | return content_dict 123 | 124 | class GroupHtmlExporter(BaseExporter): 125 | def __init__(self, message): 126 | super().__init__(message) 127 | 128 | def _content_dict(self): 129 | self._extract() 130 | 131 | if self.message.group_name_card: 132 | display_identity = self.message.group_name_card 133 | elif self.message.nickname: 134 | display_identity = self.message.nickname 135 | elif self.message.sender_profile: 136 | display_identity = self.message.sender_profile.group_name_card or self.message.sender_profile.nickname or self.message.sender_profile.qq_num 137 | else: 138 | display_identity = self.message.sender_num 139 | 140 | content_str = "\n" 141 | 142 | for content in self.contents: 143 | content_str += f"{content}\n" 144 | 145 | content_str += "\n" 146 | 147 | content_dict = {"readable_time": self.readable_time, 148 | "display_identity": display_identity, 149 | "content": content_str} 150 | 151 | return content_dict -------------------------------------------------------------------------------- /exporter/base_elements.py: -------------------------------------------------------------------------------- 1 | from ast import literal_eval 2 | from functools import lru_cache 3 | from lxml import etree as lxml_etree 4 | 5 | from humanize import naturalsize 6 | from unicodedata import category 7 | 8 | from emojis import emojis 9 | 10 | pic_path = "" 11 | 12 | __all__ = [ 13 | "BaseText", 14 | "BaseImage", 15 | "BaseFile", 16 | "BaseVoice", 17 | "BaseVideo", 18 | "BaseEmoji", 19 | "BaseNotice", 20 | "BaseRedPacket", 21 | "BaseApplication", 22 | "BaseCall", 23 | "BaseFeed", 24 | ] 25 | 26 | 27 | def readable_file_size(file_size): 28 | """ 29 | Returns a human-readable file size. 30 | """ 31 | return naturalsize(file_size, binary=True, format="%.2f") if file_size else None 32 | 33 | 34 | class BaseText: 35 | def __init__(self, element): 36 | self.text = element.text 37 | 38 | self.content = self._get_content() 39 | 40 | def _get_content(self): 41 | pass 42 | 43 | 44 | class BaseImage: 45 | def __init__(self, element): 46 | self.text = element.imageText 47 | self.file_name = element.fileName 48 | self.readable_size = readable_file_size(element.fileSize) 49 | self.file_path = element.imageFilePath 50 | self.file_url = element.imageUrlOrigin 51 | 52 | self.cache_path = self._get_cache_path(element.original, element.md5HexStr.hex().upper(),pic_path) 53 | 54 | self.content = self._get_content() 55 | 56 | @staticmethod 57 | @lru_cache(maxsize=4096) 58 | def _get_cache_path(original, md5HexStr, pic_path): 59 | def crc64(raw_str): 60 | _crc64_table = [0] * 256 61 | for i in range(256): 62 | bf = i 63 | for _ in range(8): 64 | bf = bf >> 1 ^ -7661587058870466123 if bf & 1 else bf >> 1 65 | _crc64_table[i] = bf 66 | value = -1 67 | for char in raw_str: 68 | value = _crc64_table[(ord(char) ^ value) & 255] ^ value >> 8 69 | return value 70 | 71 | # original == 0 指未发原图,图片存于chatraw 72 | # original == 1 指发送原图,压缩后的图片存于chatimg,下载后原图存于chatraw 73 | folder = "chatimg" if original else "chatraw" 74 | raw_str = f"{folder}:{md5HexStr}" 75 | crc64_value = crc64(raw_str) 76 | file_name = f"Cache_{crc64_value:x}" 77 | 78 | return pic_path / folder/ file_name[-3:] / file_name 79 | 80 | def _get_content(self): 81 | pass 82 | 83 | 84 | class BaseFile: 85 | def __init__(self, element): 86 | self.file_name = element.fileName 87 | self.readable_size = readable_file_size(element.fileSize) 88 | 89 | self.content = self._get_content() 90 | 91 | def _get_content(self): 92 | pass 93 | 94 | 95 | class BaseVoice: 96 | def __init__(self, element): 97 | self.voice_text = element.voiceText 98 | self.voice_len = element.voiceLen 99 | self.file_name = element.fileName 100 | self.readable_size = readable_file_size(element.fileSize) 101 | 102 | self.content = self._get_content() 103 | 104 | def _get_content(self): 105 | pass 106 | 107 | 108 | class BaseVideo: 109 | def __init__(self, element): 110 | self.formated_video_len = self._seconds_to_hms(element.videoLen) 111 | self.file_name = element.fileName 112 | self.readable_size = readable_file_size(element.fileSize) 113 | self.path = element.videoPath 114 | 115 | self.content = self._get_content() 116 | 117 | @staticmethod 118 | def _seconds_to_hms(seconds): 119 | hours, remainder = divmod(seconds, 3600) 120 | minutes, seconds = divmod(remainder, 60) 121 | 122 | return f"{hours:02}:{minutes:02}:{seconds:02}" 123 | 124 | def _get_content(self): 125 | pass 126 | 127 | 128 | class BaseEmoji: 129 | def __init__(self, element): 130 | self.emoji_id = element.emojiId 131 | self.text = element.emojiText or emojis.get(self.emoji_id, "未知表情") 132 | 133 | self.content = self._get_content() 134 | 135 | def _get_content(self): 136 | pass 137 | 138 | 139 | class BaseNotice: 140 | def __init__(self, element): 141 | self.info = element.noticeInfo 142 | self.info2 = element.noticeInfo2 143 | 144 | self.content = self._get_content() 145 | 146 | def _parse_info(self): 147 | if not self.info and not self.info2: 148 | return "[提示]", None 149 | elif self.info: 150 | self.info = self.info.replace(r'\/', '/').replace('\u3000', ' ') 151 | self.info = ''.join(char for char in self.info if category(char) not in ('Cf', 'Cc')) 152 | 153 | recover_parser = lxml_etree.XMLParser(recover=True) 154 | try: 155 | root = lxml_etree.fromstring(self.info) 156 | except lxml_etree.XMLSyntaxError: 157 | # 尝试使用恢复模式重新解析 158 | root = lxml_etree.fromstring(self.info.encode("utf-8"), parser=recover_parser) 159 | texts = [ 160 | elem.get('txt') 161 | for elem in root.findall('.//nor') 162 | if elem.get('txt') 163 | ] 164 | elif self.info2: 165 | info2_dict = literal_eval(self.info2.replace(r"\/", "/")) 166 | texts = [item.get("txt", "") 167 | for item 168 | in info2_dict["items"]] 169 | 170 | return " ".join(texts) 171 | 172 | def _get_content(self): 173 | pass 174 | 175 | 176 | class BaseRedPacket: 177 | def __init__(self, element): 178 | self.prompt = element.redPacket.prompt 179 | self.summary = element.redPacket.summary 180 | 181 | self.content = self._get_content() 182 | 183 | def _get_content(self): 184 | pass 185 | 186 | 187 | class BaseApplication: 188 | def __init__(self, element): 189 | self.raw = element.applicationMessage 190 | 191 | self.content = self._get_content() 192 | 193 | def _get_content(self): 194 | pass 195 | 196 | 197 | class BaseCall: 198 | def __init__(self, element): 199 | self.status = element.callStatus 200 | self.text = element.callText 201 | 202 | self.content = self._get_content() 203 | 204 | def _get_content(self): 205 | pass 206 | 207 | class BaseFeed: 208 | def __init__(self, element): 209 | self.title = element.feedTitle.text 210 | self.feed_content = element.feedContent.text 211 | self.url = element.feedUrl 212 | 213 | self.content = self._get_content() 214 | 215 | def _get_content(self): 216 | pass 217 | -------------------------------------------------------------------------------- /db/models.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | 3 | from sqlalchemy import String, LargeBinary, Text, ForeignKey 4 | from sqlalchemy.ext.declarative import declarative_base 5 | from sqlalchemy.ext.hybrid import hybrid_property 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship, object_session 7 | 8 | import element_pb2 9 | from .man import DatabaseManager 10 | 11 | __all__ = ["C2cMessage", "GroupMessage", "UidMapping", "ProfileInfo", "GroupList", "GroupMember"] 12 | 13 | profile_map = {} 14 | group_map = {} 15 | member_map = defaultdict(dict) 16 | 17 | Base = declarative_base() 18 | 19 | 20 | class Message(): 21 | id: Mapped[int] = mapped_column("40001", primary_key=True) 22 | random: Mapped[int] = mapped_column("40002") 23 | seq: Mapped[int] = mapped_column("40003") 24 | chat_type: Mapped[int] = mapped_column("40010") 25 | msg_type: Mapped[int] = mapped_column("40011") 26 | sub_msg_type: Mapped[int] = mapped_column("40012") 27 | sender_flag: Mapped[int] = mapped_column("40013") 28 | sender_uid: Mapped[str] = mapped_column("40020", String(24)) # Tencent internal UID 29 | UNK_08: Mapped[int] = mapped_column("40026") 30 | 31 | UNK_11: Mapped[int] = mapped_column("40040") 32 | status: Mapped[int] = mapped_column("40041") 33 | time: Mapped[int] = mapped_column("40050") # time message sent 34 | UNK_14: Mapped[int] = mapped_column("40052") 35 | 36 | nickname: Mapped[str] = mapped_column("40093", Text) # only self, otherwise basically empty 37 | message_body: Mapped[bytes] = mapped_column("40800", LargeBinary) # protobuf 38 | UNK_18: Mapped[bytes] = mapped_column("40900", 39 | LargeBinary) # When msg_type == 8, stores a cache of forwarded message.When 9, stores the quoted messages. 40 | UNK_19: Mapped[int] = mapped_column("40105") 41 | UNK_20: Mapped[int] = mapped_column("40005") 42 | timestamp_day: Mapped[int] = mapped_column("40058") # Time at 00:00 the day 43 | UNK_22: Mapped[int] = mapped_column("40006") 44 | 45 | UNK_24: Mapped[bytes] = mapped_column("40600", LargeBinary) # If the value is 14 00 (hex), is a quoted message 46 | 47 | quoted_seq: Mapped[int] = mapped_column("40850") # seq of the message which this message quoted 48 | UNK_27: Mapped[int] = mapped_column("40851") 49 | UNK_28: Mapped[bytes] = mapped_column("40601", LargeBinary) # always null 50 | UNK_29: Mapped[bytes] = mapped_column("40801", LargeBinary) # protobuf 51 | # protobuf, insufficient resource, related with a file? 52 | UNK_30: Mapped[bytes] = mapped_column("40605", LargeBinary) 53 | sender_num: Mapped[int] = mapped_column("40033") # qq num 54 | UNK_33: Mapped[int] = mapped_column("40062") 55 | UNK_34: Mapped[int] = mapped_column("40083") 56 | UNK_35: Mapped[int] = mapped_column("40084") 57 | 58 | @property 59 | def elements(self): 60 | elements = element_pb2.Elements() 61 | try: 62 | elements.ParseFromString(self.message_body) 63 | return elements 64 | except: 65 | return elements 66 | 67 | 68 | @DatabaseManager.register_model("nt_msg") 69 | class C2cMessage(Base, Message): 70 | """ 71 | C2c Message Table 72 | nt_msg.db -> c2c_msg_table 73 | """ 74 | __tablename__ = "c2c_msg_table" 75 | 76 | # https://github.com/QQBackup/qq-win-db-key/issues/52 77 | 78 | interlocutor_uid: Mapped[str] = mapped_column("40021", String(24), 79 | ForeignKey("nt_uid_mapping_table.48902")) # Tencent internal UID 80 | UNK_10: Mapped[int] = mapped_column("40027") # group num 81 | UNK_15: Mapped[str] = mapped_column("40090", Text) # group name card 82 | UNK_23: Mapped[int] = mapped_column("40100") # @ status 83 | UNK_25: Mapped[int] = mapped_column("40060") 84 | interlocutor_num: Mapped[int] = mapped_column("40030") # qq num 85 | 86 | mapping = relationship('UidMapping', back_populates='c2c_messages') 87 | 88 | @DatabaseManager.register_model("nt_msg") 89 | class GroupMessage(Base, Message): 90 | """ 91 | Group Message Table 92 | nt_msg.db -> group_msg_table 93 | """ 94 | __tablename__ = "group_msg_table" 95 | group_num: Mapped[str] = mapped_column("40021", String(24)) 96 | group_num2: Mapped[int] = mapped_column("40027") 97 | group_name_card: Mapped[str] = mapped_column("40090", Text) # group name card 98 | at_status: Mapped[int] = mapped_column("40100") # @ status 99 | group_status: Mapped[int] = mapped_column("40060") 100 | group_num3: Mapped[int] = mapped_column("40030") 101 | 102 | @hybrid_property 103 | def mixed_group_num(self): 104 | return self.group_num or self.group_num2 or self.group_num3 105 | 106 | @property 107 | def sender_profile(self): 108 | if self.sender_uid in member_map: 109 | if self.mixed_group_num in member_map[self.sender_uid]: 110 | return member_map[self.sender_uid][self.mixed_group_num] 111 | query_result = ( 112 | object_session(self) 113 | .query(GroupMember) 114 | .filter(GroupMember.uid == self.sender_uid) 115 | .filter(GroupMember.group_number == self.mixed_group_num) 116 | .first() 117 | ) 118 | member_map[self.sender_uid][self.mixed_group_num] = query_result 119 | return query_result 120 | 121 | @DatabaseManager.register_model("nt_msg") 122 | class UidMapping(Base): 123 | """ 124 | Uid Mapping Table 125 | nt_msg.db -> nt_uid_mapping_table 126 | """ 127 | __tablename__ = "nt_uid_mapping_table" 128 | 129 | id: Mapped[int] = mapped_column("48901", primary_key=True) 130 | uid: Mapped[str] = mapped_column("48902", String(24), ) 131 | UNK_02: Mapped[str] = mapped_column("48912", nullable=True) 132 | qq_num: Mapped[int] = mapped_column("1002") 133 | 134 | c2c_messages = relationship("C2cMessage", back_populates="mapping") 135 | 136 | 137 | @DatabaseManager.register_model("profile_info") 138 | class ProfileInfo(Base): 139 | """ 140 | 好友信息 141 | profile_info.db -> profile_info_v6 142 | """ 143 | __tablename__ = "profile_info_v6" 144 | qid: Mapped[str] = mapped_column("1001") 145 | qq_num: Mapped[int] = mapped_column("1002") 146 | nickname: Mapped[str] = mapped_column("20002") 147 | UNK_4: Mapped[str] = mapped_column("24106") 148 | UNK_5: Mapped[str] = mapped_column("24107") 149 | UNK_6: Mapped[str] = mapped_column("24108") 150 | UNK_7: Mapped[str] = mapped_column("24109") 151 | remark: Mapped[str] = mapped_column("20009") 152 | signature: Mapped[str] = mapped_column("20011") 153 | uid: Mapped[str] = mapped_column("1000", primary_key=True) 154 | UNK_11: Mapped[int] = mapped_column("20001") 155 | UNK_12: Mapped[int] = mapped_column("20003") 156 | avatar_url: Mapped[str] = mapped_column("20004") 157 | UNK_14: Mapped[int] = mapped_column("20005") 158 | UNK_15: Mapped[int] = mapped_column("20006") 159 | UNK_16: Mapped[int] = mapped_column("20007") 160 | UNK_17: Mapped[int] = mapped_column("20008") 161 | UNK_18: Mapped[int] = mapped_column("20010") 162 | UNK_19: Mapped[int] = mapped_column("20012") 163 | UNK_20: Mapped[int] = mapped_column("20014") 164 | UNK_21: Mapped[bytes] = mapped_column("20017") 165 | UNK_22: Mapped[int] = mapped_column("20016") 166 | UNK_23: Mapped[int] = mapped_column("24103") 167 | UNK_24: Mapped[bytes] = mapped_column("20042") 168 | UNK_25: Mapped[bytes] = mapped_column("20059") 169 | UNK_26: Mapped[int] = mapped_column("20060") 170 | UNK_27: Mapped[int] = mapped_column("20061") 171 | UNK_28: Mapped[int] = mapped_column("20043") 172 | UNK_29: Mapped[int] = mapped_column("20048") 173 | UNK_30: Mapped[int] = mapped_column("20037") 174 | UNK_31: Mapped[int] = mapped_column("20056") 175 | UNK_32: Mapped[int] = mapped_column("20067") 176 | UNK_33: Mapped[bytes] = mapped_column("20057") 177 | UNK_34: Mapped[int] = mapped_column("20070") 178 | UNK_35: Mapped[int] = mapped_column("20071") 179 | UNK_36: Mapped[bytes] = mapped_column("21000") 180 | relation: Mapped[bytes] = mapped_column("20072") 181 | UNK_38: Mapped[int] = mapped_column("20075") 182 | UNK_39: Mapped[bytes] = mapped_column("20066") 183 | UNK_40: Mapped[int] = mapped_column("24104") 184 | UNK_41: Mapped[bytes] = mapped_column("24105") 185 | UNK_42: Mapped[int] = mapped_column("24110") 186 | UNK_43: Mapped[int] = mapped_column("24111") 187 | 188 | @DatabaseManager.register_model("group_info") 189 | class GroupList(Base): 190 | """ 191 | 群列表 192 | group_info.db -> group_list 193 | """ 194 | __tablename__ = "group_list" 195 | group_number: Mapped[int] = mapped_column("60001", primary_key=True) 196 | UNK_02: Mapped[int] = mapped_column("60221") 197 | create_time: Mapped[int] = mapped_column("60004") 198 | max_member: Mapped[int] = mapped_column("60005") 199 | member_count: Mapped[int] = mapped_column("60006") 200 | name: Mapped[str] = mapped_column("60007") 201 | UNK_07: Mapped[int] = mapped_column("60008") 202 | UNK_08: Mapped[int] = mapped_column("60009") 203 | UNK_09: Mapped[int] = mapped_column("60020") 204 | UNK_10: Mapped[int] = mapped_column("60011") 205 | UNK_11: Mapped[int] = mapped_column("60010") 206 | UNK_12: Mapped[int] = mapped_column("60017") 207 | UNK_13: Mapped[int] = mapped_column("60018") 208 | remark: Mapped[str] = mapped_column("60026") 209 | UNK_15: Mapped[int] = mapped_column("60022") 210 | UNK_16: Mapped[int] = mapped_column("60023") 211 | UNK_17: Mapped[int] = mapped_column("60027") 212 | UNK_18: Mapped[int] = mapped_column("60028") 213 | UNK_19: Mapped[int] = mapped_column("60029") 214 | UNK_20: Mapped[int] = mapped_column("60030") 215 | UNK_21: Mapped[int] = mapped_column("60031") 216 | UNK_22: Mapped[int] = mapped_column("60269") 217 | UNK_23: Mapped[int] = mapped_column("60012") 218 | UNK_24: Mapped[int] = mapped_column("60034") 219 | UNK_25: Mapped[int] = mapped_column("60035") 220 | UNK_26: Mapped[int] = mapped_column("60036") 221 | UNK_27: Mapped[int] = mapped_column("60037") 222 | UNK_28: Mapped[int] = mapped_column("60038") 223 | UNK_29: Mapped[int] = mapped_column("60204") 224 | UNK_30: Mapped[int] = mapped_column("60238") 225 | UNK_31: Mapped[int] = mapped_column("60258") 226 | UNK_32: Mapped[int] = mapped_column("60277") 227 | UNK_33: Mapped[bytes] = mapped_column("60040") 228 | UNK_34: Mapped[int] = mapped_column("60206") 229 | UNK_35: Mapped[int] = mapped_column("60255") 230 | UNK_36: Mapped[int] = mapped_column("60256") 231 | UNK_37: Mapped[int] = mapped_column("60279") 232 | UNK_38: Mapped[int] = mapped_column("60280") 233 | UNK_39: Mapped[int] = mapped_column("60281") 234 | UNK_40: Mapped[int] = mapped_column("60299") 235 | latest_bulletin: Mapped[bytes] = mapped_column("60216") 236 | UNK_42: Mapped[int] = mapped_column("60310") 237 | UNK_43: Mapped[int] = mapped_column("60259") 238 | UNK_44: Mapped[int] = mapped_column("60304") 239 | UNK_45: Mapped[str] = mapped_column("60267") 240 | UNK_46: Mapped[int] = mapped_column("60294") 241 | UNK_47: Mapped[int] = mapped_column("60295") 242 | UNK_48: Mapped[int] = mapped_column("60250") 243 | UNK_49: Mapped[int] = mapped_column("60262") 244 | UNK_50: Mapped[int] = mapped_column("60298") 245 | UNK_51: Mapped[int] = mapped_column("60252") 246 | UNK_52: Mapped[int] = mapped_column("60344") 247 | 248 | 249 | @DatabaseManager.register_model("group_info") 250 | class GroupMember(Base): 251 | """ 252 | 群成员 253 | group_info.db -> group_member3 254 | """ 255 | __tablename__ = "group_member3" 256 | group_name_card: Mapped[str] = mapped_column("64003") 257 | nickname: Mapped[str] = mapped_column("20002") 258 | group_number: Mapped[int] = mapped_column("60001", primary_key=True) 259 | uid: Mapped[str] = mapped_column("1000", primary_key=True) 260 | UNK_05: Mapped[str] = mapped_column("1001") 261 | qq_num: Mapped[int] = mapped_column("1002") 262 | UNK_07: Mapped[int] = mapped_column("64002") 263 | UNK_08: Mapped[bytes] = mapped_column("64004") 264 | UNK_09: Mapped[int] = mapped_column("64005") 265 | UNK_10: Mapped[int] = mapped_column("64006") 266 | join_time: Mapped[int] = mapped_column("64007") 267 | lastest_message_time: Mapped[int] = mapped_column("64008") 268 | lastest_ban_ends: Mapped[int] = mapped_column("64009") 269 | manager_flag: Mapped[int] = mapped_column("64010") #0 for False, 1 for True 270 | UNK_15: Mapped[int] = mapped_column("64011") 271 | UNK_16: Mapped[int] = mapped_column("64012") 272 | UNK_17: Mapped[int] = mapped_column("64013") 273 | UNK_18: Mapped[int] = mapped_column("64017") 274 | UNK_19: Mapped[int] = mapped_column("64015") 275 | status: Mapped[int] = mapped_column("64016") # 0 for in, 1 for exited 276 | UNK_21: Mapped[int] = mapped_column("64018") 277 | UNK_22: Mapped[int] = mapped_column("64034") 278 | UNK_23: Mapped[int] = mapped_column("64020") 279 | UNK_24: Mapped[int] = mapped_column("64021") 280 | UNK_25: Mapped[int] = mapped_column("64022") 281 | custom_badge: Mapped[str] = mapped_column("64023") 282 | UNK_27: Mapped[int] = mapped_column("64024") 283 | UNK_28: Mapped[int] = mapped_column("64025") 284 | UNK_29: Mapped[int] = mapped_column("64026") 285 | UNK_30: Mapped[int] = mapped_column("64027") 286 | UNK_31: Mapped[int] = mapped_column("64028") 287 | UNK_32: Mapped[str] = mapped_column("64029") 288 | UNK_33: Mapped[int] = mapped_column("64030") 289 | UNK_34: Mapped[int] = mapped_column("64031") 290 | UNK_35: Mapped[int] = mapped_column("64032") 291 | level: Mapped[int] = mapped_column("64035") -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | --------------------------------------------------------------------------------