package users
import (
"regexp"
"std"
"strconv"
"strings"
"gno.land/p/avl"
)
//----------------------------------------
// Types
type User struct {
address std.Address
name string
profile string
number int
invites int
inviter std.Address
}
func (u *User) Render() string {
str := "## user " + u.name + "\n" +
"\n" +
" * address = " + string(u.address) + "\n" +
" * " + strconv.Itoa(u.invites) + " invites\n"
if u.inviter != "" {
str = str + " * invited by " + string(u.inviter) + "\n"
}
str = str + "\n" +
u.profile + "\n"
return str
}
func (u User) Name() string { return u.name }
func (u User) Profile() string { return u.profile }
func (u User) Address() std.Address { return u.address }
//----------------------------------------
// State
var (
admin std.Address = "g1us8428u2a5satrlxzagqqa5m6vmuze025anjlj"
name2User *avl.Tree // Name -> *User
addr2User *avl.Tree // std.Address -> *User
invites *avl.Tree // string(inviter+":"+invited) -> true
counter int // user id counter
minFee int64 = 200 * 1000000 // minimum gnot must be paid to register.
maxFeeMult int64 = 10 // maximum multiples of minFee accepted.
)
//----------------------------------------
// Top-level functions
func Register(inviter std.Address, name string, profile string) {
// assert CallTx call.
std.AssertOriginCall()
// assert invited or paid.
caller := std.GetCallerAt(2)
if caller != std.GetOrigCaller() {
panic("should not happen") // because std.AssertOrigCall().
}
sentCoins := std.GetOrigSend()
minCoin := std.Coin{"ugnot", minFee}
if inviter == "" {
// banker := std.GetBanker(std.BankerTypeOrigSend)
if len(sentCoins) == 1 && sentCoins[0].IsGTE(minCoin) {
if sentCoins[0].Amount > minFee*maxFeeMult {
panic("payment must not be greater than " + strconv.Itoa(int(minFee*maxFeeMult)))
} else {
// ok
}
} else {
panic("payment must not be less than " + strconv.Itoa(int(minFee)))
}
} else {
invitekey := inviter.String() + ":" + caller.String()
_, _, ok := invites.Get(invitekey)
if !ok {
panic("invalid invitation")
}
invites.Remove(invitekey)
}
// assert not already registered.
_, _, ok := name2User.Get(name)
if ok {
panic("name already registered")
}
_, _, ok = addr2User.Get(caller.String())
if ok {
panic("address already registered")
}
// assert name is valid.
if !reName.MatchString(name) {
panic("invalid name: " + name + " (must be at least 6 characters, lowercase alphanumeric with underscore)")
}
// remainder of fees go toward invites.
invites := int(0)
if len(sentCoins) == 1 {
if sentCoins[0].Denom == "ugnot" && sentCoins[0].Amount >= minFee {
invites = int(sentCoins[0].Amount / minFee)
if inviter == "" && invites > 0 {
invites -= 1
}
}
}
// register.
counter++
user := &User{
address: caller,
name: name,
profile: profile,
number: counter,
invites: invites,
inviter: inviter,
}
name2User, _ = name2User.Set(name, user)
addr2User, _ = addr2User.Set(caller.String(), user)
}
func Invite(invitee string) {
// assert CallTx call.
std.AssertOriginCall()
// get caller/inviter.
caller := std.GetCallerAt(2)
if caller != std.GetOrigCaller() {
panic("should not happen") // because std.AssertOrigCall().
}
lines := strings.Split(invitee, "\n")
if caller == admin {
// nothing to do, all good
} else {
// ensure has invites.
_, userI, ok := addr2User.Get(caller.String())
if !ok {
panic("user unknown")
}
user := userI.(*User)
if user.invites <= 0 {
panic("user has no invite tokens")
}
user.invites -= len(lines)
if user.invites < 0 {
panic("user has insufficient invite tokens")
}
}
// for each line...
for _, line := range lines {
if line == "" {
continue // file bodies have a trailing newline.
} else if strings.HasPrefix(line, `//`) {
continue // comment
}
// record invite.
invitekey := string(caller) + ":" + string(line)
invites, _ = invites.Set(invitekey, true)
}
}
func GrantInvites(invites string) {
// assert CallTx call.
std.AssertOriginCall()
// assert admin.
caller := std.GetCallerAt(2)
if caller != std.GetOrigCaller() {
panic("should not happen") // because std.AssertOrigCall().
}
if caller != admin {
panic("unauthorized")
}
// for each line...
lines := strings.Split(invites, "\n")
for _, line := range lines {
if line == "" {
continue // file bodies have a trailing newline.
} else if strings.HasPrefix(line, `//`) {
continue // comment
}
// parse name and invites.
var name string
var invites int
parts := strings.Split(line, ":")
if len(parts) == 1 { // short for :1.
name = parts[0]
invites = 1
} else if len(parts) == 2 {
name = parts[0]
invites_, err := strconv.Atoi(parts[1])
if err != nil {
panic(err)
}
invites = int(invites_)
} else {
panic("should not happen")
}
// give invites.
_, userI, ok := name2User.Get(name)
if !ok {
// maybe address.
_, userI, ok = addr2User.Get(name)
if !ok {
panic("invalid user " + name)
}
}
user := userI.(*User)
user.invites += invites
}
}
// Any leftover fees go toward invitations.
func SetMinFee(newMinFee int64) {
// assert CallTx call.
std.AssertOriginCall()
// assert admin caller.
caller := std.GetCallerAt(2)
if caller != admin {
panic("unauthorized")
}
// update global variables.
minFee = newMinFee
}
// This helps prevent fat finger accidents.
func SetMaxFeeMultiple(newMaxFeeMult int64) {
// assert CallTx call.
std.AssertOriginCall()
// assert admin caller.
caller := std.GetCallerAt(2)
if caller != admin {
panic("unauthorized")
}
// update global variables.
maxFeeMult = newMaxFeeMult
}
//----------------------------------------
// Exposed public functions
func GetUserByName(name string) *User {
_, userI, ok := name2User.Get(name)
if !ok {
return nil
}
return userI.(*User)
}
func GetUserByAddress(addr std.Address) *User {
_, userI, ok := addr2User.Get(addr.String())
if !ok {
return nil
}
return userI.(*User)
}
// unlike GetUserByName, input must be "@" prefixed for names.
func GetUserByAddressOrName(input AddressOrName) *User {
name, isName := input.GetName()
if isName {
return GetUserByName(name)
}
return GetUserByAddress(std.Address(input))
}
//----------------------------------------
// Constants
// NOTE: name length must be clearly distinguishable from a bech32 address.
var reName = regexp.MustCompile(`^[a-z]+[_a-z0-9]{5,16}$`)
//----------------------------------------
// Render main page
func Render(path string) string {
if path == "" {
return renderHome()
} else if len(path) >= 38 { // 39? 40?
if path[:2] != "g1" {
return "invalid address " + path
}
user := GetUserByAddress(std.Address(path))
if user == nil {
// TODO: display basic information about account.
return "unknown address " + path
}
return user.Render()
} else {
user := GetUserByName(path)
if user == nil {
return "unknown username " + path
}
return user.Render()
}
}
func renderHome() string {
doc := ""
name2User.Iterate("", "", func(t *avl.Tree) bool {
user := t.Value().(*User)
doc += " * [" + user.name + "](/r/users:" + user.name + ")\n"
return false
})
return doc
}