PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/x/wiki/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / x / wiki
Files
18 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/x/wiki/v0
gno
0.9

Files (18)

  • README.mdmarkdown
  • gnomod.tomltoml
  • diff.gnogno
  • doc.gnogno
  • links.gnogno
  • page.gnogno
  • render.gnogno
  • revision.gnogno
  • talk.gnogno
  • title.gnogno
  • wiki.gnogno
  • diff_test.gnogno
  • limits_test.gnogno
  • links_test.gnogno
  • render_test.gnogno
  • talk_test.gnogno
  • title_test.gnogno
  • wiki_test.gnogno
  • talk.gnogno
    1package wiki23import (4	"errors"5	"time"67	"gno.land/p/moul/ulist/v0"8

    Functions

    not supported for pure packages by the node (vm/qfuncs)

    Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

    "gno.land/p/nt/avl/v0"
    9)
    10
    11// MaxCommentLen bounds one comment in bytes.
    12const MaxCommentLen = 2000
    13
    14// MaxReplyDepth is 1: a comment replies to a top-level comment, and that is
    15// all. Deeper nesting needs recursive rendering with no natural bound, which
    16// is exactly the shape a query gas ceiling punishes.
    17const MaxReplyDepth = 1
    18
    19var (
    20 ErrCommentTooLong = errors.New("wiki: comment exceeds the size limit")
    21 ErrEmptyComment = errors.New("wiki: empty comment")
    22 ErrNoSuchComment = errors.New("wiki: no such comment")
    23 ErrReplyToReply = errors.New("wiki: replies nest one level only")
    24)
    25
    26// Comment is one message in a page's discussion.
    27//
    28// Discussion is a comment store rather than a "Talk:" article, which is the
    29// one place this design departs from MediaWiki on purpose. A talk page is an
    30// article, so the last editor can rewrite what someone else said, and
    31// moderating one bad message means editing the whole page. An append-only
    32// store gives each message its own author, its own timestamp and its own
    33// moderation, and nobody can silently rewrite anyone else's words.
    34type Comment struct {
    35 ID uint64
    36 Parent uint64 // 0 for a top-level comment
    37 Author address
    38 Time time.Time
    39 Height int64
    40 Body string
    41 Hidden bool // a steward hid it; Body is cleared and its deposit released
    42}
    43
    44// Comment appends a message to a page's discussion. replyTo is 0 for a
    45// top-level message, or the id of a top-level message to reply to.
    46//
    47// The engine does not decide who may comment: as with Edit, that is the
    48// realm's call. It does enforce the shape, because the shape is what bounds
    49// the render.
    50func (w *Wiki) Comment(author address, now time.Time, height int64, raw, body string, replyTo uint64) (*Comment, error) {
    51 t, err := ParseTitle(raw)
    52 if err != nil {
    53 return nil, err
    54 }
    55 if !w.Exists(t) {
    56 return nil, ErrNoSuchPage
    57 }
    58 if body == "" {
    59 return nil, ErrEmptyComment
    60 }
    61 if len(body) > MaxCommentLen {
    62 return nil, ErrCommentTooLong
    63 }
    64
    65 list := w.thread(t, true)
    66 if replyTo != 0 {
    67 parent := findComment(list, replyTo)
    68 if parent == nil {
    69 return nil, ErrNoSuchComment
    70 }
    71 if parent.Parent != 0 {
    72 return nil, ErrReplyToReply
    73 }
    74 }
    75
    76 w.nextComment++
    77 c := &Comment{
    78 ID: w.nextComment,
    79 Parent: replyTo,
    80 Author: author,
    81 Time: now,
    82 Height: height,
    83 Body: body,
    84 }
    85 list.Append(c)
    86 w.numComments++
    87 w.bytesHeld += len(body)
    88 return c, nil
    89}
    90
    91// HideComment clears one message's body and returns the bytes released. The
    92// message itself stays in the thread, so a deleted comment reads as "removed"
    93// rather than as a gap someone has to reconstruct from block explorers.
    94func (w *Wiki) HideComment(raw string, id uint64) (int, error) {
    95 t, err := ParseTitle(raw)
    96 if err != nil {
    97 return 0, err
    98 }
    99 list := w.thread(t, false)
    100 if list == nil {
    101 return 0, ErrNoSuchComment
    102 }
    103 c := findComment(list, id)
    104 if c == nil {
    105 return 0, ErrNoSuchComment
    106 }
    107 if c.Hidden {
    108 return 0, nil
    109 }
    110 n := len(c.Body)
    111 c.Body = ""
    112 c.Hidden = true
    113 w.bytesHeld -= n
    114 return n, nil
    115}
    116
    117// Comments returns up to count top-level messages of a page's discussion,
    118// oldest first, skipping offset of them, each paired with its replies in the
    119// order they were written.
    120func (w *Wiki) Comments(t Title, offset, count int) []*Thread {
    121 out := []*Thread{}
    122 list := w.thread(t, false)
    123 if list == nil || count <= 0 {
    124 return out
    125 }
    126
    127 // One pass to bucket replies by parent, a second to emit top-level
    128 // messages in order. Buckets live in an avl rather than a map because
    129 // this feeds a render, and gno map iteration order is unspecified.
    130 replies := avl.NewTree()
    131 list.Iterator(0, list.Size()-1, func(_ int, v any) bool {
    132 c := v.(*Comment)
    133 if c.Parent == 0 {
    134 return false
    135 }
    136 key := commentKey(c.Parent)
    137 var bucket []*Comment
    138 if b := replies.Get(key); b != nil {
    139 bucket = b.([]*Comment)
    140 }
    141 replies.Set(key, append(bucket, c))
    142 return false
    143 })
    144
    145 skipped := 0
    146 list.Iterator(0, list.Size()-1, func(_ int, v any) bool {
    147 c := v.(*Comment)
    148 if c.Parent != 0 {
    149 return false
    150 }
    151 if skipped < offset {
    152 skipped++
    153 return false
    154 }
    155 th := &Thread{Root: c}
    156 if b := replies.Get(commentKey(c.ID)); b != nil {
    157 th.Replies = b.([]*Comment)
    158 }
    159 out = append(out, th)
    160 return len(out) >= count
    161 })
    162 return out
    163}
    164
    165// Thread is a top-level comment and its replies.
    166type Thread struct {
    167 Root *Comment
    168 Replies []*Comment
    169}
    170
    171// NumComments returns how many messages a page's discussion holds, replies
    172// and hidden messages included.
    173func (w *Wiki) NumComments(t Title) int {
    174 list := w.thread(t, false)
    175 if list == nil {
    176 return 0
    177 }
    178 return list.Size()
    179}
    180
    181// thread returns a page's comment list, creating it when create is set.
    182func (w *Wiki) thread(t Title, create bool) *ulist.List {
    183 v := w.talk.Get(t.Key())
    184 if v != nil {
    185 return v.(*ulist.List)
    186 }
    187 if !create {
    188 return nil
    189 }
    190 list := ulist.New()
    191 w.talk.Set(t.Key(), list)
    192 return list
    193}
    194
    195func findComment(list *ulist.List, id uint64) *Comment {
    196 var found *Comment
    197 list.Iterator(0, list.Size()-1, func(_ int, v any) bool {
    198 c := v.(*Comment)
    199 if c.ID == id {
    200 found = c
    201 return true
    202 }
    203 return false
    204 })
    205 return found
    206}
    207
    208// commentKey is only ever used as an avl key for reply buckets, where the
    209// order does not matter, so a plain decimal is fine here. Anything whose
    210// ordering IS meaningful needs a fixed-width key: see Title.Key.
    211func commentKey(id uint64) string {
    212 if id == 0 {
    213 return "0"
    214 }
    215 var b []byte
    216 for id > 0 {
    217 b = append([]byte{byte('0' + id%10)}, b...)
    218 id /= 10
    219 }
    220 return string(b)
    221}
    222