/////////////////// // JS EVALUATION // /////////////////// const protos = [] const modules = { "$InternalEnum": { exports: { exports: function (data) { data.__enum__ = true return data } } }, } function requireModule(name) { if (!modules[name]) { throw new Error(`Unknown requirement ${name}`) } return modules[name].exports } function requireDefault(name) { return requireModule(name).exports } function ignoreModule(name) { if (name === "WAProtoConst") { return false } else if (!name.endsWith(".pb")) { // Ignore any non-protobuf modules, except WAProtoConst above return true } else if (name.startsWith("MAWArmadillo") && (name.endsWith("TableSchema.pb") || name.endsWith("TablesSchema.pb"))) { // Ignore internal table schemas return true } else if (name === "WASignalLocalStorageProtocol.pb" || name === "WASignalWhisperTextProtocol.pb") { // Ignore standard signal protocol stuff return true } else { return false } } function defineModule(name, dependencies, callback, unknownIntOrNull) { if (ignoreModule(name)) { return } const exports = {} if (dependencies.length > 0) { callback(null, requireDefault, null, requireModule, null, null, exports) } else { callback(null, requireDefault, null, requireModule, exports, exports) } modules[name] = {exports, dependencies} } global.self = global global.__d = defineModule require("./e2ee.js") function dereference(obj, module, currentPath, next, ...remainder) { if (!next) { return obj } if (!obj.messages[next]) { obj.messages[next] = {messages: {}, enums: {}, __module__: module, __path__: currentPath, __name__: next} } return dereference(obj.messages[next], module, currentPath.concat([next]), ...remainder) } function dereferenceSnake(obj, currentPath, path) { let next = path[0] path = path.slice(1) while (!obj.messages[next]) { if (path.length === 0) { return [obj, currentPath, next] } next += path[0] path = path.slice(1) } return dereferenceSnake(obj.messages[next], currentPath.concat([next]), path) } function renameModule(name) { return name.replace(".pb", "") } function renameDependencies(dependencies) { return dependencies .filter(name => name.endsWith(".pb")) .map(renameModule) .map(name => name === "WAProtocol" ? "WACommon" : name) } function renameType(protoName, fieldName, field) { return fieldName } for (const [name, module] of Object.entries(modules)) { if (!name.endsWith(".pb")) { continue } else if (!module.exports) { console.warn(name, "has no exports") continue } // Slightly hacky way to get rid of WAProtocol.pb and just use the MessageKey in WACommon if (name === "WAProtocol.pb") { if (Object.entries(module.exports).length > 1) { console.warn("WAProtocol.pb has more than one export") } module.exports["MessageKeySpec"].__name__ = "MessageKey" module.exports["MessageKeySpec"].__module__ = "WACommon" module.exports["MessageKeySpec"].__path__ = [] continue } const proto = { __protobuf__: true, messages: {}, enums: {}, __name__: renameModule(name), dependencies: renameDependencies(module.dependencies), } const upperSnakeEnums = [] for (const [name, field] of Object.entries(module.exports)) { const namePath = name.replace(/Spec$/, "").split("$") field.__name__ = renameType(proto.__name__, namePath[namePath.length - 1], field) namePath[namePath.length - 1] = field.__name__ field.__path__ = namePath.slice(0, -1) field.__module__ = proto.__name__ if (field.internalSpec) { dereference(proto, proto.__name__, [], ...namePath).message = field.internalSpec } else if (namePath.length === 1 && name.toUpperCase() === name) { upperSnakeEnums.push(field) } else { dereference(proto, proto.__name__, [], ...namePath.slice(0, -1)).enums[field.__name__] = field } } // Some enums have uppercase names, instead of capital case with $ separators. // For those, we need to find the right nesting location. for (const field of upperSnakeEnums) { field.__enum__ = true const [obj, path, name] = dereferenceSnake(proto, [], field.__name__.split("_").map(part => part[0] + part.slice(1).toLowerCase())) field.__path__ = path field.__name__ = name field.__module__ = proto.__name__ obj.enums[name] = field } protos.push(proto) } //////////////////////////////// // PROTOBUF SCHEMA GENERATION // //////////////////////////////// function indent(lines, indent = "\t") { return lines.map(line => line ? `${indent}${line}` : "") } function flattenWithBlankLines(...items) { return items .flatMap(item => item.length > 0 ? [item, [""]] : []) .slice(0, -1) .flatMap(item => item) } function protoifyChildren(container) { return flattenWithBlankLines( ...Object.values(container.enums).map(protoifyEnum), ...Object.values(container.messages).map(protoifyMessage), ) } function protoifyEnum(enumDef) { const values = [] const names = Object.fromEntries(Object.entries(enumDef).map(([name, value]) => [value, name])) if (!names["0"]) { if (names["-1"]) { enumDef[names["-1"]] = 0 } else { // TODO add snake case values.push(`${enumDef.__name__.toUpperCase()}_UNKNOWN = 0;`) } } for (const [name, value] of Object.entries(enumDef)) { if (name.startsWith("__") && name.endsWith("__")) { continue } values.push(`${name} = ${value};`) } return [`enum ${enumDef.__name__} ` + "{", ...indent(values), "}"] } const {TYPES, TYPE_MASK, FLAGS} = requireModule("WAProtoConst") function fieldTypeName(typeID, typeRef, parentModule, parentPath) { switch (typeID) { case TYPES.INT32: return "int32" case TYPES.INT64: return "int64" case TYPES.UINT32: return "uint32" case TYPES.UINT64: return "uint64" case TYPES.SINT32: return "sint32" case TYPES.SINT64: return "sint64" case TYPES.BOOL: return "bool" case TYPES.ENUM: case TYPES.MESSAGE: let pathStartIndex = 0 for (let i = 0; i < parentPath.length && i < typeRef.__path__.length; i++) { if (typeRef.__path__[i] === parentPath[i]) { pathStartIndex++ } else { break } } const namePath = [] if (typeRef.__module__ !== parentModule) { namePath.push(typeRef.__module__) pathStartIndex = 0 } namePath.push(...typeRef.__path__.slice(pathStartIndex)) namePath.push(typeRef.__name__) return namePath.join(".") case TYPES.FIXED64: return "fixed64" case TYPES.SFIXED64: return "sfixed64" case TYPES.DOUBLE: return "double" case TYPES.STRING: return "string" case TYPES.BYTES: return "bytes" case TYPES.FIXED32: return "fixed32" case TYPES.SFIXED32: return "sfixed32" case TYPES.FLOAT: return "float" } } const staticRenames = { id: "ID", jid: "JID", encIv: "encIV", iv: "IV", ptt: "PTT", hmac: "HMAC", url: "URL", fbid: "FBID", jpegThumbnail: "JPEGThumbnail", dsm: "DSM", } function fixFieldName(name) { if (name === "id") { return "ID" } else if (name === "encIv") { return "encIV" } return staticRenames[name] ?? name .replace(/Id([A-Zs]|$)/, "ID$1") .replace("Jid", "JID") .replace(/Ms([A-Z]|$)/, "MS$1") .replace(/Ts([A-Z]|$)/, "TS$1") .replace(/Mac([A-Z]|$)/, "MAC$1") .replace("Url", "URL") .replace("Cdn", "CDN") .replace("Json", "JSON") .replace("Jpeg", "JPEG") .replace("Sha256", "SHA256") } function protoifyField(name, [index, flags, typeRef], parentModule, parentPath) { const preflags = [] const postflags = [""] if ((flags & FLAGS.REPEATED) !== 0) { preflags.push("repeated") } // if ((flags & FLAGS.REQUIRED) === 0) { // preflags.push("optional") // } else { // preflags.push("required") // } preflags.push(fieldTypeName(flags & TYPE_MASK, typeRef, parentModule, parentPath)) if ((flags & FLAGS.PACKED) !== 0) { postflags.push(`[packed=true]`) } return `${preflags.join(" ")} ${fixFieldName(name)} = ${index}${postflags.join(" ")};` } function protoifyFields(fields, parentModule, parentPath) { return Object.entries(fields).map(([name, definition]) => protoifyField(name, definition, parentModule, parentPath)) } function protoifyMessage(message) { const sections = [protoifyChildren(message)] const spec = message.message const fullMessagePath = message.__path__.concat([message.__name__]) for (const [name, fieldNames] of Object.entries(spec.__oneofs__ ?? {})) { const fields = Object.fromEntries(fieldNames.map(fieldName => { const def = spec[fieldName] delete spec[fieldName] return [fieldName, def] })) sections.push([`oneof ${name} ` + "{", ...indent(protoifyFields(fields, message.__module__, fullMessagePath)), "}"]) } if (spec.__reserved__) { console.warn("Found reserved keys:", message.__name__, spec.__reserved__) } delete spec.__oneofs__ delete spec.__reserved__ sections.push(protoifyFields(spec, message.__module__, fullMessagePath)) return [`message ${message.__name__} ` + "{", ...indent(flattenWithBlankLines(...sections)), "}"] } function goPackageName(name) { return name.replace(/^WA/, "wa") } function protoifyModule(module) { const output = [] output.push(`syntax = "proto3";`) output.push(`package ${module.__name__};`) output.push(`option go_package = "go.mau.fi/whatsmeow/binary/armadillo/${goPackageName(module.__name__)}";`) output.push("") if (module.dependencies.length > 0) { for (const dependency of module.dependencies) { output.push(`import "${goPackageName(dependency)}/${dependency}.proto";`) } output.push("") } const children = protoifyChildren(module) children.push("") return output.concat(children) } const fs = require("fs") for (const proto of protos) { fs.mkdirSync(goPackageName(proto.__name__), {recursive: true}) fs.writeFileSync(`${goPackageName(proto.__name__)}/${proto.__name__}.proto`, protoifyModule(proto).join("\n")) }