Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

guilds.gno

6.10 Kb · 154 lines
  1// Package guilds records which Discord server a court's moderators chose.
  2//
  3// WHY A SEPARATE REALM. kourtv2 is deployed and knows nothing about Discord, and
  4// a realm on chain is not edited — adding a field there means a new realm and a
  5// migration for every court that exists. This holds one string per court and
  6// asks kourtv2 who may set it, so the court's own moderator list stays the only
  7// authority and this file never has one of its own.
  8//
  9// WHY IT EXISTS AT ALL, when the service could verify a signed message and did.
 10// It could, and the signature had to be made at a terminal, because a wallet
 11// signs TRANSACTIONS and not arbitrary strings. So the last step of standing up
 12// a court's Discord server was a command nobody would ever discover, and the
 13// court page said so and stopped. A realm call IS a transaction, which is
 14// exactly what a wallet already does — the same button every other action on the
 15// page uses.
 16//
 17// WHAT IT DOES NOT DO. It does not check that the guild exists, that the bot is
 18// in it, or that anybody can join it. Those are the service's to know and it
 19// already does; this records a CHOICE, made by an address the court recognises,
 20// at a height anybody can read back. A guild id here with no bot behind it is a
 21// court pointing at nothing, which is visible rather than dangerous.
 22package guilds
 23
 24import (
 25	"chain"
 26	"strings"
 27
 28	bptree "gno.land/p/nt/bptree/v0"
 29
 30	kourt "gno.land/r/g1ecsuj0q572jr0dhu29q9njtnmw03hyu7tyyvv6/kourt"
 31)
 32
 33// choice is what a court's moderators decided: which server, and who said so.
 34//
 35// THE SETTER IS STORED, not only emitted. The event carries it too, and the
 36// event is the history — but kourt.xyz reads this realm over qeval and has no
 37// indexer, so a service that recorded "listed, signed by" from the event alone
 38// would need one. Reading it back is one lookup on a page that already does one.
 39type choice struct {
 40	guildID string
 41	by      address
 42}
 43
 44// guildOf is court slug -> choice. One server per court: a court with two is a
 45// court whose readers are told different things by the same page.
 46//
 47// A bptree rather than an avl tree because kourtv2 uses one for every ordered
 48// map it keeps, and Courts() wants slug order.
 49var guildOf = bptree.NewBPTree32()
 50
 51// Set records the guild for a court, or clears it when guildID is empty.
 52//
 53// THE CALLER IS THE MODERATOR, NOT THIS REALM. cur.Previous().Address() is who
 54// asked, and kourt.IsCourtMod is the only thing consulted — so a moderator
 55// removed by the court loses this at the same moment, with no state here to keep
 56// in step. IsCourtMod panics on a court that does not exist, which is the answer
 57// we want: there is nothing to bind.
 58func Set(cur realm, courtSlug, guildID string) {
 59	if !cur.IsCurrent() {
 60		panic("guilds: stale realm")
 61	}
 62	who := cur.Previous().Address()
 63	if !kourt.IsCourtMod(courtSlug, who) {
 64		panic("guilds: only a moderator of this court may set its server")
 65	}
 66	id := strings.TrimSpace(guildID)
 67	if id == "" {
 68		if _, removed := guildOf.Remove(courtSlug); removed {
 69			chain.Emit("GuildCleared", "court", courtSlug, "by", who.String())
 70		}
 71		return
 72	}
 73	// A Discord snowflake is decimal digits, and every id minted since 2015 is
 74	// 17 to 20 of them. Bounded here so a mistyped paste is refused at the call
 75	// rather than stored and puzzled over on the page.
 76	if len(id) < 17 || len(id) > 20 {
 77		panic("guilds: a Discord server id is 17 to 20 digits")
 78	}
 79	for i := 0; i < len(id); i++ {
 80		if id[i] < '0' || id[i] > '9' {
 81			panic("guilds: a Discord server id is digits only")
 82		}
 83	}
 84	guildOf.Set(courtSlug, choice{guildID: id, by: who})
 85	chain.Emit("GuildSet", "court", courtSlug, "guild", id, "by", who.String())
 86}
 87
 88// GuildOf is the court's chosen server, or "" if it has none. Never panics: the
 89// service asks this about every court it knows, including ones that never chose.
 90func GuildOf(courtSlug string) string {
 91	c, ok := guildOf.Get(courtSlug).(choice)
 92	if !ok {
 93		return ""
 94	}
 95	return c.guildID
 96}
 97
 98// Chosen is the whole record: the server, and the address that chose it.
 99//
100// ONE READ AND NOT TWO, and this exists for that reason alone. kourt.xyz asks
101// both questions together and acts on the pair — it publishes the guild AND
102// records the setter as who signed for it — and two qeval calls can straddle a
103// change. A moderator re-aiming the binding between them hands one decision's
104// guild to another decision's signer, which is then written into the site's
105// database as fact. A pair that cannot be read atomically should not be read as a
106// pair.
107//
108// BOTH EMPTY OR NEITHER. A court that never chose answers ("", ""), which is the
109// commonest answer and is not an error.
110//
111// THE SETTER IS WHO ASKED, NOT WHO MODERATES NOW. A court that replaces its
112// moderators does not change this — the choice was made by whoever made it, and
113// rewriting that would destroy the only record of who did. Whether they STILL
114// moderate is a separate question, and kourtv2 is the one to ask it.
115//
116// STRINGS AND NOT AN address, because the caller is a machine reading qeval's
117// printed output: an address prints as `("g1…" .uverse.address)` and a string as
118// `("g1…" string)`, and the second is the one a parser can hold to.
119func Chosen(courtSlug string) (guildID, by string) {
120	c, ok := guildOf.Get(courtSlug).(choice)
121	if !ok {
122		return "", ""
123	}
124	return c.guildID, c.by.String()
125}
126
127// Courts lists every court that has chosen a server, in slug order — so the
128// service can reconcile the whole set without knowing what to ask for.
129func Courts() []string {
130	out := []string{}
131	guildOf.Iterate("", "", func(k string, _ any) bool {
132		out = append(out, k)
133		return false
134	})
135	return out
136}
137
138func Render(path string) string {
139	var b strings.Builder
140	b.WriteString("# Court Discord servers\n\n")
141	b.WriteString("Which Discord server each court's moderators chose. ")
142	b.WriteString("Set by a moderator of the court, read by kourt.xyz.\n\n")
143	n := 0
144	guildOf.Iterate("", "", func(k string, v any) bool {
145		c, _ := v.(choice)
146		b.WriteString("- **" + k + "** — `" + c.guildID + "` (set by " + c.by.String() + ")\n")
147		n++
148		return false
149	})
150	if n == 0 {
151		b.WriteString("_No court has chosen one yet._\n")
152	}
153	return b.String()
154}