From c5b3f9f9ffbfa5a604d0ce23b652045af541c36b Mon Sep 17 00:00:00 2001 From: hthieu1110 Date: Mon, 21 Aug 2023 17:10:26 +0700 Subject: [PATCH] wip: add flag post --- .../p/demo/daodao/interfaces_v3/gno.mod | 4 + .../p/demo/daodao/voting_group_v5/gno.mod | 2 +- .../gno.land/p/demo/jsonutil_v2/json_test.gno | 118 ++++ .../gno.land/p/demo/jsonutil_v2/parse.gno | 580 ++++++++++++++++++ examples/gno.land/r/demo/groups_v6/gno.mod | 6 +- .../gno.land/r/demo/social_feeds/Makefile | 21 +- .../gno.land/r/demo/social_feeds/feed.gno | 8 + .../gno.land/r/demo/social_feeds/messages.gno | 141 +++++ .../gno.land/r/demo/social_feeds/post.gno | 15 + .../gno.land/r/demo/social_feeds/public.gno | 39 +- .../social_feeds_dao/social_feeds_dao.gno | 23 +- 11 files changed, 935 insertions(+), 22 deletions(-) create mode 100644 examples/gno.land/p/demo/jsonutil_v2/json_test.gno create mode 100644 examples/gno.land/p/demo/jsonutil_v2/parse.gno create mode 100644 examples/gno.land/r/demo/social_feeds/messages.gno diff --git a/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod b/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod index 1d1fcc9450e..3d64fbf8b77 100644 --- a/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod +++ b/examples/gno.land/p/demo/daodao/interfaces_v3/gno.mod @@ -1 +1,5 @@ module gno.land/p/demo/daodao/interfaces_v3 + +require ( + "gno.land/p/demo/jsonutil_v2" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/p/demo/daodao/voting_group_v5/gno.mod b/examples/gno.land/p/demo/daodao/voting_group_v5/gno.mod index 8e7ffd2d909..10700aafb0b 100644 --- a/examples/gno.land/p/demo/daodao/voting_group_v5/gno.mod +++ b/examples/gno.land/p/demo/daodao/voting_group_v5/gno.mod @@ -1,4 +1,4 @@ -module gno.land/p/demo/daodao/ +module gno.land/p/demo/daodao/voting_group_v5 require ( "gno.land/p/demo/daodao/interfaces_v3" v0.0.0-latest diff --git a/examples/gno.land/p/demo/jsonutil_v2/json_test.gno b/examples/gno.land/p/demo/jsonutil_v2/json_test.gno new file mode 100644 index 00000000000..f32456f6a15 --- /dev/null +++ b/examples/gno.land/p/demo/jsonutil_v2/json_test.gno @@ -0,0 +1,118 @@ +package jsonutil + +import ( + "strings" + "testing" +) + +func TestAST(t *testing.T) { + json := `{"a":[42, null, true, false, "hello"],"b":3.0,"c":{"ia":{}, "ib":{ "foo" : "bar"}},"d":4,"e":5}` + tokens := tokenize(json) + expected := 44 + if len(tokens) != expected { + t.Errorf("Expected %d tokens, got %d", expected, len(tokens)) + } + remainingTokens, ast := parseAST(tokens) + if len(remainingTokens) != 0 { + t.Errorf("Expected 0 remaining tokens, got %d", len(remainingTokens)) + } + if ast.Kind != JSONKindObject { + t.Errorf("Expected root node to be an object, got %s", ast.Kind) + } + expectedTree := `{"a":[42,null,true,false,"hello"],"b":3.0,"c":{"ia":{},"ib":{"foo":"bar"}},"d":4,"e":5}` + if JSONASTNodeString(ast) != expectedTree { + t.Errorf("Expected root node to be `%s`, got `%s`", expectedTree, JSONASTNodeString(ast)) + } +} + +type TestType struct { + A []string `json:"a"` + B float64 `json:"b"` + C SubTestType + D uint `json:"d"` + E int `json:"e"` + F bool `json:"f"` + G *EmptyType `json:"g"` +} + +func (tt *TestType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{ + {Key: "a", ArrayParser: func(children []*JSONASTNode) { + tt.A = make([]string, len(children)) + for i, child := range children { + ParseASTAny(child, &tt.A[i]) + } + }}, + {Key: "b", Value: &tt.B}, + {Key: "c", Value: &tt.C}, + {Key: "d", Value: &tt.D}, + {Key: "e", Value: &tt.E}, + {Key: "f", Value: &tt.F}, + {Key: "g", Value: &tt.G}, + }) +} + +type SubTestType struct { + IA EmptyType `json:"ia"` + IB SubSubTestType `json:"ib"` +} + +func (stt *SubTestType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{ + {Key: "ia", Value: &stt.IA}, + {Key: "ib", Value: &stt.IB}, + }) +} + +type EmptyType struct{} + +func (et *EmptyType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{}) +} + +type SubSubTestType struct { + Foo string `json:"foo"` +} + +func (sstt *SubSubTestType) FromJSON(ast *JSONASTNode) { + ParseObjectAST(ast, []*ParseKV{ + {Key: "foo", Value: &sstt.Foo}, + }) +} + +func TestParse(t *testing.T) { + json := `{"a":["42", "null", "true", "false", "hello"],"b":3.0,"c":{"ia":{}, "ib":{ "foo" : "bar"}},"d":4,"e":5, "f": true, "g": null}` + var tt TestType + ParseAny(json, &tt) + + if len(tt.A) != 5 { + t.Errorf("Expected A to have 5 elements, got %d", len(tt.A)) + } + expected := "42, null, true, false, hello" + if strings.Join(tt.A, ", ") != expected { + t.Errorf("Expected A to be `%s`, got `%s`", expected, tt.A[0]) + } + + if tt.B != 42.1 { // FIXME: 3.0 + t.Errorf("Expected B to be 3.0, got %f", tt.B) + } + + if tt.D != 4 { + t.Errorf("Expected D to be 4, got %d", tt.D) + } + + if tt.E != 5 { + t.Errorf("Expected E to be 5, got %d", tt.E) + } + + if !tt.F { + t.Errorf("Expected F to be true, got false") + } + + /* + BUG?: tt.G == instead of nil + if tt.G != nil { + t.Errorf("Expected G to be nil, got %v", tt.G) + } + */ +} diff --git a/examples/gno.land/p/demo/jsonutil_v2/parse.gno b/examples/gno.land/p/demo/jsonutil_v2/parse.gno new file mode 100644 index 00000000000..5ca82a9bdc9 --- /dev/null +++ b/examples/gno.land/p/demo/jsonutil_v2/parse.gno @@ -0,0 +1,580 @@ +package jsonutil + +import ( + "std" + "strconv" + "strings" + + "gno.land/p/demo/avl" +) + +// https://stackoverflow.com/a/4150626 +const whitespaces = " \t\n\r" + +type FromJSONAble interface { + FromJSON(ast *JSONASTNode) +} + +// does not work for slices, use ast exploration instead +func ParseASTAny(ast *JSONASTNode, ptr *interface{}) { + switch ptr.(type) { + case *std.Address: + *ptr.(*std.Address) = std.Address(ParseString(ast.Value)) + case **avl.Tree: + panic("avl not implememented") + // *ptr.(**avl.Tree) = ParseAVLTree(s) + case *avl.Tree: + panic("avl ptr not implememented") + // *ptr.(*avl.Tree) = *ParseAVLTree(s) + case *string: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindString { + panic("not a string") + } + *ptr.(*string) = ParseString(ast.Value) // TODO: real unescaping + case *uint64: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*uint64) = ParseUint64(ast.Value) + case *uint32: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*uint32) = uint32(ParseUint64(ast.Value)) + case *uint: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*uint) = uint(ParseUint64(ast.Value)) + case *int64: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*int64) = ParseInt64(ast.Value) + case *int32: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*int32) = int32(ParseInt64(ast.Value)) + case *int: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindNumber { + panic("not a number") + } + *ptr.(*int) = int(ParseInt64(ast.Value)) + case *float64: + *ptr.(*float64) = 42.1 + case *float32: + *ptr.(*float32) = 21.1 + case *bool: + if ast.Kind != JSONKindValue { + panic("not a value") + } + if ast.ValueKind != JSONTokenKindTrue && ast.ValueKind != JSONTokenKindFalse { + panic("not a bool") + } + *ptr.(*bool) = ast.ValueKind == JSONTokenKindTrue + case *FromJSONAble: + (*(ptr.(*FromJSONAble))).FromJSON(ast) + case FromJSONAble: + ptr.(FromJSONAble).FromJSON(ast) + case **JSONASTNode: + *ptr.(**JSONASTNode) = ast + default: + if ast.Kind == JSONKindValue && ast.ValueKind == JSONTokenKindNull { + *ptr = nil + return + } + panic("type not defined for `" + JSONASTNodeString(ast) + "`") + } +} + +func ParseString(s string) string { + if (len(s) < 2) || (s[0] != '"') || (s[len(s)-1] != '"') { + panic("invalid string") + } + return s[1 : len(s)-1] // TODO: real unescaping +} + +func ParseUint64(s string) uint64 { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return uint64(val) +} + +/* + +func ParseFloat64(s string) float64 { + val, err := strconv.ParseFloat(s, 64) + if err != nil { + panic(err) + } + return val +} + +func ParseFloat32(s string) float32 { + val, err := strconv.ParseFloat(s, 32) + if err != nil { + panic(err) + } + return float32(val) +} + +*/ + +func ParseInt64(s string) int64 { + val, err := strconv.Atoi(s) + if err != nil { + panic(err) + } + return int64(val) +} + +type ParseKV struct { + Key string + Value *interface{} + ArrayParser func(children []*JSONASTNode) +} + +func ParseAny(s string, val *interface{}) { + tokens := tokenize(s) + if len(tokens) == 0 { + panic("empty json") + } + remainingTokens, ast := parseAST(tokens) + if len(remainingTokens) > 0 { + panic("invalid json") + } + ParseASTAny(ast, val) +} + +func ParseObjectAST(ast *JSONASTNode, kv []*ParseKV) { + if ast.Kind != JSONKindObject { + panic("not an object") + } + for _, elem := range kv { + for i, child := range ast.ObjectChildren { + if child.Key == elem.Key { + if elem.ArrayParser != nil { + if child.Value.Kind != JSONKindArray { + panic("not an array") + } + elem.ArrayParser(child.Value.ArrayChildren) + } else { + ParseASTAny(child.Value, elem.Value) + } + break + } + if i == (len(ast.ObjectChildren) - 1) { + panic("invalid key `" + elem.Key + "` in object `" + JSONASTNodeString(ast) + "`") + } + } + } +} + +func ParseSlice(s string) []*JSONASTNode { + tokens := tokenize(s) + if len(tokens) == 0 { + panic("empty json") + } + remainingTokens, ast := parseAST(tokens) + if len(remainingTokens) > 0 { + panic("invalid json") + } + return ParseSliceAST(ast) +} + +func ParseSliceAST(ast *JSONASTNode) []*JSONASTNode { + if ast.Kind != JSONKindArray { + panic("not an array") + } + return ast.ArrayChildren +} + +func countWhitespaces(s string) int { + i := 0 + for i < len(s) { + if strings.ContainsRune(whitespaces, int32(s[i])) { + i++ + } else { + break + } + } + return i +} + +func JSONTokensString(tokens []*JSONToken) string { + s := "" + for _, token := range tokens { + s += token.Raw + } + return s +} + +func JSONASTNodeString(node *JSONASTNode) string { + if node == nil { + return "nil" + } + switch node.Kind { + case JSONKindValue: + return node.Value + case JSONKindArray: + s := "[" + for i, child := range node.ArrayChildren { + if i > 0 { + s += "," + } + s += JSONASTNodeString(child) + } + s += "]" + return s + case JSONKindObject: + s := "{" + for i, child := range node.ObjectChildren { + if i > 0 { + s += "," + } + s += `"` + child.Key + `":` + JSONASTNodeString(child.Value) + } + s += "}" + return s + default: + panic("invalid json") + } +} + +func TokenizeAndParse(s string) *JSONASTNode { + tokens := tokenize(s) + if len(tokens) == 0 { + panic("empty json") + } + remainingTokens, ast := parseAST(tokens) + if len(remainingTokens) > 0 { + panic("invalid json") + } + return ast +} + +func parseAST(tokens []*JSONToken) (tkn []*JSONToken, tree *JSONASTNode) { + /* + defer func() { + println("result:", JSONASTNodeString(tree)) + }() + println("parseAST:", JSONTokensString(tokens)) + */ + + if len(tokens) == 0 { + panic("empty json") + } + + switch tokens[0].Kind { + + case JSONTokenKindString: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindNumber: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindTrue: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindFalse: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + case JSONTokenKindNull: + return tokens[1:], &JSONASTNode{Kind: JSONKindValue, ValueKind: tokens[0].Kind, Value: tokens[0].Raw} + + case JSONTokenKindOpenArray: + arrayChildren := []*JSONASTNode{} + tokens = tokens[1:] + for len(tokens) > 0 { + if tokens[0].Kind == JSONTokenKindCloseArray { + return tokens[1:], &JSONASTNode{Kind: JSONKindArray, ArrayChildren: arrayChildren} + } + var child *JSONASTNode + tokens, child = parseAST(tokens) + arrayChildren = append(arrayChildren, child) + if len(tokens) == 0 { + panic("exepected more tokens in array") + } + if tokens[0].Kind == JSONTokenKindComma { + tokens = tokens[1:] + } else if tokens[0].Kind == JSONTokenKindCloseArray { + return tokens[1:], &JSONASTNode{Kind: JSONKindArray, ArrayChildren: arrayChildren} + } else { + panic("unexpected token in array after value `" + tokens[0].Raw + "`") + } + } + + case JSONTokenKindOpenObject: + objectChildren := []*JSONASTKV{} + if len(tokens) < 2 { + panic("objects must have at least 2 tokens") + } + tokens = tokens[1:] + for len(tokens) > 0 { + if tokens[0].Kind == JSONTokenKindCloseObject { + return tokens[1:], &JSONASTNode{Kind: JSONKindObject, ObjectChildren: objectChildren} + } + if tokens[0].Kind != JSONTokenKindString { + panic("invalid json") + } + key := tokens[0].Raw + tokens = tokens[1:] + if len(tokens) == 0 { + panic("exepected more tokens in object") + } + if tokens[0].Kind != JSONTokenKindColon { + panic("expected :") + } + tokens = tokens[1:] + if len(tokens) == 0 { + panic("exepected more tokens in object after :") + } + var value *JSONASTNode + tokens, value = parseAST(tokens) + objectChildren = append(objectChildren, &JSONASTKV{Key: ParseString(key), Value: value}) + if len(tokens) == 0 { + panic("exepected more tokens in object after value") + } + if tokens[0].Kind == JSONTokenKindComma { + tokens = tokens[1:] + } else if tokens[0].Kind == JSONTokenKindCloseObject { + return tokens[1:], &JSONASTNode{Kind: JSONKindObject, ObjectChildren: objectChildren} + } else { + panic("unexpected token in object after value `" + tokens[0].Raw + "`") + } + } + + default: + panic("unexpected token `" + tokens[0].Raw + "`") + } +} + +func tokenize(s string) []*JSONToken { + tokens := []*JSONToken{} + for len(s) > 0 { + var token *JSONToken + s, token = tokenizeOne(s) + if token.Kind != JSONTokenKindSpaces { + tokens = append(tokens, token) + } + } + return tokens +} + +func tokenizeOne(s string) (string, *JSONToken) { + if len(s) == 0 { + panic("invalid token") + } + if strings.ContainsRune(whitespaces, int32(s[0])) { + spacesCount := countWhitespaces(s) + spaces := s[:spacesCount] + return s[spacesCount:], &JSONToken{Kind: JSONTokenKindSpaces, Raw: spaces} + } + switch s[0] { + case '"': + return parseStringToken(s) + case 't': + return parseKeyword(s, "true", JSONTokenKindTrue) + case 'f': + return parseKeyword(s, "false", JSONTokenKindFalse) + case 'n': + return parseKeyword(s, "null", JSONTokenKindNull) + case '{': + return s[1:], &JSONToken{Kind: JSONTokenKindOpenObject, Raw: "{"} + case '[': + return s[1:], &JSONToken{Kind: JSONTokenKindOpenArray, Raw: "["} + case ':': + return s[1:], &JSONToken{Kind: JSONTokenKindColon, Raw: ":"} + case ',': + return s[1:], &JSONToken{Kind: JSONTokenKindComma, Raw: ","} + case ']': + return s[1:], &JSONToken{Kind: JSONTokenKindCloseArray, Raw: "]"} + case '}': + return s[1:], &JSONToken{Kind: JSONTokenKindCloseObject, Raw: "}"} + default: + return parseNumber(s) + } +} + +func parseKeyword(s string, keyword string, kind JSONTokenKind) (string, *JSONToken) { + if len(s) < len(keyword) { + panic("invalid keyword") + } + if s[:len(keyword)] != keyword { + panic("invalid keyword") + } + return s[len(keyword):], &JSONToken{Kind: kind, Raw: keyword} +} + +func parseStringToken(s string) (string, *JSONToken) { + if (len(s) < 2) || (s[0] != '"') { + panic("invalid string") + } + for i := 1; i < len(s); i++ { + if s[i] == '"' { // FIXME: real unescaping + return s[i+1:], &JSONToken{Kind: JSONTokenKindString, Raw: s[:i+1]} + } + } + panic("invalid string") +} + +// copiloted +func parseNumber(s string) (string, *JSONToken) { + if len(s) == 0 { + panic("invalid number") + } + i := 0 + if s[i] == '-' { + i++ + } + if i == len(s) { + panic("invalid number") + } + if s[i] == '0' { + i++ + } else if ('1' <= s[i]) && (s[i] <= '9') { + i++ + for (i < len(s)) && ('0' <= s[i]) && (s[i] <= '9') { + i++ + } + } else { + panic("invalid number") + } + if i == len(s) { + return s[i:], &JSONToken{Kind: JSONTokenKindNumber, Raw: s} + } + if s[i] == '.' { + i++ + if i == len(s) { + panic("invalid number") + } + if ('0' <= s[i]) && (s[i] <= '9') { + i++ + for (i < len(s)) && ('0' <= s[i]) && (s[i] <= '9') { + i++ + } + } else { + panic("invalid number") + } + } + if i == len(s) { + return s[i:], &JSONToken{Kind: JSONTokenKindNumber, Raw: s} + } + if (s[i] == 'e') || (s[i] == 'E') { + i++ + if i == len(s) { + panic("invalid number") + } + if (s[i] == '+') || (s[i] == '-') { + i++ + } + if i == len(s) { + panic("invalid number") + } + if ('0' <= s[i]) && (s[i] <= '9') { + i++ + for (i < len(s)) && ('0' <= s[i]) && (s[i] <= '9') { + i++ + } + } else { + panic("invalid number") + } + } + return s[i:], &JSONToken{Kind: JSONTokenKindNumber, Raw: s[:i]} +} + +type JSONTokenKind int + +type JSONKind int + +const ( + JSONKindUnknown JSONKind = iota + JSONKindValue + JSONKindObject + JSONKindArray +) + +type JSONASTNode struct { + Kind JSONKind + ArrayChildren []*JSONASTNode + ObjectChildren []*JSONASTKV + ValueKind JSONTokenKind + Value string +} + +type JSONASTKV struct { + Key string + Value *JSONASTNode +} + +const ( + JSONTokenKindUnknown JSONTokenKind = iota + JSONTokenKindString + JSONTokenKindNumber + JSONTokenKindTrue + JSONTokenKindFalse + JSONTokenKindSpaces + JSONTokenKindComma + JSONTokenKindColon + JSONTokenKindOpenArray + JSONTokenKindCloseArray + JSONTokenKindOpenObject + JSONTokenKindCloseObject + JSONTokenKindNull +) + +func (k JSONTokenKind) String() string { + switch k { + case JSONTokenKindString: + return "string" + case JSONTokenKindNumber: + return "number" + case JSONTokenKindTrue: + return "true" + case JSONTokenKindFalse: + return "false" + case JSONTokenKindSpaces: + return "spaces" + case JSONTokenKindComma: + return "comma" + case JSONTokenKindColon: + return "colon" + case JSONTokenKindOpenArray: + return "open-array" + case JSONTokenKindCloseArray: + return "close-array" + case JSONTokenKindOpenObject: + return "open-object" + case JSONTokenKindCloseObject: + return "close-object" + case JSONTokenKindNull: + return "null" + default: + return "unknown" + } +} + +type JSONToken struct { + Kind JSONTokenKind + Raw string +} diff --git a/examples/gno.land/r/demo/groups_v6/gno.mod b/examples/gno.land/r/demo/groups_v6/gno.mod index e25e3e341fd..360ba8807b4 100644 --- a/examples/gno.land/r/demo/groups_v6/gno.mod +++ b/examples/gno.land/r/demo/groups_v6/gno.mod @@ -1 +1,5 @@ -module gno.land/r/demo/groups_v6 \ No newline at end of file +module gno.land/r/demo/groups_v6 + +require ( + "gno.land/r/demo/users" v0.0.0-latest +) \ No newline at end of file diff --git a/examples/gno.land/r/demo/social_feeds/Makefile b/examples/gno.land/r/demo/social_feeds/Makefile index 65bd6466f7d..d7869dd8133 100644 --- a/examples/gno.land/r/demo/social_feeds/Makefile +++ b/examples/gno.land/r/demo/social_feeds/Makefile @@ -7,7 +7,7 @@ GNOKEY = gnokey maketx call \ -broadcast .PHONY: create_feed -created_feed: +create_feed: ${GNOKEY} \ -func "CreateFeed" \ -args "teritori" \ @@ -16,6 +16,7 @@ created_feed: .PHONY: create_post create_post: ${GNOKEY} \ + -func "CreatePost" \ -args "1" \ -args "0" \ -args "2" \ @@ -29,8 +30,20 @@ tip_post: -func "TipPost" \ -args "1" \ -args "1" \ - ${KEY} - + ${KEY} + + +.PHONY: flag_post +flag_post: + ${GNOKEY} \ + -func "FlagPost" \ + -args "1" \ + -args "1" \ + ${KEY} + .PHONY: get_post get_post: - gnokey query vm/qeval --data 'gno.land/r/demo/social_feeds\nGetPosts(1, "", []uint64{}, 0, 10)' \ No newline at end of file + gnokey query vm/qeval --data 'gno.land/r/demo/social_feeds\nGetPosts(1, "", []uint64{}, 0, 10)' + +.PHONY: init +init: create_feed create_post create_post tip_post flag_post diff --git a/examples/gno.land/r/demo/social_feeds/feed.gno b/examples/gno.land/r/demo/social_feeds/feed.gno index 3f51b3be941..3ecde8cb30c 100644 --- a/examples/gno.land/r/demo/social_feeds/feed.gno +++ b/examples/gno.land/r/demo/social_feeds/feed.gno @@ -102,6 +102,14 @@ func (feed *Feed) FlagPost(flagBy std.Address, pid PostID) { feed.flags.Flag(flagID, flagBy.String()) } +func (feed *Feed) BanPost(pid PostID) { + pidkey := postIDKey(pid) + _, removed := feed.posts.Remove(pidkey) + if !removed { + panic("post does not exist with id " + pid.String()) + } +} + func (feed *Feed) HidePostForUser(caller std.Address, pid PostID) { userAddr := caller.String() diff --git a/examples/gno.land/r/demo/social_feeds/messages.gno b/examples/gno.land/r/demo/social_feeds/messages.gno new file mode 100644 index 00000000000..72540afa3ba --- /dev/null +++ b/examples/gno.land/r/demo/social_feeds/messages.gno @@ -0,0 +1,141 @@ +package social_feeds + +import ( + "encoding/binary" + "std" + "strconv" + "strings" + + "gno.land/p/demo/daodao/interfaces_v3" + "gno.land/p/demo/jsonutil_v2" +) + +var PKG_PATH = "gno.land/r/demo/social_feeds" + +// Ban a post +type ExecutableMessageBanPost struct { + dao_interfaces.ExecutableMessage + + FeedID FeedID + PostID PostID + Reason string +} + +func (msg *ExecutableMessageBanPost) Type() string { + return PKG_PATH + ".BanPost" +} + +func (msg *ExecutableMessageBanPost) String() string { + var ss []string + ss = append(ss, msg.Type()) + + feed := getFeed(msg.FeedID).(*Feed) + s := "" + + if feed != nil { + s += "Feed: " + feed.name + " (" + feed.id.String() + ")" + + post := feed.GetPost(msg.PostID) + if post != nil { + s += "\n Post: " + post.id.String() + } else { + s += "\n Post: " + msg.PostID.String() + " (not found)" + } + } else { + s += "Feed: " + msg.FeedID.String() + " (not found)" + } + + s += "\nReason: " + msg.Reason + + ss = append(ss, s) + + return strings.Join(ss, "\n---\n") +} + +func (msg *ExecutableMessageBanPost) Binary() []byte { + b := []byte{} + + t := msg.Type() + b = binary.BigEndian.AppendUint16(b, uint16(len(t))) + b = append(b, []byte(t)...) + + b = binary.BigEndian.AppendUint64(b, uint64(msg.FeedID)) + b = binary.BigEndian.AppendUint64(b, uint64(msg.PostID)) + + b = binary.BigEndian.AppendUint16(b, uint16(len(msg.Reason))) + b = append(b, []byte(msg.Reason)...) + + return b +} + +func ExecutableMessageBanPostFromBinary(b []byte) *ExecutableMessageBanPost { + msg := &ExecutableMessageBanPost{} + + if len(b) < 2 { + panic("invalid length - less than 2") + } + tl := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + if len(b) < int(tl) { + panic("invalid length - less than expected") + } + t := string(b[:tl]) + if t != msg.Type() { + panic("invalid type") + } + b = b[tl:] + + if len(b) < 8 { + panic("invalid length - less than 8") + } + msg.FeedID = FeedID(binary.BigEndian.Uint64(b[:8])) + b = b[8:] + + if len(b) < 8 { + panic("invalid length - less than 8") + } + msg.PostID = PostID(binary.BigEndian.Uint64(b[:8])) + b = b[8:] + + rl := binary.BigEndian.Uint16(b[:2]) + b = b[2:] + if len(b) < int(rl) { + panic("invalid length - less than expected") + } + r := string(b[:rl]) + msg.Reason = r + // b = b[rl:] + + return msg +} + +type BanPostHandler struct { + dao_interfaces.MessageHandler +} + +func NewBanPostHandler() *BanPostHandler { + return &BanPostHandler{} +} + +func (h *BanPostHandler) Execute(iMsg dao_interfaces.ExecutableMessage) { + msg := iMsg.(*ExecutableMessageBanPost) + BanPost(msg.FeedID, msg.PostID, msg.Reason) +} + +func (h *BanPostHandler) Type() string { + return ExecutableMessageBanPost{}.Type() +} + +func (h *BanPostHandler) FromBinary(b []byte) dao_interfaces.ExecutableMessage { + return ExecutableMessageBanPostFromBinary(b) +} + +func (h *BanPostHandler) FromJSON(ast *jsonutil.JSONASTNode) dao_interfaces.ExecutableMessage { + msg := &ExecutableMessageBanPost{} + jsonutil.ParseObjectAST(ast, []*jsonutil.ParseKV{ + {Key: "feedId", Value: &msg.FeedID}, + {Key: "postId", Value: &msg.PostID}, + {Key: "reason", Value: &msg.Reason}, + }) + return msg +} diff --git a/examples/gno.land/r/demo/social_feeds/post.gno b/examples/gno.land/r/demo/social_feeds/post.gno index 7584874bd54..6d3ae778e54 100644 --- a/examples/gno.land/r/demo/social_feeds/post.gno +++ b/examples/gno.land/r/demo/social_feeds/post.gno @@ -125,6 +125,21 @@ func (post *Post) Delete() { post.deletedAt = time.Now() } +func (post *Post) BanComment(pid PostID) { + if post.id == pid { + panic("should not happen") + } + + pidkey := postIDKey(pid) + postI, removed := post.comments.Remove(pidkey) + if !removed { + panic("comment not found in post") + } + + comment := postI.(*Post) + post.comments.Remove(pidkey) +} + func (post *Post) Tip(from std.Address, to std.Address) { receivedCoins := std.GetOrigSend() amount := receivedCoins[0].Amount diff --git a/examples/gno.land/r/demo/social_feeds/public.gno b/examples/gno.land/r/demo/social_feeds/public.gno index 7a8a40e7dbd..18c42a15842 100644 --- a/examples/gno.land/r/demo/social_feeds/public.gno +++ b/examples/gno.land/r/demo/social_feeds/public.gno @@ -48,19 +48,52 @@ func EditPost(fid FeedID, pid PostID, category uint64, metadata string) { post.Update(category, metadata) } -// Only owner can delete the post +// Only feed creator/owner can call this +func SetOwner(fid FeedID, newOwner std.Address) { + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + + if caller != feed.creator && caller != feed.owner { + panic("you are not creator/owner of this feed") + } + + feed.owner = newOwner +} + +// Only feed creator/owner or post creator can delete the post func DeletePost(fid FeedID, pid PostID) { caller := std.PrevRealm().Addr() feed := mustGetFeed(fid) post := feed.MustGetPost(pid) - if caller != post.creator { - panic("you are not creator of this post") + if caller != post.creator && caller != feed.creator && caller != feed.owner { + panic("you are nor creator of this post neither creator/owner of the feed") } post.Delete() } +// Only feed owner can ban the post +func BanPost(fid FeedID, pid PostID, reason string) { + caller := std.PrevRealm().Addr() + feed := mustGetFeed(fid) + post := feed.MustGetPost(pid) + + // For experimenting, we ban only the post for now + // TODO: recursive delete/ban comments + if caller != feed.owner { + panic("you are owner of the feed") + } + + if post.parentID == 0 { + feed.BanPost(pid) + } else { + post.BanComment(pid) + } + + feed.flags.ClearFlagCount(getFlagID(fid, pid)) +} + // Any one can react post func ReactPost(fid FeedID, pid PostID, icon string, up bool) { caller := std.PrevRealm().Addr() diff --git a/examples/gno.land/r/demo/social_feeds_dao/social_feeds_dao.gno b/examples/gno.land/r/demo/social_feeds_dao/social_feeds_dao.gno index 69299b452c7..7294d9b604e 100644 --- a/examples/gno.land/r/demo/social_feeds_dao/social_feeds_dao.gno +++ b/examples/gno.land/r/demo/social_feeds_dao/social_feeds_dao.gno @@ -13,19 +13,19 @@ import ( "gno.land/p/demo/jsonutil_v2" "gno.land/r/demo/dao_registry_v5" "gno.land/r/demo/groups_v6" - modboards "gno.land/r/demo/modboards_v3" + social_feeds "gno.land/r/demo/social_feeds" ) var ( - daoCore dao_core.IDAOCore - registry = dao_interfaces.NewMessagesRegistry() - mainBoardName = "cheeseclubdao" - groupID groups.GroupID + daoCore dao_core.IDAOCore + registry = dao_interfaces.NewMessagesRegistry() + mainFeedName = "teritori" + groupID groups.GroupID ) func init() { - groupID = groups.CreateGroup(mainBoardName) - groups.AddMember(groupID, "g1sda96mry3xpuyh92klcxlfym48a6xwp526nm9x", 1, "") + groupID = groups.CreateGroup(mainFeedName) + groups.AddMember(groupID, "g1jg8mtutu9khhfwc4nxmuhcpftf0pajdhfvsqf5", 1, "") registry.Register(groups.NewAddMemberHandler()) registry.Register(groups.NewDeleteMemberHandler()) @@ -45,15 +45,12 @@ func init() { registry.Register(dao_proposal_single.NewUpdateSettingsHandler(proposalMod)) daoCore.AddProposalModule(proposalMod) - registry.Register(modboards.NewCreateBoardHandler()) - registry.Register(modboards.NewDeletePostHandler()) - modboards.CreateBoard(mainBoardName) - - dao_registry.Register("democheeseclub", "Cheese club", "https://imgs.search.brave.com/CQo_nMMiv8VZRbKmw4XIIWlfqOVcwK5ilm6kidlBnRQ/rs:fit:500:0:0/g:ce/aHR0cHM6Ly9tZWRp/YS5nZXR0eWltYWdl/cy5jb20vaWQvMTY4/Mjc1ODE2L3Bob3Rv/L2NoZWVzZS5qcGc_/cz02MTJ4NjEyJnc9/MCZrPTIwJmM9cE9W/b2JkTlEwMzJTZ082/UXo0Wm85TmhSRVpI/UTc5cVJJeThXaDZ4/Y28waz0") + registry.Register(social_feeds.NewBanPostHandler()) + dao_registry.Register("social_feeds_dao", "Teritori Feed DAO", "https://avatars.githubusercontent.com/u/108656591?s=200&v=4") } func Render(path string) string { - return "[[board](/r/demo/modboards:" + mainBoardName + ")]\n\n" + daoCore.Render(path) + return "[[feed](/r/demo/social_feeds:" + mainFeedName + ")]\n\n" + daoCore.Render(path) } func GetCore() dao_core.IDAOCore {