593 lines
18 KiB
Go
593 lines
18 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/.
|
|
*/
|
|
// The database package contains database management and storage logic.
|
|
package database
|
|
|
|
import (
|
|
"bytes"
|
|
"compress/gzip"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.erbosoft.com/amy/amsterdam/config"
|
|
)
|
|
|
|
// PostHeader represents the "header" of a post, everything except for its text and attachment.
|
|
type PostHeader struct {
|
|
PostId int64 `db:"postid"` // ID of the post
|
|
Parent int64 `db:"parent"` // ID of parent (unused?)
|
|
TopicId int32 `db:"topicid"` // topic containing the post
|
|
Num int32 `db:"num"` // post number
|
|
LineCount *int32 `db:"linecount"` // number of lines
|
|
CreatorUid int32 `db:"creator_uid"` // UID creating post
|
|
Posted time.Time `db:"posted"` // date posted
|
|
Hidden bool `db:"hidden"` // is post hidden?
|
|
ScribbleUid *int32 `db:"scribble_uid"` // UID of who scribbled it
|
|
ScribbleDate *time.Time `db:"scribble_date"` // when was it scribbled?
|
|
Pseud *string `db:"pseud"` // post's "pseud" (name/header)
|
|
}
|
|
|
|
type PostData struct {
|
|
PostId int64 `db:"postid"` // ID of the post
|
|
Data *string `db:"data"` // actual post data
|
|
}
|
|
|
|
// PostAttachInfo contains information about a file attachment to a post.
|
|
type PostAttachInfo struct {
|
|
Filename string // name of attached file
|
|
MIMEType string // MIME type of attached file
|
|
Length int32 // length in bytes of attached file
|
|
}
|
|
|
|
const (
|
|
stgMethodPlain = 0 // attachment stored as raw data
|
|
stgMethodGZIP = 1 // attachment stored as GZIP data
|
|
)
|
|
|
|
// ErrNoPostData is returned if post data is missing.
|
|
var ErrNoPostData = errors.New("no post data")
|
|
|
|
// IsScribbled returns true if the post has been scribbled, false if not.
|
|
func (p *PostHeader) IsScribbled() bool {
|
|
return p.ScribbleUid != nil && p.ScribbleDate != nil
|
|
}
|
|
|
|
// IsPublished returns true if the post has been published to the front page.
|
|
func (p *PostHeader) IsPublished(ctx context.Context) (bool, error) {
|
|
rs, err := amdb.QueryContext(ctx, "SELECT COUNT(*) FROM postpublish WHERE postid = ?", p.PostId)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !rs.Next() {
|
|
return false, errors.New("internal failure in IsPublished")
|
|
}
|
|
ct := 0
|
|
err = rs.Scan(&ct)
|
|
return ct > 0, err
|
|
}
|
|
|
|
/* AttachmentInfo returns attachment information for a post.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* Returns:
|
|
* Pointer to structure with post attachment info.
|
|
* Standard Go error status.
|
|
*/
|
|
func (p *PostHeader) AttachmentInfo(ctx context.Context) (*PostAttachInfo, error) {
|
|
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
|
return nil, errors.New("no attachment data for scribbled post")
|
|
}
|
|
rs, err := amdb.QueryContext(ctx, "SELECT filename, mimetype, datalen FROM postattach WHERE postid = ?", p.PostId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !rs.Next() {
|
|
return nil, nil
|
|
}
|
|
var rc PostAttachInfo
|
|
err = rs.Scan(&(rc.Filename), &(rc.MIMEType), &(rc.Length))
|
|
return &rc, err
|
|
}
|
|
|
|
/* AttachmentData returns attachment data for a post.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* Returns:
|
|
* Attachment data as a byte array.
|
|
* Standard Go error status.
|
|
*/
|
|
func (p *PostHeader) AttachmentData(ctx context.Context) ([]byte, error) {
|
|
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
|
return nil, errors.New("no attachment data for scribbled post")
|
|
}
|
|
rs, err := amdb.QueryContext(ctx, "SELECT datalen, stgmethod, data FROM postattach WHERE postid = ?", p.PostId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !rs.Next() {
|
|
return nil, nil
|
|
}
|
|
var datalen int32
|
|
var stgmethod int16
|
|
var dbdata []byte
|
|
err = rs.Scan(&datalen, &stgmethod, &dbdata)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if stgmethod == stgMethodPlain {
|
|
return dbdata, nil
|
|
}
|
|
r, err := gzip.NewReader(bytes.NewReader(dbdata))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
outdata := make([]byte, 0, datalen)
|
|
n, err := r.Read(outdata)
|
|
r.Close()
|
|
if err != nil || n < int(datalen) {
|
|
if err == nil {
|
|
err = errors.New("unable to read entire attachment")
|
|
}
|
|
return nil, err
|
|
}
|
|
return outdata, nil
|
|
}
|
|
|
|
/* SetAttachment sets the attachment data for a post.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* u - user attempting to upload attachment data
|
|
* fileName - Name of the original attachment file.
|
|
* mimeType - MIME type of the attachment data.
|
|
* length - Length of the attachment data in bytes.
|
|
* data - The attachment data itself.
|
|
* Returns:
|
|
* Standard Go error status.
|
|
*/
|
|
func (p *PostHeader) SetAttachment(ctx context.Context, u *User, fileName string, mimeType string, length int32, data []byte, ipaddr string) error {
|
|
var ar *AuditRecord = nil
|
|
defer func() {
|
|
AmStoreAudit(ar)
|
|
}()
|
|
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
|
return errors.New("cannot attach to scribbled post")
|
|
}
|
|
if u.Uid != p.CreatorUid {
|
|
return errors.New("cannot attach to a post that is not yours")
|
|
}
|
|
ai, err := p.AttachmentInfo(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if ai != nil {
|
|
return errors.New("attachment already present for this post")
|
|
}
|
|
if length > config.GlobalComputedConfig.UploadMaxSize {
|
|
return fmt.Errorf("file too large to be attached; maximum size is %s", config.GlobalConfig.Posting.Uploads.MaxSize)
|
|
}
|
|
|
|
// Compress the data with GZIP if we need to.
|
|
var stgmethod int16
|
|
var realData []byte
|
|
if _, ok := config.GlobalComputedConfig.UploadNoCompress[mimeType]; ok {
|
|
realData = data
|
|
stgmethod = stgMethodPlain
|
|
} else {
|
|
buf := new(bytes.Buffer)
|
|
w := gzip.NewWriter(buf)
|
|
_, err := w.Write(data)
|
|
if err == nil {
|
|
err = w.Close()
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
realData = buf.Bytes()
|
|
stgmethod = stgMethodGZIP
|
|
}
|
|
|
|
// Write to the database.
|
|
_, err = amdb.ExecContext(ctx, "INSERT INTO postattach (postid, datalen, filename, mimetype, stgmethod, data) VALUES (?, ?, ?, ?, ?, ?)",
|
|
p.PostId, length, fileName, mimeType, stgmethod, realData)
|
|
// Generate an audit record.
|
|
ar = AmNewAudit(AuditConferenceUploadAttachment, u.Uid, ipaddr, fmt.Sprintf("post=%d", p.PostId),
|
|
fmt.Sprintf("len=%d,type=%s,name=%s,method=%d", length, mimeType, fileName, stgmethod))
|
|
return err
|
|
}
|
|
|
|
// HitAttachment records a "hit" on an attachment.
|
|
func (p *PostHeader) HitAttachment(ctx context.Context) error {
|
|
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
|
return errors.New("no attachment on scribbled post")
|
|
}
|
|
_, err := amdb.ExecContext(ctx, "UPDATE postattach SET hits = hits + 1, lasthit = NOW() WHERE postid = ?", p.PostId)
|
|
return err
|
|
}
|
|
|
|
// Text returns the text associated with a post.
|
|
func (p *PostHeader) Text(ctx context.Context) (string, error) {
|
|
var dbdata []PostData
|
|
if err := amdb.SelectContext(ctx, &dbdata, "SELECT * FROM postdata WHERE postid = ?", p.PostId); err != nil {
|
|
return "", err
|
|
}
|
|
if len(dbdata) > 1 {
|
|
return "", fmt.Errorf("too many data records (%d) for post #%d", len(dbdata), p.PostId)
|
|
}
|
|
if len(dbdata) == 0 || dbdata[0].Data == nil {
|
|
return "", ErrNoPostData
|
|
}
|
|
return *dbdata[0].Data, nil
|
|
}
|
|
|
|
// SetHidden sets the "hidden" flag on a post.
|
|
func (p *PostHeader) SetHidden(ctx context.Context, u *User, flag bool, ipaddr string) error {
|
|
var ar *AuditRecord = nil
|
|
defer func() {
|
|
AmStoreAudit(ar)
|
|
}()
|
|
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
|
return errors.New("cannot hide or unhide scribbled post")
|
|
}
|
|
if p.Hidden == flag {
|
|
return nil // no-op
|
|
}
|
|
_, err := amdb.ExecContext(ctx, "UPDATE posts SET hidden = ? WHERE postid = ?", flag, p.PostId)
|
|
if err == nil {
|
|
p.Hidden = flag
|
|
ar = AmNewAudit(AuditConferenceHideMessage, u.Uid, ipaddr, fmt.Sprintf("post=%d", p.PostId), fmt.Sprintf("hidden=%t", flag))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Scribble causes a post to be scribbled.
|
|
func (p *PostHeader) Scribble(ctx context.Context, u *User, ipaddr string) error {
|
|
var ar *AuditRecord = nil
|
|
defer func() {
|
|
AmStoreAudit(ar)
|
|
}()
|
|
if p.ScribbleDate != nil && p.ScribbleUid != nil {
|
|
return errors.New("cannot scribble an already-scribbled post")
|
|
}
|
|
|
|
success := false
|
|
tx := amdb.MustBegin()
|
|
defer func() {
|
|
if !success {
|
|
tx.Rollback()
|
|
}
|
|
}()
|
|
unlock := true
|
|
tx.ExecContext(ctx, "LOCK TABLES posts WRITE, postdata WRITE, postattach WRITE, postpublish WRITE;")
|
|
defer func() {
|
|
if unlock {
|
|
tx.ExecContext(ctx, "UNLOCK TABLES;")
|
|
}
|
|
}()
|
|
|
|
// Scribble on the post header.
|
|
scribblePseud := "<EM><B>(Scribbled)</B></EM>" // FUTURE: configurable option
|
|
_, err := tx.ExecContext(ctx, "UPDATE posts SET linecount = 0, hidden = 0, scribble_uid = ?, scribble_date = NOW(), pseud = ? WHERE postid = ?", u.Uid, scribblePseud, p.PostId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Reread the scribble date.
|
|
rs, err := tx.QueryContext(ctx, "SELECT scribble_date FROM posts WHERE postid = ?", p.PostId)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !rs.Next() {
|
|
return errors.New("internal error while scribbling")
|
|
}
|
|
var newScribbleDate time.Time
|
|
if err = rs.Scan(&newScribbleDate); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Delete all auxiliary data.
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM postdata WHERE postid = ?", p.PostId)
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM postattach WHERE postid = ?", p.PostId)
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM postpublish WHERE postid = ?", p.PostId)
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Unlock tables and commit.
|
|
tx.ExecContext(ctx, "UNLOCK TABLES;")
|
|
unlock = false
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
success = true
|
|
|
|
// Patch fields in the post header
|
|
var newLines int32 = 0
|
|
p.LineCount = &newLines
|
|
p.Hidden = false
|
|
newUid := u.Uid
|
|
p.ScribbleUid = &newUid
|
|
p.ScribbleDate = &newScribbleDate
|
|
p.Pseud = &scribblePseud
|
|
|
|
// Audit the operation.
|
|
ar = AmNewAudit(AuditConferenceScribbleMessage, u.Uid, ipaddr, fmt.Sprintf("post=%d", p.PostId))
|
|
return nil
|
|
}
|
|
|
|
// Nuke causes a post to be nuked (deleted entirely from the topic).
|
|
func (p *PostHeader) Nuke(ctx context.Context, u *User, ipaddr string) error {
|
|
var ar *AuditRecord = nil
|
|
defer func() {
|
|
AmStoreAudit(ar)
|
|
}()
|
|
success := false
|
|
tx := amdb.MustBegin()
|
|
defer func() {
|
|
if !success {
|
|
tx.Rollback()
|
|
}
|
|
}()
|
|
unlock := true
|
|
tx.ExecContext(ctx, "LOCK TABLES posts WRITE, postdata WRITE, postattach WRITE, postdogear WRITE, postpublish WRITE, topics WRITE;")
|
|
defer func() {
|
|
if unlock {
|
|
tx.ExecContext(ctx, "UNLOCK TABLES;")
|
|
}
|
|
}()
|
|
|
|
// Delete all the references to this post.
|
|
_, err := tx.ExecContext(ctx, "DELETE FROM posts WHERE postid = ?", p.PostId)
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM postdata WHERE postid = ?", p.PostId)
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM postattach WHERE postid = ?", p.PostId)
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM postdogear WHERE postid = ?", p.PostId)
|
|
if err == nil {
|
|
_, err = tx.ExecContext(ctx, "DELETE FROM postpublish WHERE postid = ?", p.PostId)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Renumber phase 1 - renumber posts in the same topic with a number higher than the nuked post
|
|
if _, err = tx.ExecContext(ctx, "UPDATE posts SET num = (num - 1) WHERE topicid = ? AND num > ?", p.TopicId, p.Num); err != nil {
|
|
return err
|
|
}
|
|
// Renumber phase 2 - reset the top message in this topic
|
|
if _, err = tx.ExecContext(ctx, "UPDATE topics SET top_message = (top_message - 1) WHERE topicid = ?", p.TopicId); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Unlock tables and commit.
|
|
tx.ExecContext(ctx, "UNLOCK TABLES;")
|
|
unlock = false
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
success = true
|
|
ar = AmNewAudit(AuditConferenceNukeMessage, u.Uid, ipaddr, fmt.Sprintf("post=%d", p.PostId))
|
|
return nil
|
|
}
|
|
|
|
/* AmGetPost gets a single post from the database by ID.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* postId - ID of the post to retrieve.
|
|
* Returns:
|
|
* Pointer to PostHeader for the post, or nil.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmGetPost(ctx context.Context, postId int64) (*PostHeader, error) {
|
|
var dbdata []PostHeader
|
|
if err := amdb.SelectContext(ctx, &dbdata, "SELECT * FROM posts WHERE postid = ?", postId); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(dbdata) == 0 {
|
|
return nil, errors.New("post not found")
|
|
}
|
|
if len(dbdata) > 1 {
|
|
return nil, fmt.Errorf("AmGetPost: too many entries (%d) for post ID %d", len(dbdata), postId)
|
|
}
|
|
return &(dbdata[0]), nil
|
|
}
|
|
|
|
/* AmGetPostRange gets a range of posts from a topic by post numbers.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* topic - Topic pointer to retrieve posts from.
|
|
* first - Number of first post to retrieve.
|
|
* last - Number of last post to retrieve.
|
|
* Returns:
|
|
* Array of pointers to PostHeader objects, or nil.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmGetPostRange(ctx context.Context, topic *Topic, first, last int32) ([]*PostHeader, error) {
|
|
var posts []PostHeader
|
|
if err := amdb.SelectContext(ctx, &posts, "SELECT * FROM posts WHERE topicid = ? AND num >= ? AND num <= ? ORDER BY num", topic.TopicId, first, last); err != nil {
|
|
return nil, err
|
|
}
|
|
rc := make([]*PostHeader, len(posts))
|
|
for i := range posts {
|
|
rc[i] = &(posts[i])
|
|
}
|
|
return rc, nil
|
|
}
|
|
|
|
/* AmNewPost adds a new post to a topic.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* conf - Pointer to conference containing the topic.
|
|
* topic - Pointer to topic.
|
|
* user - Pointer to user posting the message.
|
|
* pseud - Pseud for the new post.
|
|
* post - New post text.
|
|
* postLines - Number of lines in the post text.
|
|
* ipaddr - IP address of user maing the post.
|
|
* Returns:
|
|
* New post header pointer.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmNewPost(ctx context.Context, conf *Conference, topic *Topic, user *User, pseud string, post string, postLines int32, ipaddr string) (*PostHeader, error) {
|
|
success := false
|
|
var ar *AuditRecord = nil
|
|
defer func() {
|
|
AmStoreAudit(ar)
|
|
}()
|
|
tx := amdb.MustBegin()
|
|
defer func() {
|
|
if !success {
|
|
tx.Rollback()
|
|
}
|
|
}()
|
|
unlock := true
|
|
tx.ExecContext(ctx, "LOCK TABLES confs WRITE, topics WRITE, topicsettings WRITE, posts WRITE, postdata WRITE;")
|
|
defer func() {
|
|
if unlock {
|
|
tx.ExecContext(ctx, "UNLOCK TABLES;")
|
|
}
|
|
}()
|
|
|
|
// Add the post header information.
|
|
rs, err := tx.ExecContext(ctx, "INSERT INTO posts (topicid, num, linecount, creator_uid, posted, pseud) VALUES (?, ?, ?, ?, NOW(), ?)",
|
|
topic.TopicId, topic.TopMessage+1, postLines, user.Uid, pseud)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
xid, err := rs.LastInsertId()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read back the post header.
|
|
var dbdata []PostHeader
|
|
if err := tx.SelectContext(ctx, &dbdata, "SELECT * FROM posts WHERE postid = ?", xid); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(dbdata) == 0 {
|
|
return nil, errors.New("AmNewPost: new post not found")
|
|
}
|
|
if len(dbdata) > 1 {
|
|
return nil, fmt.Errorf("AmNewPost: too many entries (%d) for post ID %d", len(dbdata), xid)
|
|
}
|
|
hdr := &(dbdata[0])
|
|
|
|
// Add the post data.
|
|
_, err = tx.ExecContext(ctx, "INSERT INTO postdata (postid, data) VALUES (?, ?)", hdr.PostId, post)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Update the topic.
|
|
_, err = tx.ExecContext(ctx, "UPDATE topics SET top_message = ?, lastupdate = ? WHERE topicid = ?", hdr.Num, hdr.Posted, topic.TopicId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
topic.TopMessage = hdr.Num
|
|
topic.LastUpdate = hdr.Posted
|
|
|
|
tx.ExecContext(ctx, "UNLOCK TABLES;")
|
|
unlock = false
|
|
|
|
// update the "last update" date of the conference and the "last posted" date in the conference settings
|
|
if err = conf.TouchUpdate(ctx, tx, hdr.Posted); err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = conf.TouchPost(ctx, tx, user, hdr.Posted)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err = tx.Commit(); err != nil {
|
|
return nil, err
|
|
}
|
|
success = true
|
|
|
|
// create audit record
|
|
ar = AmNewAudit(AuditConferencePostMessage, user.Uid, ipaddr, fmt.Sprintf("confid=%d", conf.ConfId),
|
|
fmt.Sprintf("topic=%d", topic.Number), fmt.Sprintf("post=%d", hdr.PostId), fmt.Sprintf("pseud=%s", *hdr.Pseud))
|
|
|
|
return hdr, nil
|
|
}
|
|
|
|
/* AmGetPublishedPosts gets all posts published to the front page, up to the maximum number configured in the database.
|
|
* Parameters:
|
|
* ctx - Standard Go context value.
|
|
* Returns:
|
|
* Array of post headers, or nil.
|
|
* Standard Go error status.
|
|
*/
|
|
func AmGetPublishedPosts(ctx context.Context) ([]*PostHeader, error) {
|
|
// Read the globals.
|
|
gv, err := AmGlobals(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Read the published posts.
|
|
rs, err := amdb.QueryContext(ctx, "SELECT postid FROM postpublish ORDER BY on_date DESC")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Extract post IDs to an array.
|
|
pids := make([]int64, gv.FrontPagePosts)
|
|
i := 0
|
|
for i < int(gv.FrontPagePosts) && rs.Next() {
|
|
if err = rs.Scan(&(pids[i])); err != nil {
|
|
return nil, err
|
|
}
|
|
i++
|
|
}
|
|
if i < int(gv.FrontPagePosts) {
|
|
pids = pids[:i] // truncate if we have fewer posts than spaces
|
|
}
|
|
|
|
// Use the post IDs to build a SQL statement.
|
|
pidStrs := make([]string, len(pids))
|
|
for i, pid := range pids {
|
|
pidStrs[i] = fmt.Sprintf("%d", pid)
|
|
}
|
|
sql := fmt.Sprintf("SELECT * FROM posts WHERE postid IN (%s)", strings.Join(pidStrs, ", "))
|
|
|
|
// Use the SQL to read in all the post headers using a single database query.
|
|
var data []PostHeader
|
|
if err = amdb.SelectContext(ctx, &data, sql); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(data) < len(pids) {
|
|
return nil, errors.New("internal error reading post headers")
|
|
}
|
|
|
|
// Build the return array by making sure we point to the post headers in the same order the post IDs were returned.
|
|
rc := make([]*PostHeader, len(pids))
|
|
q := 0
|
|
for i := range data {
|
|
for j := range pids {
|
|
if data[i].PostId == pids[j] {
|
|
rc[j] = &(data[i])
|
|
q++
|
|
}
|
|
}
|
|
}
|
|
if q < len(pids) {
|
|
return nil, errors.New("internal error generating output")
|
|
}
|
|
|
|
return rc, nil
|
|
}
|