guestbook.gno

2.92 Kb ยท 127 lines
  1// Realm guestbook contains an implementation of a simple guestbook.
  2// Come and sign yourself up!
  3package guestbook
  4
  5import (
  6	"std"
  7	"strconv"
  8	"strings"
  9	"time"
 10	"unicode"
 11
 12	"gno.land/p/demo/avl"
 13	"gno.land/p/demo/seqid"
 14)
 15
 16// Signature is a single entry in the guestbook.
 17type Signature struct {
 18	Message string
 19	Author  std.Address
 20	Time    time.Time
 21}
 22
 23const (
 24	maxMessageLength = 140
 25	maxPerPage       = 25
 26)
 27
 28var (
 29	signatureID seqid.ID
 30	guestbook   avl.Tree // id -> Signature
 31	hasSigned   avl.Tree // address -> struct{}
 32)
 33
 34func init() {
 35	Sign("You reached the end of the guestbook!")
 36}
 37
 38const (
 39	errNotAUser                  = "this guestbook can only be signed by users"
 40	errAlreadySigned             = "you already signed the guestbook!"
 41	errInvalidCharacterInMessage = "invalid character in message"
 42)
 43
 44// Sign signs the guestbook, with the specified message.
 45func Sign(message string) {
 46	crossing()
 47	prev := std.PreviousRealm()
 48	switch {
 49	case !prev.IsUser():
 50		panic(errNotAUser)
 51	case hasSigned.Has(prev.Address().String()):
 52		panic(errAlreadySigned)
 53	}
 54	message = validateMessage(message)
 55
 56	guestbook.Set(signatureID.Next().Binary(), Signature{
 57		Message: message,
 58		Author:  prev.Address(),
 59		// NOTE: time.Now() will yield the "block time", which is deterministic.
 60		Time: time.Now(),
 61	})
 62	hasSigned.Set(prev.Address().String(), struct{}{})
 63}
 64
 65func validateMessage(msg string) string {
 66	if len(msg) > maxMessageLength {
 67		panic("Keep it brief! (max " + strconv.Itoa(maxMessageLength) + " bytes!)")
 68	}
 69	out := ""
 70	for _, ch := range msg {
 71		switch {
 72		case unicode.IsLetter(ch),
 73			unicode.IsNumber(ch),
 74			unicode.IsSpace(ch),
 75			unicode.IsPunct(ch):
 76			out += string(ch)
 77		default:
 78			panic(errInvalidCharacterInMessage)
 79		}
 80	}
 81	return out
 82}
 83
 84func Render(maxID string) string {
 85	var bld strings.Builder
 86
 87	bld.WriteString("# Guestbook ๐Ÿ“\n\n[Come sign the guestbook!](./guestbook$help&func=Sign)\n\n---\n\n")
 88
 89	var maxIDBinary string
 90	if maxID != "" {
 91		mid, err := seqid.FromString(maxID)
 92		if err != nil {
 93			panic(err)
 94		}
 95
 96		// AVL iteration is exclusive, so we need to decrease the ID value to get the "true" maximum.
 97		mid--
 98		maxIDBinary = mid.Binary()
 99	}
100
101	var lastID seqid.ID
102	var printed int
103	guestbook.ReverseIterate("", maxIDBinary, func(key string, val any) bool {
104		sig := val.(Signature)
105		message := strings.ReplaceAll(sig.Message, "\n", "\n> ")
106		bld.WriteString("> " + message + "\n>\n")
107		idValue, ok := seqid.FromBinary(key)
108		if !ok {
109			panic("invalid seqid id")
110		}
111
112		bld.WriteString("> _Written by " + sig.Author.String() + " at " + sig.Time.Format(time.DateTime) + "_ (#" + idValue.String() + ")\n\n---\n\n")
113		lastID = idValue
114
115		printed++
116		// stop after exceeding limit
117		return printed >= maxPerPage
118	})
119
120	if printed == 0 {
121		bld.WriteString("No messages!")
122	} else if printed >= maxPerPage {
123		bld.WriteString("<p style='text-align:right'><a href='./guestbook:" + lastID.String() + "'>Next page</a></p>")
124	}
125
126	return bld.String()
127}