Skip to content

Commit d6ef163

Browse files
authored
feat(worker/near.social): support near.social transactions (#575)
1 parent 2c1be2d commit d6ef163

File tree

13 files changed

+751
-116
lines changed

13 files changed

+751
-116
lines changed

internal/engine/worker/decentralized/contract/linear/worker.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,8 @@ func (w *worker) Transform(ctx context.Context, task engine.Task) (*activityx.Ac
103103
}
104104

105105
activity.Actions = append(activity.Actions, actions...)
106+
} else {
107+
return nil, fmt.Errorf("no actions found in transaction")
106108
}
107109

108110
return activity, nil
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package nearsocial
2+
3+
type Config struct {
4+
// TODO Implement support for customizable configurations.
5+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package nearsocial
2+
3+
type FunctionCallArgs struct {
4+
Data map[string]UserContent `json:"data"`
5+
}
6+
7+
type UserContent struct {
8+
Index map[string]string `json:"index,omitempty"`
9+
Post map[string]string `json:"post,omitempty"`
10+
}
11+
12+
type IndexData struct {
13+
Key IndexKey `json:"key"`
14+
Value IndexValue `json:"value"`
15+
}
16+
17+
type RepostIndexData []struct {
18+
Key interface{} `json:"key"`
19+
Value struct {
20+
Type string `json:"type"`
21+
Item *struct {
22+
Type string `json:"type"`
23+
Path string `json:"path"`
24+
BlockHeight int64 `json:"blockHeight"`
25+
} `json:"item,omitempty"`
26+
} `json:"value"`
27+
}
28+
29+
type IndexKey struct {
30+
Type string `json:"type"`
31+
Path string `json:"path,omitempty"`
32+
BlockHeight int64 `json:"blockHeight,omitempty"`
33+
}
34+
35+
type IndexValue struct {
36+
Type string `json:"type"`
37+
Path string `json:"path,omitempty"`
38+
Item *Item `json:"item,omitempty"`
39+
}
40+
41+
type Item struct {
42+
Type string `json:"type"`
43+
Path string `json:"path"`
44+
BlockHeight int64 `json:"blockHeight,omitempty"`
45+
}
46+
47+
type PostData struct {
48+
Type string `json:"type"`
49+
Text string `json:"text,omitempty"`
50+
Item *Item `json:"item,omitempty"`
51+
}
52+
53+
type HashtagData struct {
54+
Key string `json:"key"`
55+
Value HashtagValue `json:"value"`
56+
}
57+
58+
type HashtagValue struct {
59+
Type string `json:"type"`
60+
Path string `json:"path"`
61+
}
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
package nearsocial
2+
3+
import (
4+
"context"
5+
"encoding/base64"
6+
"encoding/json"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/rss3-network/node/config"
11+
"github.com/rss3-network/node/internal/engine"
12+
source "github.com/rss3-network/node/internal/engine/source/near"
13+
"github.com/rss3-network/node/provider/near"
14+
workerx "github.com/rss3-network/node/schema/worker/decentralized"
15+
"github.com/rss3-network/protocol-go/schema"
16+
activityx "github.com/rss3-network/protocol-go/schema/activity"
17+
"github.com/rss3-network/protocol-go/schema/metadata"
18+
"github.com/rss3-network/protocol-go/schema/network"
19+
"github.com/rss3-network/protocol-go/schema/tag"
20+
"github.com/rss3-network/protocol-go/schema/typex"
21+
)
22+
23+
var _ engine.Worker = (*worker)(nil)
24+
25+
type worker struct {
26+
config *config.Module
27+
}
28+
29+
func (w *worker) Name() string {
30+
return workerx.NearSocial.String()
31+
}
32+
33+
func (w *worker) Platform() string {
34+
return workerx.PlatformNearSocial.String()
35+
}
36+
37+
func (w *worker) Network() []network.Network {
38+
return []network.Network{
39+
network.Near,
40+
}
41+
}
42+
43+
func (w *worker) Tags() []tag.Tag {
44+
return []tag.Tag{
45+
tag.Social,
46+
}
47+
}
48+
49+
func (w *worker) Types() []schema.Type {
50+
return []schema.Type{
51+
typex.SocialPost,
52+
typex.SocialComment,
53+
typex.SocialShare,
54+
}
55+
}
56+
57+
const (
58+
socialNearReceiverID = "social.near"
59+
)
60+
61+
func (w *worker) Filter() engine.DataSourceFilter {
62+
return &source.Filter{
63+
ReceiverIDs: []string{
64+
socialNearReceiverID,
65+
},
66+
}
67+
}
68+
69+
// Transform processes a Near task and returns an activity.
70+
// It is the main entry point for processing Near Social transactions.
71+
func (w *worker) Transform(ctx context.Context, task engine.Task) (*activityx.Activity, error) {
72+
// Cast the task to a Near task.
73+
nearTask, ok := task.(*source.Task)
74+
if !ok {
75+
return nil, fmt.Errorf("invalid task type: %T", task)
76+
}
77+
78+
// Build the activity with the platform information.
79+
activity, err := task.BuildActivity(activityx.WithActivityPlatform(w.Platform()))
80+
if err != nil {
81+
return nil, fmt.Errorf("build activity: %w", err)
82+
}
83+
84+
// Process the Near Social action in the transaction.
85+
action, err := w.handleNearSocialAction(ctx, nearTask, activity.Timestamp)
86+
if err != nil {
87+
return nil, fmt.Errorf("handle near social action: %w", err)
88+
}
89+
90+
if action != nil {
91+
activity.Type = action.Type
92+
activity.Actions = append(activity.Actions, action)
93+
} else {
94+
return nil, fmt.Errorf("no action found in transaction")
95+
}
96+
97+
return activity, nil
98+
}
99+
100+
// handleNearSocialAction processes the action in the Near Social transaction and returns an activityx.Action.
101+
// This function is responsible for identifying and processing the 'set' function call in the transaction.
102+
func (w *worker) handleNearSocialAction(_ context.Context, task *source.Task, timestamp uint64) (*activityx.Action, error) {
103+
for _, action := range task.Transaction.Transaction.Actions {
104+
if action.FunctionCall != nil && action.FunctionCall.MethodName == "set" {
105+
return w.processSetFunction(task.Transaction.Transaction.SignerID, action.FunctionCall, timestamp)
106+
}
107+
}
108+
109+
return nil, nil
110+
}
111+
112+
// processSetFunction handles the "set" function call and returns an activityx.Action.
113+
// This function decodes and processes the arguments of the 'set' function call to build a social action.
114+
func (w *worker) processSetFunction(signerID string, functionCall *near.FunctionCallAction, timestamp uint64) (*activityx.Action, error) {
115+
// Decode base64 args
116+
decodedArgs, err := base64.StdEncoding.DecodeString(functionCall.Args)
117+
if err != nil {
118+
return nil, fmt.Errorf("decode base64 args: %w", err)
119+
}
120+
121+
// Unmarshal decoded args into FunctionCallArgs struct
122+
var args FunctionCallArgs
123+
if err := json.Unmarshal(decodedArgs, &args); err != nil {
124+
return nil, fmt.Errorf("unmarshal function call args: %w", err)
125+
}
126+
127+
for _, userContent := range args.Data {
128+
if userContent.Post != nil {
129+
for path, postDataJSON := range userContent.Post {
130+
var postData PostData
131+
if err := json.Unmarshal([]byte(postDataJSON), &postData); err != nil {
132+
return nil, fmt.Errorf("unmarshal post data: %w", err)
133+
}
134+
135+
return w.buildSocialAction(signerID, path, postData, args, timestamp)
136+
}
137+
}
138+
139+
if userContent.Index != nil {
140+
return w.buildSocialAction(signerID, "", PostData{}, args, timestamp)
141+
}
142+
}
143+
144+
return nil, nil
145+
}
146+
147+
// buildSocialAction constructs an activityx.Action based on the provided social action data.
148+
// This function is crucial for creating the appropriate action type (post, comment, or share) and populating its metadata.
149+
func (w *worker) buildSocialAction(signerID, path string, postData PostData, args FunctionCallArgs, timestamp uint64) (*activityx.Action, error) {
150+
action := &activityx.Action{
151+
Type: typex.SocialPost,
152+
Platform: w.Platform(),
153+
From: signerID,
154+
To: signerID,
155+
Metadata: metadata.SocialPost{
156+
Handle: signerID,
157+
Body: postData.Text,
158+
Timestamp: timestamp,
159+
},
160+
}
161+
162+
if path == "comment" {
163+
action = w.handleComment(action, postData)
164+
}
165+
166+
if userContent, ok := args.Data[signerID]; ok {
167+
action = w.processUserContent(action, userContent, signerID, timestamp)
168+
}
169+
170+
return action, nil
171+
}
172+
173+
func (w *worker) handleComment(action *activityx.Action, postData PostData) *activityx.Action {
174+
action.Type = typex.SocialComment
175+
176+
if postData.Item != nil {
177+
target := &metadata.SocialPost{
178+
Handle: strings.Split(postData.Item.Path, "/")[0],
179+
PublicationID: fmt.Sprintf("%s-%d", postData.Item.Path, postData.Item.BlockHeight),
180+
}
181+
182+
if socialPost, ok := action.Metadata.(metadata.SocialPost); ok {
183+
socialPost.Target = target
184+
action.Metadata = socialPost
185+
}
186+
187+
action.To = target.Handle
188+
}
189+
190+
return action
191+
}
192+
193+
func (w *worker) processUserContent(action *activityx.Action, userContent UserContent, signerID string, timestamp uint64) *activityx.Action {
194+
action = w.processHashtags(action, userContent)
195+
action = w.processCommentIndex(action, userContent)
196+
action = w.processReposts(action, userContent, signerID, timestamp)
197+
198+
return action
199+
}
200+
201+
func (w *worker) processHashtags(action *activityx.Action, userContent UserContent) *activityx.Action {
202+
if hashtagJSON, ok := userContent.Index["hashtag"]; ok {
203+
var hashtags []HashtagData
204+
205+
if err := json.Unmarshal([]byte(hashtagJSON), &hashtags); err == nil {
206+
tags := make([]string, 0, len(hashtags))
207+
208+
for _, hashtag := range hashtags {
209+
tags = append(tags, hashtag.Key)
210+
}
211+
212+
if socialPost, ok := action.Metadata.(metadata.SocialPost); ok {
213+
socialPost.Tags = tags
214+
action.Metadata = socialPost
215+
}
216+
}
217+
}
218+
219+
return action
220+
}
221+
222+
func (w *worker) processCommentIndex(action *activityx.Action, userContent UserContent) *activityx.Action {
223+
if indexJSON, ok := userContent.Index["comment"]; ok {
224+
var indexData IndexData
225+
if err := json.Unmarshal([]byte(indexJSON), &indexData); err == nil {
226+
if socialPost, ok := action.Metadata.(metadata.SocialPost); ok {
227+
socialPost.PublicationID = fmt.Sprintf("%s-%d", indexData.Key.Path, indexData.Key.BlockHeight)
228+
action.Metadata = socialPost
229+
}
230+
}
231+
}
232+
233+
return action
234+
}
235+
236+
func (w *worker) processReposts(action *activityx.Action, userContent UserContent, signerID string, timestamp uint64) *activityx.Action {
237+
if repostJSON, ok := userContent.Index["repost"]; ok {
238+
var repostData RepostIndexData
239+
if err := json.Unmarshal([]byte(repostJSON), &repostData); err == nil {
240+
if len(repostData) > 0 {
241+
action.Type = typex.SocialShare
242+
243+
if repostData[0].Value.Item != nil {
244+
pathParts := strings.Split(repostData[0].Value.Item.Path, "/")
245+
if len(pathParts) >= 2 {
246+
target := &metadata.SocialPost{
247+
Handle: pathParts[0],
248+
PublicationID: fmt.Sprintf("%s-%d", repostData[0].Value.Item.Path, repostData[0].Value.Item.BlockHeight),
249+
}
250+
251+
action.Metadata = metadata.SocialPost{
252+
Handle: signerID,
253+
Timestamp: timestamp,
254+
Target: target,
255+
}
256+
action.To = target.Handle
257+
}
258+
}
259+
}
260+
}
261+
}
262+
263+
return action
264+
}
265+
266+
// NewWorker returns a new near social worker.
267+
func NewWorker(config *config.Module) (engine.Worker, error) {
268+
var instance = worker{
269+
config: config,
270+
}
271+
272+
return &instance, nil
273+
}

0 commit comments

Comments
 (0)