/* * Amsterdam Web Communities System * Copyright (c) 2025 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 ( "errors" "math" "strconv" "strings" ) // PostLinkData is the structure holding the decoded parts of the post link. type PostLinkData struct { Community string Conference string Topic int16 FirstPost int32 LastPost int32 } // NeedsDBVerification returns true if the post link data needs tro be varified against the database. func (d *PostLinkData) NeedsDBVerification() bool { return d.Community != "" || d.Conference != "" } // VerifyNames verifies the post link data against the database. func (d *PostLinkData) VerifyNames() error { if d.Community != "" { comm, err := AmGetCommunityByAlias(d.Community) if err != nil { return err } if comm == nil { return errors.New("community alias not found") } } if d.Conference != "" { conf, err := AmGetConferenceByAlias(d.Conference) if err != nil { return err } if conf == nil { return errors.New("conference alias not found") } } return nil } // Maximum lengths of the components. const ( maxLinkLength = 130 maxCommunityLength = 32 maxConferenceLength = 64 ) // validateCommunity validates the community name and saves it. func validateCommunity(name string, rc *PostLinkData) error { if len(name) > maxCommunityLength { return errors.New("community alias is too long") } if !AmIsValidAmsterdamID(name) { return errors.New("community alias is not a valid identifier") } rc.Community = name return nil } // validateConference validates the conference name and saves it. func validateConference(name string, rc *PostLinkData) error { if len(name) > maxConferenceLength { return errors.New("conference alias is too long") } if !AmIsValidAmsterdamID(name) { return errors.New("conference alias is not a valid identifier") } rc.Conference = name return nil } // decodeTopicNumber decodes the topic number and saves it. func decodeTopicNumber(data string, rc *PostLinkData) error { v, err := strconv.Atoi(data) if err != nil { return errors.New("invalid topic number reference") } if v > math.MaxInt16 { return errors.New("topic number out of range") } rc.Topic = int16(v) return nil } // decodePostRange decodes the post ranges (first and last post) and saves them. func decodePostRange(data string, rc *PostLinkData) error { pos := strings.IndexByte(data, '-') var tempVal int32 = -1 if pos > 0 { temp := data[:pos] data = data[pos+1:] v, err := strconv.Atoi(temp) if err != nil { return errors.New("invalid post number reference") } tempVal = int32(v) if len(data) == 0 { // range is open-ended (number-) rc.FirstPost = tempVal rc.LastPost = -1 return nil } } else if pos == 0 { return errors.New("cannot have - at beginning of post range") } v2, err := strconv.Atoi(data) if err != nil { return errors.New("invalid post number reference") } rc.FirstPost = int32(v2) if tempVal >= 0 { if tempVal < rc.FirstPost { // "frontwards" range - reorder the components rc.LastPost = rc.FirstPost rc.FirstPost = tempVal } else { // "backwards" range rc.LastPost = tempVal } } else { // a "range" of a single post rc.LastPost = rc.FirstPost } return nil } /* AmDecodePostLink decodes a post link and returns the complete breakdown of its components. * Parameters: * data - The post link to be decoded. * Returns: * Pointer to structure containing post link data, or nil. * Standard Go error status. */ func AmDecodePostLink(data string) (*PostLinkData, error) { if data == "" { return nil, errors.New("empty string") } if len(data) > maxLinkLength { return nil, errors.New("post link string too long") } rc := PostLinkData{ Community: "", Conference: "", Topic: -1, FirstPost: -1, LastPost: -1, } work := data // First test: Bang pos := strings.IndexByte(work, '!') if pos > 0 { err := validateCommunity(work[:pos], &rc) if err != nil { return nil, err } work = work[pos+1:] if len(work) == 0 { return &rc, nil // community link } } else if pos == 0 { return nil, errors.New("cannot have ! at beginning") } // Second test: Dot #1 pos = strings.IndexByte(work, '.') if pos < 0 { // no dots in here, must be either "postlink" or "community!conference" var err error if rc.Community == "" { err = decodePostRange(work, &rc) } else { err = validateConference(work, &rc) } if err != nil { return nil, err } } // Peel off the initial substring before the dot. confOrTopic := work[:pos] work = work[pos+1:] if len(work) == 0 { // we had "conference." or "topic." or maybe "community!conference." if rc.Community == "" { // it's either "conference." or "topic." - try the latter first err := decodeTopicNumber(confOrTopic, &rc) if err != nil { // it's not a topic number, try it as a conference name err = validateConference(confOrTopic, &rc) } if err != nil { return nil, err } } else { // it was "community!conference." err := validateConference(confOrTopic, &rc) if err != nil { return nil, err } } } // Third test: Dot #2 pos = strings.IndexByte(work, '.') if pos < 0 { // we had "conference.topic" or "topic.posts" or maybe "community!conference.topic" var err error if rc.Community == "" { // either "conference.topic" or "topic.posts" isTopic := false err = decodeTopicNumber(confOrTopic, &rc) if err != nil { // it's "conference.topic" err = validateConference(confOrTopic, &rc) isTopic = true } if err == nil { if isTopic { err = decodeTopicNumber(work, &rc) } else { err = decodePostRange(work, &rc) } } } else { // we have "community!conference.topic" err = validateConference(confOrTopic, &rc) if err == nil { err = decodeTopicNumber(work, &rc) } } if err != nil { return nil, err } return &rc, nil } else if pos == 0 { return nil, errors.New("cannot have . at beginning of string") } // We definitely have "conference.topic.something" or "community!conference.topic.something" err := validateConference(confOrTopic, &rc) if err == nil { err = decodeTopicNumber(work[:pos], &rc) } if err != nil { return nil, err } work = work[pos+1:] if len(work) == 0 { // we had "conference.topic." or "communtiy!conference.topic.", those are both valid return &rc, nil } err = decodePostRange(work, &rc) // the rest must be the post range if err != nil { return nil, err } return &rc, nil }