Files
amsterdam/exports/vcif_xml.go
T
2026-02-26 18:19:40 -07:00

234 lines
7.6 KiB
Go

/*
* 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 <vcif> 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 <topic> 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 <post> 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,omitempty"` // is post scribbled?
Pseud string `xml:"pseud"` // post pseud
Text string `xml:"text"` // post text
Attachment *VCIFPostAttachment `xml:"attachment,omitempty"` // 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 <scribbled> 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 <attachment> 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<vcif>\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("</vcif>\r\n"))
return err
}