/* * Amsterdam Web Communities System * Copyright (c) 2025-2026 Erbosoft Metaverse Design Solutions, All Rights Reserved * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ // Package exports contains interfacing code for external data formats. package exports import ( "context" "encoding/base64" "encoding/xml" "fmt" "io" "strings" "git.erbosoft.com/amy/amsterdam/database" ) /* * This file defines structures and code for working with Venice Conference Interchange Format (VCIF) files. * Amsterdam uses this name for the format for backwards compatibility. */ // ISO8601 is the full ISO 8601 formatting string. const ISO8601 = "20060102T150405" // ISO8601_DATE is the ISO 8601 date-only formatting string. const ISO8601_DATE = "20060102" // VCIFBase is the top-level element for a VCIF file. type VCIFBase struct { XMLName xml.Name `xml:"vcif"` // I am the element Topics []VCIFTopic `xml:"topic"` // list of topics } // VCIFTopic is the VCIF element representing a topic. type VCIFTopic struct { XMLName xml.Name `xml:"topic"` // I am the element Index int `xml:"index,attr"` // topic index (number, not TopicId) Frozen bool `xml:"frozen,attr"` // is topic frozen? Archived bool `xml:"archived,attr"` // is topic archived? Name string `xml:"topicname"` // topic name Posts []VCIFPost `xml:"post"` // posts in the topic } // VCIFPost is the VCIF element representing a post in a topic. type VCIFPost struct { XMLName xml.Name `xml:"post"` // I am the element ID int64 `xml:"id,attr"` // post ID (PostId) Parent int64 `xml:"parent,attr"` // parent PostId (usually 0) Index int `xml:"index,attr"` // post index (number) Lines int `xml:"lines,attr"` // line count Author string `xml:"author,attr"` // author username DateISO8601 string `xml:"date,attr"` // post date, ISO 8601 format Hidden bool `xml:"hidden,attr"` // is post hidden? Scribbled *VCIFScribble `xml:"scribbled"` // is post scribbled? Pseud string `xml:"pseud"` // post pseud Text string `xml:"text"` // post text Attachment *VCIFPostAttachment `xml:"attachment"` // attachment data } // VCIFScribble is the VCIF element representing that a post has been scribbled. type VCIFScribble struct { XMLName xml.Name `xml:"scribbled"` // I am the element ByUser string `xml:"by,attr"` // who scribbled it? OnDateISO8601 string `xml:"date,attr"` // scribble date, ISO 8601 format } // VCIFPostAttachment is the VCIF element representing an attachment to a post. type VCIFPostAttachment struct { XMLName xml.Name `xml:"attachment"` // I am the element Length int `xml:"length,attr"` // length in bytes MIMEType string `xml:"type,attr"` // MIME datatype Filename string `xml:"filename,attr"` // attachment filename Base64Data string `xml:",chardata"` // attachment data, Base 64 encoded } /* VCIFFromPost fills in a VCIF post structure with data from a conference post. * Parameters: * ctx - Standard Go context value. * target - Pointer to the target VCIF post structure. * post - The post to fill the target from. * bugWorkaround - Work around bug in extracting compressed attachment data. * Returns: * Standard Go error status. */ func VCIFFromPost(ctx context.Context, target *VCIFPost, post *database.PostHeader, bugWorkaround bool) error { // Fill in the posting user. user, err := post.Creator(ctx) if err != nil { return err } target.Author = user.Username // Fill in the post text. target.Text, err = post.Text(ctx) if err == database.ErrNoPostData { target.Text = "" } else if err != nil { return err } // Fill in the scribble data. if post.IsScribbled() { scribbler, err := database.AmGetUser(ctx, *post.ScribbleUid) if err != nil { return err } scribbleData := VCIFScribble{ ByUser: scribbler.Username, OnDateISO8601: post.ScribbleDate.Format(ISO8601), } target.Scribbled = &scribbleData } else { target.Scribbled = nil } // Fill in the attachment data. target.Attachment = nil if !post.IsScribbled() { ainfo, err := post.AttachmentInfo(ctx) if err != nil { return err } if ainfo != nil { newAttachment := VCIFPostAttachment{ Length: int(ainfo.Length), MIMEType: ainfo.MIMEType, Filename: ainfo.Filename, } data, err := post.AttachmentData(ctx, bugWorkaround) if err != nil { return err } newAttachment.Base64Data = base64.StdEncoding.EncodeToString(data) target.Attachment = &newAttachment } } // Fill in the rest of the data that can't fail. target.ID = post.PostId target.Parent = post.Parent target.Index = int(post.Num) if post.LineCount != nil { target.Lines = int(*post.LineCount) } else { target.Lines = 0 } target.DateISO8601 = post.Posted.Format(ISO8601) target.Hidden = post.Hidden if post.Pseud != nil { target.Pseud = *post.Pseud } else { target.Pseud = "" } return nil } /* VCIFFromTopic fills in a VCIF topic structure with cata from a conference topic. * Parameterrs: * ctx - Standard Go context value. * target - Pointer to the target VCIF topic value. * topic - The topic to fill the target from. * bugWorkaround - Work around bug in extracting compressed attachment data. * Returns: * Standard Go error status. */ func VCIFFromTopic(ctx context.Context, target *VCIFTopic, topic *database.Topic, bugWorkaround bool) error { // Get all posts in the topic. posts, err := database.AmGetPostRange(ctx, topic, 0, topic.TopMessage) if err != nil { return err } // Build the posts array. myPostArray := make([]VCIFPost, len(posts)) for i, p := range posts { err = VCIFFromPost(ctx, &(myPostArray[i]), p, bugWorkaround) if err != nil { return fmt.Errorf("error converting post %d: %v", p.Num, err) } } target.Posts = myPostArray // Fill in the rest of the data that can't fail. target.Index = int(topic.Number) target.Frozen = topic.Frozen target.Archived = topic.Archived target.Name = topic.Name return nil } /* VCIFStreamTopicFile takes a list of topics and writes their VCIF XML encoding to the specified writer. * Parameters: * ctx - Standard Go context value. * w - Writer to receive the XML data. * topics - Array of topics to be written. * bugWorkaround - Work around bug in extracting compressed attachment data. * Returns: * Standard Go error status. */ func VCIFStreamTopicFile(ctx context.Context, w io.Writer, topics []*database.Topic, bugWorkaround bool) error { // Write the header of the file. var b strings.Builder b.WriteString(xml.Header) b.WriteString("\r\n\r\n") _, err := w.Write([]byte(b.String())) if err != nil { return err } // Create the XML encoder. enc := xml.NewEncoder(w) enc.Indent(" ", " ") // Encode each topic in turn and write it to the writer. for _, t := range topics { var encodedTopic VCIFTopic err = VCIFFromTopic(ctx, &encodedTopic, t, bugWorkaround) if err != nil { return fmt.Errorf("error converting topic %d: %v", t.Number, err) } enc.Encode(encodedTopic) } // Write the trailing tag. _, err = w.Write([]byte("\r\n")) return err }