Search Apps Documentation Source Content File Folder Download Copy

guestbook.gno

2.90 Kb ยท 126 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	prev := std.PrevRealm()
 47	switch {
 48	case !prev.IsUser():
 49		panic(errNotAUser)
 50	case hasSigned.Has(prev.Addr().String()):
 51		panic(errAlreadySigned)
 52	}
 53	message = validateMessage(message)
 54
 55	guestbook.Set(signatureID.Next().Binary(), Signature{
 56		Message: message,
 57		Author:  prev.Addr(),
 58		// NOTE: time.Now() will yield the "block time", which is deterministic.
 59		Time: time.Now(),
 60	})
 61	hasSigned.Set(prev.Addr().String(), struct{}{})
 62}
 63
 64func validateMessage(msg string) string {
 65	if len(msg) > maxMessageLength {
 66		panic("Keep it brief! (max " + strconv.Itoa(maxMessageLength) + " bytes!)")
 67	}
 68	out := ""
 69	for _, ch := range msg {
 70		switch {
 71		case unicode.IsLetter(ch),
 72			unicode.IsNumber(ch),
 73			unicode.IsSpace(ch),
 74			unicode.IsPunct(ch):
 75			out += string(ch)
 76		default:
 77			panic(errInvalidCharacterInMessage)
 78		}
 79	}
 80	return out
 81}
 82
 83func Render(maxID string) string {
 84	var bld strings.Builder
 85
 86	bld.WriteString("# Guestbook ๐Ÿ“\n\n[Come sign the guestbook!](./guestbook$help&func=Sign)\n\n---\n\n")
 87
 88	var maxIDBinary string
 89	if maxID != "" {
 90		mid, err := seqid.FromString(maxID)
 91		if err != nil {
 92			panic(err)
 93		}
 94
 95		// AVL iteration is exclusive, so we need to decrease the ID value to get the "true" maximum.
 96		mid--
 97		maxIDBinary = mid.Binary()
 98	}
 99
100	var lastID seqid.ID
101	var printed int
102	guestbook.ReverseIterate("", maxIDBinary, func(key string, val interface{}) bool {
103		sig := val.(Signature)
104		message := strings.ReplaceAll(sig.Message, "\n", "\n> ")
105		bld.WriteString("> " + message + "\n>\n")
106		idValue, ok := seqid.FromBinary(key)
107		if !ok {
108			panic("invalid seqid id")
109		}
110
111		bld.WriteString("> _Written by " + sig.Author.String() + " at " + sig.Time.Format(time.DateTime) + "_ (#" + idValue.String() + ")\n\n---\n\n")
112		lastID = idValue
113
114		printed++
115		// stop after exceeding limit
116		return printed >= maxPerPage
117	})
118
119	if printed == 0 {
120		bld.WriteString("No messages!")
121	} else if printed >= maxPerPage {
122		bld.WriteString("<p style='text-align:right'><a href='./guestbook:" + lastID.String() + "'>Next page</a></p>")
123	}
124
125	return bld.String()
126}