users.gno

8.33 Kb ยท 350 lines
  1package users
  2
  3import (
  4	"regexp"
  5	"std"
  6	"strconv"
  7	"strings"
  8
  9	"gno.land/p/demo/avl"
 10	"gno.land/p/demo/avl/pager"
 11	"gno.land/p/demo/avlhelpers"
 12	"gno.land/p/demo/users"
 13)
 14
 15//----------------------------------------
 16// State
 17
 18var (
 19	admin std.Address = "g1u7y667z64x2h7vc6fmpcprgey4ck233jaww9zq" // @moul
 20
 21	restricted avl.Tree                  // Name -> true - restricted name
 22	name2User  avl.Tree                  // Name -> *users.User
 23	addr2User  avl.Tree                  // std.Address -> *users.User
 24	invites    avl.Tree                  // string(inviter+":"+invited) -> true
 25	counter    int                       // user id counter
 26	minFee     int64    = 20 * 1_000_000 // minimum gnot must be paid to register.
 27	maxFeeMult int64    = 10             // maximum multiples of minFee accepted.
 28)
 29
 30//----------------------------------------
 31// Top-level functions
 32
 33func Register(inviter std.Address, name string, profile string) {
 34	// assert CallTx call.
 35	std.AssertOriginCall()
 36	// assert invited or paid.
 37	caller := std.GetCallerAt(2)
 38	if caller != std.GetOrigCaller() {
 39		panic("should not happen") // because std.AssertOrigCall().
 40	}
 41
 42	sentCoins := std.GetOrigSend()
 43	minCoin := std.NewCoin("ugnot", minFee)
 44
 45	if inviter == "" {
 46		// banker := std.GetBanker(std.BankerTypeOrigSend)
 47		if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) {
 48			if sentCoins[0].Amount > minFee*maxFeeMult {
 49				panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult)))
 50			} else {
 51				// ok
 52			}
 53		} else {
 54			panic("payment must not be less than " + strconv.Itoa(int(minFee)))
 55		}
 56	} else {
 57		invitekey := inviter.String() + ":" + caller.String()
 58		_, ok := invites.Get(invitekey)
 59		if !ok {
 60			panic("invalid invitation")
 61		}
 62		invites.Remove(invitekey)
 63	}
 64
 65	// assert not already registered.
 66	_, ok := name2User.Get(name)
 67	if ok {
 68		panic("name already registered: " + name)
 69	}
 70	_, ok = addr2User.Get(caller.String())
 71	if ok {
 72		panic("address already registered: " + caller.String())
 73	}
 74
 75	isInviterAdmin := inviter == admin
 76
 77	// check for restricted name
 78	if _, isRestricted := restricted.Get(name); isRestricted {
 79		// only address invite by the admin can register restricted name
 80		if !isInviterAdmin {
 81			panic("restricted name: " + name)
 82		}
 83
 84		restricted.Remove(name)
 85	}
 86
 87	// assert name is valid.
 88	// admin inviter can bypass name restriction
 89	if !isInviterAdmin && !reName.MatchString(name) {
 90		panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)")
 91	}
 92
 93	// remainder of fees go toward invites.
 94	invites := int(0)
 95	if len(sentCoins) == 1 {
 96		if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee {
 97			invites = int(sentCoins[0].Amount / minFee)
 98			if inviter == "" && invites > 0 {
 99				invites -= 1
100			}
101		}
102	}
103	// register.
104	counter++
105	user := &users.User{
106		Address: caller,
107		Name:    name,
108		Profile: profile,
109		Number:  counter,
110		Invites: invites,
111		Inviter: inviter,
112	}
113	name2User.Set(name, user)
114	addr2User.Set(caller.String(), user)
115}
116
117func Invite(invitee string) {
118	// assert CallTx call.
119	std.AssertOriginCall()
120	// get caller/inviter.
121	caller := std.GetCallerAt(2)
122	if caller != std.GetOrigCaller() {
123		panic("should not happen") // because std.AssertOrigCall().
124	}
125	lines := strings.Split(invitee, "\n")
126	if caller == admin {
127		// nothing to do, all good
128	} else {
129		// ensure has invites.
130		userI, ok := addr2User.Get(caller.String())
131		if !ok {
132			panic("user unknown")
133		}
134		user := userI.(*users.User)
135		if user.Invites <= 0 {
136			panic("user has no invite tokens")
137		}
138		user.Invites -= len(lines)
139		if user.Invites < 0 {
140			panic("user has insufficient invite tokens")
141		}
142	}
143	// for each line...
144	for _, line := range lines {
145		if line == "" {
146			continue // file bodies have a trailing newline.
147		} else if strings.HasPrefix(line, `//`) {
148			continue // comment
149		}
150		// record invite.
151		invitekey := string(caller) + ":" + string(line)
152		invites.Set(invitekey, true)
153	}
154}
155
156func GrantInvites(invites string) {
157	// assert CallTx call.
158	std.AssertOriginCall()
159	// assert admin.
160	caller := std.GetCallerAt(2)
161	if caller != std.GetOrigCaller() {
162		panic("should not happen") // because std.AssertOrigCall().
163	}
164	if caller != admin {
165		panic("unauthorized")
166	}
167	// for each line...
168	lines := strings.Split(invites, "\n")
169	for _, line := range lines {
170		if line == "" {
171			continue // file bodies have a trailing newline.
172		} else if strings.HasPrefix(line, `//`) {
173			continue // comment
174		}
175		// parse name and invites.
176		var name string
177		var invites int
178		parts := strings.Split(line, ":")
179		if len(parts) == 1 { // short for :1.
180			name = parts[0]
181			invites = 1
182		} else if len(parts) == 2 {
183			name = parts[0]
184			invites_, err := strconv.Atoi(parts[1])
185			if err != nil {
186				panic(err)
187			}
188			invites = int(invites_)
189		} else {
190			panic("should not happen")
191		}
192		// give invites.
193		userI, ok := name2User.Get(name)
194		if !ok {
195			// maybe address.
196			userI, ok = addr2User.Get(name)
197			if !ok {
198				panic("invalid user " + name)
199			}
200		}
201		user := userI.(*users.User)
202		user.Invites += invites
203	}
204}
205
206// Any leftover fees go toward invitations.
207func SetMinFee(newMinFee int64) {
208	// assert CallTx call.
209	std.AssertOriginCall()
210	// assert admin caller.
211	caller := std.GetCallerAt(2)
212	if caller != admin {
213		panic("unauthorized")
214	}
215	// update global variables.
216	minFee = newMinFee
217}
218
219// This helps prevent fat finger accidents.
220func SetMaxFeeMultiple(newMaxFeeMult int64) {
221	// assert CallTx call.
222	std.AssertOriginCall()
223	// assert admin caller.
224	caller := std.GetCallerAt(2)
225	if caller != admin {
226		panic("unauthorized")
227	}
228	// update global variables.
229	maxFeeMult = newMaxFeeMult
230}
231
232//----------------------------------------
233// Exposed public functions
234
235func GetUserByName(name string) *users.User {
236	userI, ok := name2User.Get(name)
237	if !ok {
238		return nil
239	}
240	return userI.(*users.User)
241}
242
243func GetUserByAddress(addr std.Address) *users.User {
244	userI, ok := addr2User.Get(addr.String())
245	if !ok {
246		return nil
247	}
248	return userI.(*users.User)
249}
250
251// unlike GetUserByName, input must be "@" prefixed for names.
252func GetUserByAddressOrName(input users.AddressOrName) *users.User {
253	name, isName := input.GetName()
254	if isName {
255		return GetUserByName(name)
256	}
257	return GetUserByAddress(std.Address(input))
258}
259
260// Get a list of user names starting from the given prefix. Limit the
261// number of results to maxResults. (This can be used for a name search tool.)
262func ListUsersByPrefix(prefix string, maxResults int) []string {
263	return avlhelpers.ListByteStringKeysByPrefix(name2User, prefix, maxResults)
264}
265
266func Resolve(input users.AddressOrName) std.Address {
267	name, isName := input.GetName()
268	if !isName {
269		return std.Address(input) // TODO check validity
270	}
271
272	user := GetUserByName(name)
273	return user.Address
274}
275
276// Add restricted name to the list
277func AdminAddRestrictedName(name string) {
278	// assert CallTx call.
279	std.AssertOriginCall()
280	// get caller
281	caller := std.GetOrigCaller()
282	// assert admin
283	if caller != admin {
284		panic("unauthorized")
285	}
286
287	if user := GetUserByName(name); user != nil {
288		panic("already registered name")
289	}
290
291	// register restricted name
292
293	restricted.Set(name, true)
294}
295
296//----------------------------------------
297// Constants
298
299// NOTE: name length must be clearly distinguishable from a bech32 address.
300var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`)
301
302//----------------------------------------
303// Render main page
304
305func Render(fullPath string) string {
306	path, _ := splitPathAndQuery(fullPath)
307	if path == "" {
308		return renderHome(fullPath)
309	} else if len(path) >= 38 { // 39? 40?
310		if path[:2] != "g1" {
311			return "invalid address " + path
312		}
313		user := GetUserByAddress(std.Address(path))
314		if user == nil {
315			// TODO: display basic information about account.
316			return "unknown address " + path
317		}
318		return user.Render()
319	} else {
320		user := GetUserByName(path)
321		if user == nil {
322			return "unknown username " + path
323		}
324		return user.Render()
325	}
326}
327
328func renderHome(path string) string {
329	doc := ""
330
331	page := pager.NewPager(&name2User, 50).MustGetPageByPath(path)
332
333	for _, item := range page.Items {
334		user := item.Value.(*users.User)
335		doc += " * [" + user.Name + "](/r/demo/users:" + user.Name + ")\n"
336	}
337	doc += "\n"
338	doc += page.Selector()
339	return doc
340}
341
342func splitPathAndQuery(fullPath string) (string, string) {
343	parts := strings.SplitN(fullPath, "?", 2)
344	path := parts[0]
345	queryString := ""
346	if len(parts) > 1 {
347		queryString = "?" + parts[1]
348	}
349	return path, queryString
350}