From 4b7f42004512c83869370c20cc22398b1a4a2ab2 Mon Sep 17 00:00:00 2001 From: Jeff Carr Date: Thu, 27 Mar 2025 15:03:11 -0500 Subject: [PATCH] a real world example --- argv.go | 3 +- example/Signal-Desktop-Backups.proto | 1315 ++++++++++++++++++++++++++ main.go | 5 + protoReformat.go | 160 ++-- 4 files changed, 1412 insertions(+), 71 deletions(-) create mode 100644 example/Signal-Desktop-Backups.proto diff --git a/argv.go b/argv.go index 0f248cb..9b05d60 100644 --- a/argv.go +++ b/argv.go @@ -19,7 +19,8 @@ type args struct { Regret bool `arg:"--regret" help:"ignore needed UUID. You will eventually regret this."` Delete bool `arg:"--delete" help:"use delete with copy experiment"` DryRun bool `arg:"--dry-run" help:"check the .proto syntax, but don't do anything"` - Format bool `arg:"--format" help:"eformat the .proto file and exit"` + Format bool `arg:"--format" help:"format the .proto file and exit"` + Comments bool `arg:"--format-comments" help:"enforce parseable comments in a .proto file"` NoFormat bool `arg:"--no-format" help:"do not auto-reformat the .proto file"` GoSrc string `arg:"--go-src" help:"default is ~/go/src. could be set to your go.work path"` GoPath string `arg:"--gopath" help:"the gopath of this repo"` diff --git a/example/Signal-Desktop-Backups.proto b/example/Signal-Desktop-Backups.proto new file mode 100644 index 0000000..c939e53 --- /dev/null +++ b/example/Signal-Desktop-Backups.proto @@ -0,0 +1,1315 @@ +// Copyright 2024 Signal Messenger, LLC +// SPDX-License-Identifier: AGPL-3.0-only +syntax = "proto3"; + +package signalbackups; + +option java_package = "org.thoughtcrime.securesms.backup.v2.proto"; + +message BackupInfo { + uint64 version = 1; + uint64 backupTimeMs = 2; + bytes mediaRootBackupKey = 3; // 32-byte random value generated when the backup is uploaded for the first time. + string currentAppVersion = 4; + string firstAppVersion = 5; +} + +// Frames must follow in the following ordering rules: +// +// 1. There is exactly one AccountData and it is the first frame. +// 2. A frame referenced by ID must come before the referencing frame. +// e.g. a Recipient must come before any Chat referencing it. +// 3. All ChatItems must appear in global Chat rendering order. +// (The order in which they were received by the client.) +// 4. ChatFolders must appear in render order (e.g., left to right for +// LTR locales), but can appear anywhere relative to other frames respecting +// rule 2 (after Recipients and Chats). +// +// Recipients, Chats, StickerPacks, AdHocCalls, and NotificationProfiles +// can be in any order. (But must respect rule 2.) +// +// For example, Chats may all be together at the beginning, +// or may each immediately precede its first ChatItem. +message Frame { + // If unset, importers should skip this frame without throwing an error. + oneof item { + AccountData account = 1; + Recipient recipient = 2; + Chat chat = 3; + ChatItem chatItem = 4; + StickerPack stickerPack = 5; + AdHocCall adHocCall = 6; + NotificationProfile notificationProfile = 7; + ChatFolder chatFolder = 8; + } +} + +message AccountData { + enum PhoneNumberSharingMode { + UNKNOWN = 0; // Interpret as "Nobody" + EVERYBODY = 1; + NOBODY = 2; + } + message UsernameLink { + enum Color { + UNKNOWN = 0; // Interpret as "Blue" + BLUE = 1; + WHITE = 2; + GREY = 3; + OLIVE = 4; + GREEN = 5; + ORANGE = 6; + PINK = 7; + PURPLE = 8; + } + + bytes entropy = 1; // 32 bytes of entropy used for encryption + bytes serverId = 2; // 16 bytes of encoded UUID provided by the server + Color color = 3; + } + + message AccountSettings { + bool readReceipts = 1; + bool sealedSenderIndicators = 2; + bool typingIndicators = 3; + bool linkPreviews = 4; + bool notDiscoverableByPhoneNumber = 5; + bool preferContactAvatars = 6; + uint32 universalExpireTimerSeconds = 7; // 0 means no universal expire timer. + repeated string preferredReactionEmoji = 8; + bool displayBadgesOnProfile = 9; + bool keepMutedChatsArchived = 10; + bool hasSetMyStoriesPrivacy = 11; + bool hasViewedOnboardingStory = 12; + bool storiesDisabled = 13; + optional bool storyViewReceiptsEnabled = 14; + bool hasSeenGroupStoryEducationSheet = 15; + bool hasCompletedUsernameOnboarding = 16; + PhoneNumberSharingMode phoneNumberSharingMode = 17; + ChatStyle defaultChatStyle = 18; + repeated ChatStyle.CustomChatColor customChatColors = 19; + } + + message SubscriberData { + bytes subscriberId = 1; + string currencyCode = 2; + bool manuallyCancelled = 3; + } + + message IAPSubscriberData { + bytes subscriberId = 1; + + // If unset, importers should ignore the subscriber data without throwing an error. + oneof iapSubscriptionId { + // Identifies an Android Play Store IAP subscription. + string purchaseToken = 2; + // Identifies an iOS App Store IAP subscription. + uint64 originalTransactionId = 3; + } + } + + bytes profileKey = 1; + optional string username = 2; + UsernameLink usernameLink = 3; + string givenName = 4; + string familyName = 5; + string avatarUrlPath = 6; + SubscriberData donationSubscriberData = 7; + reserved 8; // A deprecated format // backupsSubscriberData + AccountSettings accountSettings = 9; + IAPSubscriberData backupsSubscriberData = 10; + string svrPin = 11; +} + +message Recipient { + uint64 id = 1; // generated id for reference only within this file + // If unset, importers should skip this frame without throwing an error. + oneof destination { + Contact contact = 2; + Group group = 3; + DistributionListItem distributionList = 4; + Self self = 5; + ReleaseNotes releaseNotes = 6; + CallLink callLink = 7; + } +} + +// If unset - computed as the value of the first byte of SHA-256(msg=CONTACT_ID) +// modulo the count of colors. Once set the avatar color for a recipient is +// never recomputed or changed. +// +// `CONTACT_ID` is the first available identifier from the list: +// - ServiceIdToBinary(ACI) +// - E164 +// - ServiceIdToBinary(PNI) +// - Group Id +enum AvatarColor { + A100 = 0; + A110 = 1; + A120 = 2; + A130 = 3; + A140 = 4; + A150 = 5; + A160 = 6; + A170 = 7; + A180 = 8; + A190 = 9; + A200 = 10; + A210 = 11; +} + +message Contact { + enum IdentityState { + DEFAULT = 0; // A valid value -- indicates unset by the user + VERIFIED = 1; + UNVERIFIED = 2; // Was once verified and is now unverified + } + + message Registered {} + message NotRegistered { + uint64 unregisteredTimestamp = 1; + } + + enum Visibility { + VISIBLE = 0; // A valid value -- the contact is not hidden + HIDDEN = 1; + HIDDEN_MESSAGE_REQUEST = 2; + } + + message Name { + string given = 1; + string family = 2; + } + + optional bytes aci = 1; // should be 16 bytes + optional bytes pni = 2; // should be 16 bytes + optional string username = 3; + optional uint64 e164 = 4; + bool blocked = 5; + Visibility visibility = 6; + + // If unset, consider the user to be registered + oneof registration { + Registered registered = 7; + NotRegistered notRegistered = 8; + } + + optional bytes profileKey = 9; + bool profileSharing = 10; + optional string profileGivenName = 11; + optional string profileFamilyName = 12; + bool hideStory = 13; + optional bytes identityKey = 14; + IdentityState identityState = 15; + Name nickname = 16; // absent iff both `given` and `family` are empty + string note = 17; + string systemGivenName = 18; + string systemFamilyName = 19; + string systemNickname = 20; + optional AvatarColor avatarColor = 21; +} + +message Group { + enum StorySendMode { + DEFAULT = 0; // A valid value -- indicates unset by the user + DISABLED = 1; + ENABLED = 2; + } + + bytes masterKey = 1; + bool whitelisted = 2; + bool hideStory = 3; + StorySendMode storySendMode = 4; + GroupSnapshot snapshot = 5; + bool blocked = 6; + optional AvatarColor avatarColor = 7; + + // These are simply plaintext copies of the groups proto from Groups.proto. + // They should be kept completely in-sync with Groups.proto. + // These exist to allow us to have the latest snapshot of a group during restoration without having to hit the network. + // We would use Groups.proto if we could, but we want a plaintext version to improve export readability. + // For documentation, defer to Groups.proto. The only name change is Group -> GroupSnapshot to avoid the naming conflict. + message GroupSnapshot { + reserved 1; // The field is deprecated in the context of static group state // publicKey + GroupAttributeBlob title = 2; + GroupAttributeBlob description = 11; + string avatarUrl = 3; + GroupAttributeBlob disappearingMessagesTimer = 4; + AccessControl accessControl = 5; + uint32 version = 6; + repeated Member members = 7; + repeated MemberPendingProfileKey membersPendingProfileKey = 8; + repeated MemberPendingAdminApproval membersPendingAdminApproval = 9; + bytes inviteLinkPassword = 10; + bool announcements_only = 12; + repeated MemberBanned members_banned = 13; + } + + message GroupAttributeBlob { + // If unset, consider the field it represents to not be present + oneof content { + string title = 1; + bytes avatar = 2; + uint32 disappearingMessagesDuration = 3; + string descriptionText = 4; + } + } + + message Member { + enum Role { + UNKNOWN = 0; // Intepret as "Default" + DEFAULT = 1; + ADMINISTRATOR = 2; + } + + bytes userId = 1; + Role role = 2; + reserved 3; // This field is ignored in Backups, in favor of Contact frames for members // profileKey + reserved 4; // This field is deprecated in the context of static group state // presentation + uint32 joinedAtVersion = 5; + } + + message MemberPendingProfileKey { + Member member = 1; + bytes addedByUserId = 2; + uint64 timestamp = 3; + } + + message MemberPendingAdminApproval { + bytes userId = 1; + reserved 2; // This field is ignored in Backups, in favor of Contact frames for members // profileKey + reserved 3; // This field is deprecated in the context of static group state // presentation + uint64 timestamp = 4; + } + + message MemberBanned { + bytes userId = 1; + uint64 timestamp = 2; + } + + message AccessControl { + enum AccessRequired { + UNKNOWN = 0; // Intepret as "Unsatisfiable" + ANY = 1; + MEMBER = 2; + ADMINISTRATOR = 3; + UNSATISFIABLE = 4; + } + + AccessRequired attributes = 1; + AccessRequired members = 2; + AccessRequired addFromInviteLink = 3; + } +} + +message Self { + optional AvatarColor avatarColor = 1; +} + +message ReleaseNotes {} + +message Chat { + uint64 id = 1; // generated id for reference only within this file + uint64 recipientId = 2; + bool archived = 3; + optional uint32 pinnedOrder = 4; // will be displayed in ascending order + optional uint64 expirationTimerMs = 5; + optional uint64 muteUntilMs = 6; // INT64_MAX (2^63 - 1) = "always muted". + bool markedUnread = 7; + bool dontNotifyForMentionsIfMuted = 8; + ChatStyle style = 9; + uint32 expireTimerVersion = 10; +} + +/** + * Call Links have some associated data including a call, but unlike other recipients + * are not tied to threads because they do not have messages associated with them. + * + * note: + * - room id can be derived from the root key + * - the presence of an admin key means this user is a call admin + */ +message CallLink { + enum Restrictions { + UNKNOWN = 0; // Interpret as "Admin Approval" + NONE = 1; + ADMIN_APPROVAL = 2; + } + + bytes rootKey = 1; + optional bytes adminKey = 2; // Only present if the user is an admin + string name = 3; + Restrictions restrictions = 4; + uint64 expirationMs = 5; +} + +message AdHocCall { + enum State { + UNKNOWN_STATE = 0; // Interpret as "Generic" + GENERIC = 1; + } + + uint64 callId = 1; + // Refers to a `CallLink` recipient. + uint64 recipientId = 2; + State state = 3; + uint64 callTimestamp = 4; +} + +message DistributionListItem { + // distribution ids are UUIDv4s. "My Story" is represented + // by an all-0 UUID (00000000-0000-0000-0000-000000000000). + bytes distributionId = 1; // distribution list ids are uuids + + // If unset, importers should skip the item entirely without showing an error. + oneof item { + uint64 deletionTimestamp = 2; + DistributionList distributionList = 3; + } +} + +message DistributionList { + enum PrivacyMode { + UNKNOWN = 0; // Interpret as "Only with" + ONLY_WITH = 1; + ALL_EXCEPT = 2; + ALL = 3; + } + + string name = 1; + bool allowReplies = 2; + PrivacyMode privacyMode = 3; + repeated uint64 memberRecipientIds = 4; // generated recipient id +} + +message ChatItem { + message IncomingMessageDetails { + uint64 dateReceived = 1; + optional uint64 dateServerSent = 2; + bool read = 3; + bool sealedSender = 4; + } + + message OutgoingMessageDetails { + repeated SendStatus sendStatus = 1; + } + + message DirectionlessMessageDetails { + } + + uint64 chatId = 1; // conversation id + uint64 authorId = 2; // recipient id + uint64 dateSent = 3; + optional uint64 expireStartDate = 4; // timestamp of when expiration timer started ticking down + optional uint64 expiresInMs = 5; // how long timer of message is (ms) + repeated ChatItem revisions = 6; // ordered from oldest to newest + bool sms = 7; + + // If unset, importers should skip this item without throwing an error. + oneof directionalDetails { + IncomingMessageDetails incoming = 8; + OutgoingMessageDetails outgoing = 9; + DirectionlessMessageDetails directionless = 10; + } + + // If unset, importers should skip this item without throwing an error. + oneof item { + StandardMessage standardMessage = 11; + ContactMessage contactMessage = 12; + StickerMessage stickerMessage = 13; + RemoteDeletedMessage remoteDeletedMessage = 14; + ChatUpdateMessage updateMessage = 15; + PaymentNotification paymentNotification = 16; + GiftBadge giftBadge = 17; + ViewOnceMessage viewOnceMessage = 18; + DirectStoryReplyMessage directStoryReplyMessage = 19; // group story reply messages are not backed up + } +} + +message SendStatus { + message Pending {} + + message Sent { + bool sealedSender = 1; + } + + message Delivered { + bool sealedSender = 1; + } + + message Read { + bool sealedSender = 1; + } + + message Viewed { + bool sealedSender = 1; + } + + // e.g. user in group was blocked, so we skipped sending to them + message Skipped {} + + message Failed { + enum FailureReason { + UNKNOWN = 0; // A valid value -- could indicate a crash or lack of information + NETWORK = 1; + IDENTITY_KEY_MISMATCH = 2; + } + + FailureReason reason = 1; + } + + uint64 recipientId = 1; + uint64 timestamp = 2; // the time the status was last updated -- if from a receipt, it should be the sentTime of the receipt + + // If unset, importers should consider the status to be "pending" + oneof deliveryStatus { + Pending pending = 3; + Sent sent = 4; + Delivered delivered = 5; + Read read = 6; + Viewed viewed = 7; + Skipped skipped = 8; + Failed failed = 9; + } +} + +message Text { + string body = 1; + repeated BodyRange bodyRanges = 2; +} + +message StandardMessage { + optional Quote quote = 1; + optional Text text = 2; + repeated MessageAttachment attachments = 3; + repeated LinkPreview linkPreview = 4; + optional FilePointer longText = 5; + repeated Reaction reactions = 6; +} + +message ContactMessage { + ContactAttachment contact = 1; + repeated Reaction reactions = 2; +} + +message DirectStoryReplyMessage { + message TextReply { + Text text = 1; + FilePointer longText = 2; + } + + // If unset, importers should ignore the message without throwing an error. + oneof reply { + TextReply textReply = 1; + string emoji = 2; + } + + repeated Reaction reactions = 3; + reserved 4; // storySentTimestamp +} + +message PaymentNotification { + message TransactionDetails { + message MobileCoinTxoIdentification { // Used to map to payments on the ledger + repeated bytes publicKey = 1; // for received transactions + repeated bytes keyImages = 2; // for sent transactions + } + + message FailedTransaction { // Failed payments can't be synced from the ledger + enum FailureReason { + GENERIC = 0; // A valid value -- reason unknown + NETWORK = 1; + INSUFFICIENT_FUNDS = 2; + } + FailureReason reason = 1; + } + + message Transaction { + enum Status { + INITIAL = 0; // A valid value -- state unconfirmed + SUBMITTED = 1; + SUCCESSFUL = 2; + } + Status status = 1; + + // This identification is used to map the payment table to the ledger + // and is likely required otherwise we may have issues reconciling with + // the ledger + MobileCoinTxoIdentification mobileCoinIdentification = 2; + optional uint64 timestamp = 3; + optional uint64 blockIndex = 4; + optional uint64 blockTimestamp = 5; + optional bytes transaction = 6; // mobile coin blobs + optional bytes receipt = 7; // mobile coin blobs + } + + // If unset, importers should treat the transaction as successful with no metadata. + oneof payment { + Transaction transaction = 1; + FailedTransaction failedTransaction = 2; + } + } + + optional string amountMob = 1; // stored as a decimal string, e.g. 1.00001 + optional string feeMob = 2; // stored as a decimal string, e.g. 1.00001 + optional string note = 3; + TransactionDetails transactionDetails = 4; +} + +message GiftBadge { + enum State { + UNOPENED = 0; // A valid state + OPENED = 1; + REDEEMED = 2; + FAILED = 3; + } + + bytes receiptCredentialPresentation = 1; + State state = 2; +} + +message ViewOnceMessage { + // Will be null for viewed messages + MessageAttachment attachment = 1; + repeated Reaction reactions = 2; +} + +message ContactAttachment { + message Name { + string givenName = 1; + string familyName = 2; + string prefix = 3; + string suffix = 4; + string middleName = 5; + string nickname = 6; + } + + message Phone { + enum Type { + UNKNOWN = 0; // Interpret as "Home" + HOME = 1; + MOBILE = 2; + WORK = 3; + CUSTOM = 4; + } + + string value = 1; + Type type = 2; + string label = 3; + } + + message Email { + enum Type { + UNKNOWN = 0; // Intepret as "Home" + HOME = 1; + MOBILE = 2; + WORK = 3; + CUSTOM = 4; + } + + string value = 1; + Type type = 2; + string label = 3; + } + + message PostalAddress { + enum Type { + UNKNOWN = 0; // Interpret as "Home" + HOME = 1; + WORK = 2; + CUSTOM = 3; + } + + Type type = 1; + string label = 2; + string street = 3; + string pobox = 4; + string neighborhood = 5; + string city = 6; + string region = 7; + string postcode = 8; + string country = 9; + } + + optional Name name = 1; + repeated Phone number = 3; + repeated Email email = 4; + repeated PostalAddress address = 5; + optional FilePointer avatar = 6; + string organization = 7; +} + +message StickerMessage { + Sticker sticker = 1; + repeated Reaction reactions = 2; +} + +// Tombstone for remote delete +message RemoteDeletedMessage {} + +message Sticker { + bytes packId = 1; + bytes packKey = 2; + uint32 stickerId = 3; + optional string emoji = 4; + // Stickers are uploaded to be sent as attachments; we also + // back them up as normal attachments when they are in messages. + // DO NOT treat this as the definitive source of a sticker in + // an installed StickerPack that shares the same packId. + FilePointer data = 5; +} + +message LinkPreview { + string url = 1; + optional string title = 2; + optional FilePointer image = 3; + optional string description = 4; + optional uint64 date = 5; +} + +// A FilePointer on a message that has additional +// metadata that applies only to message attachments. +message MessageAttachment { + // Similar to SignalService.AttachmentPointer.Flags, + // but explicitly mutually exclusive. Note the different raw values + // (non-zero starting values are not supported in proto3.) + enum Flag { + NONE = 0; // A valid value -- no flag applied + VOICE_MESSAGE = 1; + BORDERLESS = 2; + GIF = 3; + } + + FilePointer pointer = 1; + Flag flag = 2; + bool wasDownloaded = 3; + // Cross-client identifier for this attachment among all attachments on the + // owning message. See: SignalService.AttachmentPointer.clientUuid. + optional bytes clientUuid = 4; +} + +message FilePointer { + // References attachments in the backup (media) storage tier. + message BackupLocator { + string mediaName = 1; + // If present, the cdn number of the succesful upload. + // If empty/0, may still have been uploaded, and clients + // can discover the cdn number via the list endpoint. + optional uint32 cdnNumber = 2; + bytes key = 3; + bytes digest = 4; + uint32 size = 5; + // Fallback in case backup tier upload failed. + optional string transitCdnKey = 6; + optional uint32 transitCdnNumber = 7; + } + + // References attachments in the transit storage tier. + // May be downloaded or not when the backup is generated; + // primarily for free-tier users who cannot copy the + // attachments to the backup (media) storage tier. + message AttachmentLocator { + string cdnKey = 1; + uint32 cdnNumber = 2; + optional uint64 uploadTimestamp = 3; + bytes key = 4; + bytes digest = 5; + uint32 size = 6; + } + + // References attachments that are invalid in such a way where download + // cannot be attempted. Could range from missing digests to missing + // CDN keys or anything else that makes download attempts impossible. + // This serves as a 'tombstone' so that the UX can show that an attachment + // did exist, but for whatever reason it's not retrievable. + message InvalidAttachmentLocator { + } + + // If unset, importers should consider it to be an InvalidAttachmentLocator without throwing an error. + oneof locator { + BackupLocator backupLocator = 1; + AttachmentLocator attachmentLocator = 2; + InvalidAttachmentLocator invalidAttachmentLocator = 3; + } + + optional string contentType = 4; + optional bytes incrementalMac = 5; + optional uint32 incrementalMacChunkSize = 6; + optional string fileName = 7; + optional uint32 width = 8; + optional uint32 height = 9; + optional string caption = 10; + optional string blurHash = 11; +} + +message Quote { + enum Type { + UNKNOWN = 0; // Interpret as "Normal" + NORMAL = 1; + GIFT_BADGE = 2; + VIEW_ONCE = 3; + } + + message QuotedAttachment { + optional string contentType = 1; + optional string fileName = 2; + optional MessageAttachment thumbnail = 3; + } + + optional uint64 targetSentTimestamp = 1; // null if the target message could not be found at time of quote insert + uint64 authorId = 2; + optional Text text = 3; + repeated QuotedAttachment attachments = 4; + Type type = 5; +} + +message BodyRange { + enum Style { + NONE = 0; // Importers should ignore the body range without throwing an error. + BOLD = 1; + ITALIC = 2; + SPOILER = 3; + STRIKETHROUGH = 4; + MONOSPACE = 5; + } + + // 'start' and 'length' are measured in UTF-16 code units. + // They may refer to offsets in a longText attachment. + uint32 start = 1; + uint32 length = 2; + + // If unset, importers should ignore the body range without throwing an error. + oneof associatedValue { + bytes mentionAci = 3; + Style style = 4; + } +} + +message Reaction { + string emoji = 1; + uint64 authorId = 2; + uint64 sentTimestamp = 3; + // A higher sort order means that a reaction is more recent. Some clients may export this as + // incrementing numbers (e.g. 1, 2, 3), others as timestamps. + uint64 sortOrder = 4; +} + +message ChatUpdateMessage { + // If unset, importers should ignore the update message without throwing an error. + oneof update { + SimpleChatUpdate simpleUpdate = 1; + GroupChangeChatUpdate groupChange = 2; + ExpirationTimerChatUpdate expirationTimerChange = 3; + ProfileChangeChatUpdate profileChange = 4; + ThreadMergeChatUpdate threadMerge = 5; + SessionSwitchoverChatUpdate sessionSwitchover = 6; + IndividualCall individualCall = 7; + GroupCall groupCall = 8; + LearnedProfileChatUpdate learnedProfileChange = 9; + } +} + +message IndividualCall { + enum Type { + UNKNOWN_TYPE = 0; // Interpret as "Audio call" + AUDIO_CALL = 1; + VIDEO_CALL = 2; + } + + enum Direction { + UNKNOWN_DIRECTION = 0; // Interpret as "Incoming" + INCOMING = 1; + OUTGOING = 2; + } + + enum State { + UNKNOWN_STATE = 0; // Interpret as "Accepted" + ACCEPTED = 1; + NOT_ACCEPTED = 2; + // An incoming call that is no longer ongoing, which we neither accepted + // not actively declined. For example, it expired, was canceled by the + // sender, or was rejected due to being in another call. + MISSED = 3; + // We auto-declined an incoming call due to a notification profile. + MISSED_NOTIFICATION_PROFILE = 4; + } + + optional uint64 callId = 1; + Type type = 2; + Direction direction = 3; + State state = 4; + uint64 startedCallTimestamp = 5; + bool read = 6; +} + +message GroupCall { + enum State { + UNKNOWN_STATE = 0; // Interpret as "Generic" + // A group call was started without ringing. + GENERIC = 1; + // We joined a group call that was started without ringing. + JOINED = 2; + // An incoming group call is actively ringing. + RINGING = 3; + // We accepted an incoming group ring. + ACCEPTED = 4; + // We declined an incoming group ring. + DECLINED = 5; + // We missed an incoming group ring, for example because it expired. + MISSED = 6; + // We auto-declined an incoming group ring due to a notification profile. + MISSED_NOTIFICATION_PROFILE = 7; + // An outgoing ring was started. We don't track any state for outgoing rings + // beyond that they started. + OUTGOING_RING = 8; + } + + optional uint64 callId = 1; + State state = 2; + optional uint64 ringerRecipientId = 3; + optional uint64 startedCallRecipientId = 4; + uint64 startedCallTimestamp = 5; + optional uint64 endedCallTimestamp = 6; // The time the call ended. + bool read = 7; +} + +message SimpleChatUpdate { + enum Type { + UNKNOWN = 0; // Importers should skip the update without throwing an error. + JOINED_SIGNAL = 1; + IDENTITY_UPDATE = 2; + IDENTITY_VERIFIED = 3; + IDENTITY_DEFAULT = 4; // marking as unverified + CHANGE_NUMBER = 5; + RELEASE_CHANNEL_DONATION_REQUEST = 6; + END_SESSION = 7; + CHAT_SESSION_REFRESH = 8; + BAD_DECRYPT = 9; + PAYMENTS_ACTIVATED = 10; + PAYMENT_ACTIVATION_REQUEST = 11; + UNSUPPORTED_PROTOCOL_MESSAGE = 12; + REPORTED_SPAM = 13; + BLOCKED = 14; + UNBLOCKED = 15; + MESSAGE_REQUEST_ACCEPTED = 16; + } + + Type type = 1; +} + +// For 1:1 chat updates only. +// For group thread updates use GroupExpirationTimerUpdate. +message ExpirationTimerChatUpdate { + uint64 expiresInMs = 1; // 0 means the expiration timer was disabled +} + +message ProfileChangeChatUpdate { + string previousName = 1; + string newName = 2; +} + +message LearnedProfileChatUpdate { + // If unset, importers should consider the previous name to be an empty string. + oneof previousName { + uint64 e164 = 1; + string username = 2; + } +} + +message ThreadMergeChatUpdate { + uint64 previousE164 = 1; +} + +message SessionSwitchoverChatUpdate { + uint64 e164 = 1; +} + +message GroupChangeChatUpdate { + message Update { + // If unset, importers should consider it to be a GenericGroupUpdate with unset updaterAci + oneof update { + GenericGroupUpdate genericGroupUpdate = 1; + GroupCreationUpdate groupCreationUpdate = 2; + GroupNameUpdate groupNameUpdate = 3; + GroupAvatarUpdate groupAvatarUpdate = 4; + GroupDescriptionUpdate groupDescriptionUpdate = 5; + GroupMembershipAccessLevelChangeUpdate groupMembershipAccessLevelChangeUpdate = 6; + GroupAttributesAccessLevelChangeUpdate groupAttributesAccessLevelChangeUpdate = 7; + GroupAnnouncementOnlyChangeUpdate groupAnnouncementOnlyChangeUpdate = 8; + GroupAdminStatusUpdate groupAdminStatusUpdate = 9; + GroupMemberLeftUpdate groupMemberLeftUpdate = 10; + GroupMemberRemovedUpdate groupMemberRemovedUpdate = 11; + SelfInvitedToGroupUpdate selfInvitedToGroupUpdate = 12; + SelfInvitedOtherUserToGroupUpdate selfInvitedOtherUserToGroupUpdate = 13; + GroupUnknownInviteeUpdate groupUnknownInviteeUpdate = 14; + GroupInvitationAcceptedUpdate groupInvitationAcceptedUpdate = 15; + GroupInvitationDeclinedUpdate groupInvitationDeclinedUpdate = 16; + GroupMemberJoinedUpdate groupMemberJoinedUpdate = 17; + GroupMemberAddedUpdate groupMemberAddedUpdate = 18; + GroupSelfInvitationRevokedUpdate groupSelfInvitationRevokedUpdate = 19; + GroupInvitationRevokedUpdate groupInvitationRevokedUpdate = 20; + GroupJoinRequestUpdate groupJoinRequestUpdate = 21; + GroupJoinRequestApprovalUpdate groupJoinRequestApprovalUpdate = 22; + GroupJoinRequestCanceledUpdate groupJoinRequestCanceledUpdate = 23; + GroupInviteLinkResetUpdate groupInviteLinkResetUpdate = 24; + GroupInviteLinkEnabledUpdate groupInviteLinkEnabledUpdate = 25; + GroupInviteLinkAdminApprovalUpdate groupInviteLinkAdminApprovalUpdate = 26; + GroupInviteLinkDisabledUpdate groupInviteLinkDisabledUpdate = 27; + GroupMemberJoinedByLinkUpdate groupMemberJoinedByLinkUpdate = 28; + GroupV2MigrationUpdate groupV2MigrationUpdate = 29; + GroupV2MigrationSelfInvitedUpdate groupV2MigrationSelfInvitedUpdate = 30; + GroupV2MigrationInvitedMembersUpdate groupV2MigrationInvitedMembersUpdate = 31; + GroupV2MigrationDroppedMembersUpdate groupV2MigrationDroppedMembersUpdate = 32; + GroupSequenceOfRequestsAndCancelsUpdate groupSequenceOfRequestsAndCancelsUpdate = 33; + GroupExpirationTimerUpdate groupExpirationTimerUpdate = 34; + } + } + + // Must be one or more; all updates batched together came from + // a single batched group state update. + repeated Update updates = 1; +} + +message GenericGroupUpdate { + optional bytes updaterAci = 1; +} + +message GroupCreationUpdate { + optional bytes updaterAci = 1; +} + +message GroupNameUpdate { + optional bytes updaterAci = 1; + // Null value means the group name was removed. + optional string newGroupName = 2; +} + +message GroupAvatarUpdate { + optional bytes updaterAci = 1; + bool wasRemoved = 2; +} + +message GroupDescriptionUpdate { + optional bytes updaterAci = 1; + // Null value means the group description was removed. + optional string newDescription = 2; +} + +enum GroupV2AccessLevel { + UNKNOWN = 0; // Interpret as "Unsatisfiable" + ANY = 1; + MEMBER = 2; + ADMINISTRATOR = 3; + UNSATISFIABLE = 4; +} + +message GroupMembershipAccessLevelChangeUpdate { + optional bytes updaterAci = 1; + GroupV2AccessLevel accessLevel = 2; +} + +message GroupAttributesAccessLevelChangeUpdate { + optional bytes updaterAci = 1; + GroupV2AccessLevel accessLevel = 2; +} + +message GroupAnnouncementOnlyChangeUpdate { + optional bytes updaterAci = 1; + bool isAnnouncementOnly = 2; +} + +message GroupAdminStatusUpdate { + optional bytes updaterAci = 1; + // The aci who had admin status granted or revoked. + bytes memberAci = 2; + bool wasAdminStatusGranted = 3; +} + +message GroupMemberLeftUpdate { + bytes aci = 1; +} + +message GroupMemberRemovedUpdate { + optional bytes removerAci = 1; + bytes removedAci = 2; +} + +message SelfInvitedToGroupUpdate { + optional bytes inviterAci = 1; +} + +message SelfInvitedOtherUserToGroupUpdate { + // If no invitee id available, use GroupUnknownInviteeUpdate + bytes inviteeServiceId = 1; +} + +message GroupUnknownInviteeUpdate { + // Can be the self user. + optional bytes inviterAci = 1; + uint32 inviteeCount = 2; +} + +message GroupInvitationAcceptedUpdate { + optional bytes inviterAci = 1; + bytes newMemberAci = 2; +} + +message GroupInvitationDeclinedUpdate { + optional bytes inviterAci = 1; + // Note: if invited by pni, just set inviteeAci to nil. + optional bytes inviteeAci = 2; +} + +message GroupMemberJoinedUpdate { + bytes newMemberAci = 1; +} + +message GroupMemberAddedUpdate { + optional bytes updaterAci = 1; + bytes newMemberAci = 2; + bool hadOpenInvitation = 3; + // If hadOpenInvitation is true, optionally include aci of the inviter. + optional bytes inviterAci = 4; +} + +// An invitation to self was revoked. +message GroupSelfInvitationRevokedUpdate { + optional bytes revokerAci = 1; +} + +// These invitees should never be the local user. +// Use GroupSelfInvitationRevokedUpdate in those cases. +// The inviter or updater can be the local user. +message GroupInvitationRevokedUpdate { + message Invitee { + optional bytes inviterAci = 1; + // Prefer to use aci over pni. No need to set + // pni if aci is set. Both can be missing. + optional bytes inviteeAci = 2; + optional bytes inviteePni = 3; + } + + // The member that revoked the invite(s), not the inviter! + // Assumed to be an admin (at the time, may no longer be an + // admin or even a member). + optional bytes updaterAci = 1; + repeated Invitee invitees = 2; +} + +message GroupJoinRequestUpdate { + bytes requestorAci = 1; +} + +message GroupJoinRequestApprovalUpdate { + bytes requestorAci = 1; + // The aci that approved or rejected the request. + optional bytes updaterAci = 2; + bool wasApproved = 3; +} + +message GroupJoinRequestCanceledUpdate { + bytes requestorAci = 1; +} + +// A single requestor has requested to join and cancelled +// their request repeatedly with no other updates in between. +// The last action encompassed by this update is always a +// cancellation; if there was another open request immediately +// after, it will be a separate GroupJoinRequestUpdate, either +// in the same frame or in a subsequent frame. +message GroupSequenceOfRequestsAndCancelsUpdate { + bytes requestorAci = 1; + uint32 count = 2; +} + +message GroupInviteLinkResetUpdate { + optional bytes updaterAci = 1; +} + +message GroupInviteLinkEnabledUpdate { + optional bytes updaterAci = 1; + bool linkRequiresAdminApproval = 2; +} + +message GroupInviteLinkAdminApprovalUpdate { + optional bytes updaterAci = 1; + bool linkRequiresAdminApproval = 2; +} + +message GroupInviteLinkDisabledUpdate { + optional bytes updaterAci = 1; +} + +message GroupMemberJoinedByLinkUpdate { + bytes newMemberAci = 1; +} + +// A gv1->gv2 migration occurred. +message GroupV2MigrationUpdate {} + +// Another user migrated gv1->gv2 but was unable to add +// the local user and invited them instead. +message GroupV2MigrationSelfInvitedUpdate {} + +// The local user migrated gv1->gv2 but was unable to +// add some members and invited them instead. +// (Happens if we don't have the invitee's profile key) +message GroupV2MigrationInvitedMembersUpdate { + uint32 invitedMembersCount = 1; +} + +// The local user migrated gv1->gv2 but was unable to +// add or invite some members and dropped them instead. +// (Happens for e164 members where we don't have an aci). +message GroupV2MigrationDroppedMembersUpdate { + uint32 droppedMembersCount = 1; +} + +// For 1:1 timer updates, use ExpirationTimerChatUpdate. +message GroupExpirationTimerUpdate { + uint64 expiresInMs = 1; // 0 means the expiration timer was disabled + optional bytes updaterAci = 2; +} + +message StickerPack { + bytes packId = 1; + bytes packKey = 2; +} + +message ChatStyle { + message Gradient { + uint32 angle = 1; // degrees + repeated fixed32 colors = 2; // 0xAARRGGBB + repeated float positions = 3; // percent from 0 to 1 + } + + message CustomChatColor { + uint64 id = 1; + + // If unset, use the default chat color + oneof color { + fixed32 solid = 2; // 0xAARRGGBB + Gradient gradient = 3; + } + } + + message AutomaticBubbleColor { + } + + enum WallpaperPreset { + UNKNOWN_WALLPAPER_PRESET = 0; // Interpret as the wallpaper being unset + SOLID_BLUSH = 1; + SOLID_COPPER = 2; + SOLID_DUST = 3; + SOLID_CELADON = 4; + SOLID_RAINFOREST = 5; + SOLID_PACIFIC = 6; + SOLID_FROST = 7; + SOLID_NAVY = 8; + SOLID_LILAC = 9; + SOLID_PINK = 10; + SOLID_EGGPLANT = 11; + SOLID_SILVER = 12; + GRADIENT_SUNSET = 13; + GRADIENT_NOIR = 14; + GRADIENT_HEATMAP = 15; + GRADIENT_AQUA = 16; + GRADIENT_IRIDESCENT = 17; + GRADIENT_MONSTERA = 18; + GRADIENT_BLISS = 19; + GRADIENT_SKY = 20; + GRADIENT_PEACH = 21; + } + + enum BubbleColorPreset { + UNKNOWN_BUBBLE_COLOR_PRESET = 0; // Interpret as the user's default chat bubble color + SOLID_ULTRAMARINE = 1; + SOLID_CRIMSON = 2; + SOLID_VERMILION = 3; + SOLID_BURLAP = 4; + SOLID_FOREST = 5; + SOLID_WINTERGREEN = 6; + SOLID_TEAL = 7; + SOLID_BLUE = 8; + SOLID_INDIGO = 9; + SOLID_VIOLET = 10; + SOLID_PLUM = 11; + SOLID_TAUPE = 12; + SOLID_STEEL = 13; + GRADIENT_EMBER = 14; + GRADIENT_MIDNIGHT = 15; + GRADIENT_INFRARED = 16; + GRADIENT_LAGOON = 17; + GRADIENT_FLUORESCENT = 18; + GRADIENT_BASIL = 19; + GRADIENT_SUBLIME = 20; + GRADIENT_SEA = 21; + GRADIENT_TANGERINE = 22; + } + + // If unset, importers should consider there to be no wallpaper. + oneof wallpaper { + WallpaperPreset wallpaperPreset = 1; + // This `FilePointer` is expected not to contain a `fileName`, `width`, + // `height`, or `caption`. + FilePointer wallpaperPhoto = 2; + } + + // If unset, importers should consider it to be AutomaticBubbleColor + oneof bubbleColor { + // Bubble setting is automatically determined based on the wallpaper setting, + // or `SOLID_ULTRAMARINE` for `noWallpaper` + AutomaticBubbleColor autoBubbleColor = 3; + BubbleColorPreset bubbleColorPreset = 4; + + // See AccountSettings.customChatColors + uint64 customColorId = 5; + } + + bool dimWallpaperInDarkMode = 7; +} + +message NotificationProfile { + enum DayOfWeek { + UNKNOWN = 0; // Interpret as "Monday" + MONDAY = 1; + TUESDAY = 2; + WEDNESDAY = 3; + THURSDAY = 4; + FRIDAY = 5; + SATURDAY = 6; + SUNDAY = 7; + } + + string name = 1; + optional string emoji = 2; + fixed32 color = 3; // 0xAARRGGBB + uint64 createdAtMs = 4; + bool allowAllCalls = 5; + bool allowAllMentions = 6; + repeated uint64 allowedMembers = 7; // generated recipient id for allowed groups and contacts + bool scheduleEnabled = 8; + uint32 scheduleStartTime = 9; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) + uint32 scheduleEndTime = 10; // 24-hour clock int, 0000-2359 (e.g., 15, 900, 1130, 2345) + repeated DayOfWeek scheduleDaysEnabled = 11; +} + +message ChatFolder { + // Represents the default "All chats" folder record vs all other custom folders + enum FolderType { + UNKNOWN = 0; // Interpret as "Custom" + ALL = 1; + CUSTOM = 2; + } + + string name = 1; + bool showOnlyUnread = 2; + bool showMutedChats = 3; + // Folder includes all 1:1 chats, unless excluded + bool includeAllIndividualChats = 4; + // Folder includes all group chats, unless excluded + bool includeAllGroupChats = 5; + FolderType folderType = 6; + repeated uint64 includedRecipientIds = 7; // generated recipient id of groups, contacts, and/or note to self + repeated uint64 excludedRecipientIds = 8; // generated recipient id of groups, contacts, and/or note to self +} diff --git a/main.go b/main.go index e4c510f..e0702f9 100644 --- a/main.go +++ b/main.go @@ -66,6 +66,10 @@ func main() { protoReformat(argv.Proto) okExit("") } + if argv.Comments { + protoReformatComments(argv.Proto) + okExit("") + } if argv.Regret { // this will override the manditory Uuid checks @@ -86,6 +90,7 @@ func main() { log.Info("autogenpb parse error:", err) badExit(err) } + if !argv.NoFormat { protoReformat(argv.Proto) } diff --git a/protoReformat.go b/protoReformat.go index 07baf76..5cd685d 100644 --- a/protoReformat.go +++ b/protoReformat.go @@ -40,12 +40,31 @@ func (msg *StdMessage) name() string { } type Message interface { - format() []string name() string load() addMsg(Message) } +func protoReformatComments(filename string) error { + // read in the .proto file + data, err := os.ReadFile(filename) + if err != nil { + log.Info("file read failed", filename, err) + return err + } + + var newfile string + + log.Info("filename", filename) + alltest := makeLineIter(data) + // gets the max vartype and varname + for line := range alltest { + newfile += fmt.Sprintln(commentPreprocessor(line)) + } + saveFile(filename, newfile) + return nil +} + func protoReformat(filename string) error { // read in the .proto file data, err := os.ReadFile(filename) @@ -56,17 +75,6 @@ func protoReformat(filename string) error { var newfile string - /* check the comment preprocessor - log.Info("filename", filename) - alltest := makeLineIter(data) - // gets the max vartype and varname - for line := range alltest { - newfile += fmt.Sprintln(commentPreprocessor(line)) - } - saveFile(filename, newfile) - os.Exit(-1) - */ - var fmtmsg *FormatMsg fmtmsg = new(FormatMsg) @@ -109,11 +117,12 @@ func protoReformat(filename string) error { // write out the messages allTheLines = newLinesScanner(strings.Split(string(data), "\n")) for allTheLines.Scan() { - line := allTheLines.Next() + line := allTheLines.NextRaw() + if strings.HasPrefix(line, "oneof ") { newmsg := fmtmsg.newOneofMessage(line) newmsg.load() - for _, newline := range newmsg.format() { + for _, newline := range newmsg.msgPB.format() { newfile += fmt.Sprintln(newline) } continue @@ -123,7 +132,7 @@ func protoReformat(filename string) error { newmsg := fmtmsg.newEnumMessage(line) newmsg.load() // loadEnumDefinition(newmsg) - for _, newline := range newmsg.format() { + for _, newline := range newmsg.msgPB.format() { newfile += fmt.Sprintln(newline) } continue @@ -133,7 +142,7 @@ func protoReformat(filename string) error { newmsg := fmtmsg.newStdMessage(line) newmsg.load() log.Info("got to message", line) - for _, newline := range newmsg.format() { + for _, newline := range newmsg.msgPB.format() { newfile += fmt.Sprintln(newline) } continue @@ -215,13 +224,13 @@ func (msg *StdMessage) load() { if strings.HasPrefix(line, "oneof ") { newmsg := msg.msgPB.newOneofMessage(line) newmsg.load() - curPB = newmsg.msgPB + // curPB = newmsg.msgPB continue } if strings.HasPrefix(line, "enum ") { newmsg := msg.msgPB.newEnumMessage(line) newmsg.load() - curPB = newmsg.msgPB + // curPB = newmsg.msgPB // loadEnumDefinition(newmsg) continue } @@ -229,14 +238,14 @@ func (msg *StdMessage) load() { // message inception. search for the architect. don't forget your totem newmsg := msg.msgPB.newStdMessage(line) newmsg.load() - curPB = newmsg.msgPB + // curPB = newmsg.msgPB continue } if strings.HasPrefix(line, "}") { msg.msgPB.Footer = line return } - curPB.Notes = append(curPB.Notes, line) + curPB.Lines = append(curPB.Lines, line) // fmtmsg.Lines = append(fmtmsg.Lines, line) } @@ -319,36 +328,54 @@ func setMaxSizes(curmsg *FormatMsg) { } } -func (curmsg *FormatMsg) format() []string { - return formatEnum(curmsg) +// use this for header and footer lines +func (msg *FormatMsg) padBase() string { + var pad string + for i := 1; i < int(msg.Depth); i += 1 { + pad += fmt.Sprintf("%8s", " ") + } + return pad } -func (curmsg *EnumMessage) format() []string { - return formatEnum(curmsg.msgPB) +// use this for lines inside the message +func (msg *FormatMsg) pad() string { + var pad string + for i := 0; i < int(msg.Depth); i += 1 { + pad += fmt.Sprintf("%8s", " ") + } + return pad } func formatEnum(curmsg *FormatMsg) []string { var newmsg []string - newmsg = append(newmsg, curmsg.Header) // +" //header") + header := fmt.Sprintf("%s%s // enum depth=%d", curmsg.padBase(), curmsg.Header, curmsg.Depth) + newmsg = append(newmsg, header) for _, line := range curmsg.Lines { - line = " " + strings.TrimSpace(line) + line = fmt.Sprintf("%s%s", curmsg.pad(), line) newmsg = append(newmsg, line) } - newmsg = append(newmsg, curmsg.Footer) // +" //footer") + footer := fmt.Sprintf("%s%s // enum footer depth=%d", curmsg.padBase(), curmsg.Footer, curmsg.Depth) + newmsg = append(newmsg, footer) return newmsg } -func (curmsg *StdMessage) format() []string { - return formatMessage(curmsg.msgPB) +func (msg *FormatMsg) format() []string { + switch msg.Type { + case FormatMsg_ENUM: + return formatEnum(msg) + case FormatMsg_MESSAGE: + return formatMessage(msg) + } + return formatMessage(msg) } func formatMessage(curmsg *FormatMsg) []string { var newmsg []string if curmsg.Header != "" { - line := curmsg.Header + line := fmt.Sprintf("%s%s // msg depth=%d", curmsg.padBase(), curmsg.Header, curmsg.Depth) parts := strings.Fields(line) if len(parts) > 3 { // hack to actually indent comments on the message line itself. you're welcome @@ -356,51 +383,34 @@ func formatMessage(curmsg *FormatMsg) []string { end := strings.Join(parts[3:], " ") offset := int(curmsg.MaxVarname) + int(curmsg.MaxVartype) + 16 - len(start) pad := fmt.Sprintf("%d", offset) - hmm := "%s %" + pad + "s %s" - line = fmt.Sprintf(hmm, start, " ", end) + hmm := "%s %" + pad + "s %s // depth=%d" + line = fmt.Sprintf(hmm, start, " ", end, curmsg.Depth) + } else { + line = fmt.Sprintf("%s // len(parts)=%d depth=%d", line, len(parts), curmsg.Depth) } - newmsg = append(newmsg, line) // +" //header") + newmsg = append(newmsg, line) // " //header") + } else { + newmsg = append(newmsg, "// ERROR: header was blank") // +" //header") } // find the max length of varname and vartype setMaxSizes(curmsg) - /* - for _, line := range curmsg.Lines { - parts := strings.Split(line, ";") - if len(parts) < 2 { - // line is blank or just a comment - continue - } - vartype, varname, _, _ := tokenMsgVar(line) - if len(vartype) > int(curmsg.MaxVartype) { - curmsg.MaxVartype = int64(len(vartype)) + for _, msg := range curmsg.Msgs { + switch msg.Type { + case FormatMsg_ENUM: + for _, line := range formatEnum(msg) { + line = fmt.Sprintf("%s%s", curmsg.pad(), line) + newmsg = append(newmsg, line) } - if len(varname) > int(curmsg.MaxVarname) { - curmsg.MaxVarname = int64(len(varname)) + case FormatMsg_MESSAGE: + for _, line := range formatMessage(msg) { + line = fmt.Sprintf("%s%s", curmsg.pad(), line) + newmsg = append(newmsg, line) } + default: } - */ - - /* - for _, msg := range curmsg.Enums { - for _, newline := range formatEnum(msg) { - newmsg = append(newmsg, newline) - } - } - - for _, msg := range curmsg.Oneofs { - for _, newline := range formatEnum(msg) { - newmsg = append(newmsg, newline) - } - } - - for _, msg := range curmsg.Msgs { - for _, newline := range msg.format() { - newmsg = append(newmsg, newline) - } - } - */ + } for _, line := range curmsg.Lines { line = strings.TrimSpace(line) @@ -455,7 +465,15 @@ func (it *LinesScanner) Scan() bool { return true } -// Next() returns the next thing in the array +// does no cleaning of the data +func (it *LinesScanner) NextRaw() string { + if it.index-1 == len(it.things) { + fmt.Println("Next() error in LinesScanner", it.index) + } + return it.things[it.index-1] +} + +// cleans out comments func (it *LinesScanner) Next() string { if it.index-1 == len(it.things) { fmt.Println("Next() error in LinesScanner", it.index) @@ -463,8 +481,8 @@ func (it *LinesScanner) Next() string { // out := commentPreprocessor(it.things[it.index-1]) out := it.things[it.index-1] out = commentPreprocessor(out) - // return strings.TrimSpace(out) - return out + return strings.TrimSpace(out) + // return out } // END DEFINE THE ITERATOR @@ -481,11 +499,13 @@ func commentPreprocessor(line string) string { var comments []string for _, match := range matches { comments = append(comments, strings.TrimSpace(match[1])) + // comments = append(comments, match[1]) } // Remove the block comments from the original line line = re.ReplaceAllString(line, "") - line = strings.TrimSpace(line) + // line = strings.TrimSpace(line) + line = strings.TrimSuffix(line, " ") // Append comments at the end with // for _, comment := range comments {