Smart Contract

SocialProfileV3

A.4bbff461fa8f6192.SocialProfileV3

Deployed

14h ago
Feb 28, 2026, 02:27:40 AM UTC

Dependents

0 imports
1// TODOs
2// * check events have enough information to be useful
3// * add FLOW balance checking
4// * Refactor Post to a simpler set of attributes, and put Newsfeed fields in a metadata field
5//      add pub let metadata: {String: [AnyStruct{FantastecSwapDataProperties.MetadataElement}]}
6/*
7TODO
8    Use MetadataElement for all metadata properties [done]
9    Reduce Post so that NewPost items are in metadata [done]
10    Make Comment a resource, or have a nextCommentId on the post
11    Add liker's address to commentLikes
12    Should there be a limit to the number of comments? as Address will get more and more
13    Should there be a limit to the number of likes? as Address will get more and more
14
15    Check Events parameters are sufficient
16
17    PostDetails to PostStruct, and use the same structure as with Post
18
19*/
20
21import FantastecSwapDataProperties from 0x4bbff461fa8f6192
22
23access(all) contract SocialProfileV3 {
24
25    access(all) entitlement Poster
26
27    access(all) event PostCreated           (owner: Address, postId: UInt64)
28    access(all) event PostDestroyed         (owner: Address, postId: UInt64)
29    access(all) event NewsFeedPostCreated   (owner: Address, postId: UInt64)
30    access(all) event PostLiked             (owner: Address, postId: UInt64, liker: Address)
31    access(all) event PostUnliked           (owner: Address, postId: UInt64, liker: Address)
32    access(all) event CommentCreated        (owner: Address, postId: UInt64, commenter: Address, commentId: UInt64)
33    access(all) event CommentDestroyed      (owner: Address, postId: UInt64, commenter: Address, commentId: UInt64)
34    access(all) event CommentLiked          (owner: Address, postId: UInt64, commentId: UInt64, liker: Address)
35    access(all) event CommentUnliked        (owner: Address, postId: UInt64, commentId: UInt64, liker: Address)
36
37    access(all) event ProfileFollowed       (owner: Address, follower: Address)
38    access(all) event ProfileUnfollowed     (owner: Address, follower: Address)
39    access(all) event ProfileUpdated        (owner: Address, field: String)
40
41    access(all) event Installed             (owner: Address)
42    access(all) event Destroyed             (owner: Address)
43
44    access(all) let SocialProfileStoragePath: StoragePath
45    access(all) let SocialProfilePublicPath:  PublicPath
46
47//    access(contract) let maxCommentsPerPost: UInt64
48//    access(contract) let maxPostsPerProfile: UInt64
49//    access(contract) let maxFollowingPerProfile: UInt64
50    access(contract) var nextCommentId: UInt64
51
52    access(all) struct PostDetails {
53        access(all) let id: UInt64
54        access(all) let author: Address // If we don't add author here, can it be inferred in some way on chain? perhaps by event history?
55        access(all) let content: String
56        access(all) let image: FantastecSwapDataProperties.Media?
57        access(all) let dateCreated: UFix64
58        access(all) let likeCount: UInt
59        access(all) let comments: {UInt64: Comment}
60        access(all) var metadata: {String: [{FantastecSwapDataProperties.MetadataElement}]}
61        // consider mirroring the minimal post and have the rest of the properties in metadata
62        init(
63            id: UInt64, 
64            author: Address,
65            content: String, 
66            image: FantastecSwapDataProperties.Media?, 
67            dateCreated: UFix64,
68            likeCount: UInt,
69            comments: {UInt64: Comment},
70            metadata: {String: [{FantastecSwapDataProperties.MetadataElement}]}
71            ) {
72            self.id = id
73            self.author = author
74            self.image = image
75            self.content = content
76            self.dateCreated = dateCreated
77            self.likeCount = likeCount
78            self.comments = comments
79            self.metadata = metadata
80        }
81    }
82
83    access(all) struct Comment {
84        access(all) let id: UInt64
85        access(all) let author: Address
86        access(all) let content: String
87        access(all) let dateCreated: UFix64
88        access(all) var likeCount: Int
89        access(all) var metadata: {String: [{FantastecSwapDataProperties.MetadataElement}]}
90
91        init(id: UInt64, author: Address, content: String) {
92            let dateCreated = getCurrentBlock().timestamp
93            self.id = id
94            self.author = author
95            self.content = content
96            self.dateCreated = dateCreated
97            self.likeCount = 0
98            self.metadata = {}
99        }
100    
101        access(contract) fun incrementLike() {
102            self.likeCount = self.likeCount + 1
103        }
104
105        access(contract) fun decrementLike() {
106            if self.likeCount == 0 {
107                panic("Cannot unlike as likeCount already 0")
108            }
109            self.likeCount = self.likeCount - 1
110        }
111    }
112
113    access(all) struct Profile {
114        access(all) let avatar: String
115        access(all) let username: String
116        access(all) let name: String
117        access(all) let bio: String
118        access(all) let coverMedia: FantastecSwapDataProperties.Media?
119        access(all) let following: {Address: Bool}
120        access(all) let followers : Int
121        access(all) var metadata: {String: [{FantastecSwapDataProperties.MetadataElement}]}
122        init(
123            avatar: String, 
124            bio: String, 
125            name: String, 
126            username: String, 
127            coverMedia: FantastecSwapDataProperties.Media?, 
128            following: {Address: Bool}, 
129            followers: Int,
130            metadata: {String: [{FantastecSwapDataProperties.MetadataElement}]}
131        ) {
132            self.avatar = avatar
133            self.bio = bio
134            self.name = name
135            self.username = username
136            self.coverMedia = coverMedia
137            self.following = following
138            self.followers = followers
139            self.metadata = metadata
140        }
141    }
142
143    access(all) resource Post {
144        access(all) let id: UInt64
145        access(all) let author: Address
146        access(all) let content: String
147        access(all) let image: FantastecSwapDataProperties.Media?
148        access(all) let dateCreated: UFix64
149        access(all) var likeCount: Int // should this be access(contract)? Can someone else change this value?
150        access(all) let comments: {UInt64: Comment}
151        access(all) var metadata: {String: [{FantastecSwapDataProperties.MetadataElement}]} // should this be access(contract)? Can someone else change this value?
152
153        init(content: String, author: Address, image: FantastecSwapDataProperties.Media?) {
154            self.id = self.uuid
155            self.image = image
156            self.author = author
157            self.content = content
158            self.dateCreated = getCurrentBlock().timestamp
159            self.likeCount = 0
160            self.comments = {}
161            self.metadata = {}
162        }
163
164        /* Post Likes */
165        access(contract) fun incrementLike() {
166            self.likeCount = self.likeCount + 1
167        }
168
169        access(contract) fun decrementLike() {
170            if self.likeCount == 0 {
171                panic("Cannot unlike as likeCount already 0")
172            }
173            self.likeCount = self.likeCount - 1
174        }
175
176        /* Comments */
177        access(contract) fun getComment(_ commentId: UInt64): Comment? {
178            let comment = self.comments[commentId]
179            return comment
180        }
181
182       access(contract) fun addComment(comment: Comment) {
183            let id = comment.id
184            self.comments[id] = comment
185        }
186
187        access(contract) fun removeComment(comment: Comment) {
188            let id = comment.id
189            self.comments.remove(key: id)
190        }
191        
192        /* Comment Likes */
193        access(contract) fun likeComment(_ commentId: UInt64) {
194            let comment = self.comments[commentId] ?? panic("Comment does not exist with that id")
195            comment.incrementLike()
196            self.comments[commentId] = comment
197        }
198
199        access(contract) fun unlikeComment(_ commentId: UInt64) {
200            let comment = self.comments[commentId] ?? panic("Comment does not exist with that id")
201            comment.decrementLike()
202            self.comments[commentId] = comment
203        }
204
205        /* Metadata */
206        access(contract) fun addMetadata(
207        _ type: String,
208        _ metadata: {FantastecSwapDataProperties.MetadataElement},
209        ) {
210            if (self.metadata[type] == nil) {
211                self.metadata[type] = []
212            }
213            self.metadata[type] = FantastecSwapDataProperties.addToMetadata(type, self.metadata[type]!, metadata)
214        }
215
216        access(contract) fun removeMetadata(
217        _ type: String,
218        _ id: UInt64?,
219        ) {
220            if (self.metadata[type] == nil) {
221                self.metadata[type] = []
222            }
223            self.metadata[type] = FantastecSwapDataProperties.removeFromMetadata(type, self.metadata[type]!, id)
224        }        
225    }
226
227    access(all) resource interface SocialProfilePublic {
228        access(all) fun borrowPost(_ id: UInt64): &Post?
229        access(all) fun getPostIds(): [UInt64]
230        access(all) fun getLikedPosts(): [UInt64]
231        access(all) fun getAvatar(): String
232        access(all) fun getBio(): String
233        access(all) fun getCoverMedia(): FantastecSwapDataProperties.Media?
234        access(all) fun getUsername(): String
235        access(all) fun getName(): String
236        access(all) fun getFollowing(): {Address:Bool}
237        access(all) fun getFollowersCount(): Int
238        access(all) fun getMetadata(): {String: [{FantastecSwapDataProperties.MetadataElement}]} // pub let metadata: {String: [AnyStruct{FantastecSwapDataProperties.MetadataElement}]}
239        access(contract) fun incrementFollower() // care to explain why it's like this? by access(contract) in public interface?
240        access(contract) fun decrementFollower() // this permits another SP to call someone else's SP, but (contract) permits only the contract to call it
241    }
242
243    access(all) resource interface SocialProfilePrivate {
244        access(Poster) fun addMetadata(_ type: String, _ metadata: {FantastecSwapDataProperties.MetadataElement})
245        access(Poster) fun createComment(theirAddress: Address, postId: UInt64, content: String)
246        access(Poster) fun createPost(content: String, image: FantastecSwapDataProperties.Media?)
247        access(Poster) fun createNewsFeedPost(content: String, title: String, publishedDate: UFix64, image: FantastecSwapDataProperties.Media?, buttonUrl: String, buttonText: String)
248        access(Poster) fun deleteComment(theirAddress: Address, postId: UInt64, commentId: UInt64)
249        access(Poster) fun follow(theirAddress: Address)
250        access(Poster) fun likeComment(theirAddress: Address, postId: UInt64, commentId: UInt64)
251        access(Poster) fun likePost(theirAddress: Address, id: UInt64)
252        access(Poster) fun removeMetadata(_ type: String, _ id: UInt64?)
253        access(Poster) fun removePost(_ id: UInt64)
254        access(Poster) fun setAvatar(avatar: String)
255        access(Poster) fun setBio(bio: String)
256        access(Poster) fun setName(name: String)
257        access(Poster) fun setUsername(username: String)
258        access(Poster) fun setCoverMedia(media: FantastecSwapDataProperties.Media?)
259        access(Poster) fun unfollow(theirAddress: Address)
260        access(Poster) fun unlikeComment(theirAddress: Address, postId: UInt64, commentId: UInt64)
261        access(Poster) fun unlikePost(theirAddress: Address, id: UInt64)
262    }
263
264    access(all) resource SocialProfile: SocialProfilePrivate, SocialProfilePublic {
265        access(self) var posts: @{UInt64: Post}
266        access(self) var likedPosts: {UInt64:Bool}
267        access(self) var likedComments: {UInt64: Bool}
268        access(self) var avatar: String
269        access(self) var bio: String
270        access(self) var name: String
271        access(self) var username: String
272        access(self) var coverMedia: FantastecSwapDataProperties.Media?
273        access(self) var followers: Int
274        access(self) var following: {Address: Bool}
275        access(self) var metadata: {String: [{FantastecSwapDataProperties.MetadataElement}]}
276
277        /* Profile Getters */
278        access(all) fun getAvatar(): String {
279            return self.avatar
280        }
281        access(all) fun getBio(): String {
282            return self.bio
283        }
284        access(all) fun getMetadata(): {String: [{FantastecSwapDataProperties.MetadataElement}]} {
285            return self.metadata
286        }
287        access(all) fun getCoverMedia(): FantastecSwapDataProperties.Media? {
288            return self.coverMedia
289        }
290        access(all) fun getUsername(): String {
291            return self.username
292        }
293        access(all) fun getName(): String {
294            return self.name
295        }
296
297        /* Profile Setters */
298        access(contract) fun emitUpdateEvent(_ field: String) {
299            emit ProfileUpdated(owner: self.owner!.address, field: field)
300        }
301        access(Poster) fun setAvatar(avatar: String) {
302            self.avatar = avatar
303            self.emitUpdateEvent("avatar")
304        }
305        access(Poster) fun setBio(bio: String) {
306            self.bio = bio
307            self.emitUpdateEvent("bio")
308        }
309        access(Poster) fun setUsername(username: String) {
310            self.username = username
311            self.emitUpdateEvent("username")
312        }
313        access(Poster) fun setCoverMedia(media: FantastecSwapDataProperties.Media?) {
314            self.coverMedia = media
315            self.emitUpdateEvent("coverMedia")
316        }
317        access(Poster) fun setName(name: String) {
318            self.name = name
319            self.emitUpdateEvent("name")
320        }
321
322        /* Follow */
323        access(all) fun getFollowing(): {Address:Bool} {
324            return self.following
325        }
326        access(all) fun getFollowersCount(): Int {
327            return self.followers
328        }
329        access(Poster) fun follow(theirAddress: Address) {
330            if self.following[theirAddress] == true  {
331                panic("You already follow this profile")
332            }
333
334            if self.owner!.address == theirAddress {
335                panic("You cannot follow your own profile")
336            }
337            
338            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
339            let theirAccount = socialProfileRef.incrementFollower()
340            
341            self.following[theirAddress] = true
342            emit ProfileFollowed(owner: self.owner!.address, follower: theirAddress)
343        }
344        access(Poster) fun unfollow(theirAddress: Address) {
345            if self.following[theirAddress] == nil {
346                panic("You can not unfollow as you do not follow profile")
347            } 
348            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
349            let theirAccount = socialProfileRef.decrementFollower()
350
351            self.following.remove(key: theirAddress)
352            emit ProfileUnfollowed(owner: self.owner!.address, follower: theirAddress)
353        }
354
355        /* Posts */
356        access(Poster) fun createPost(content: String, image: FantastecSwapDataProperties.Media?) {
357            let p <- create Post(
358                content: content,
359                author: self.owner!.address,
360                image: image
361            )
362            emit PostCreated(owner: self.owner!.address, postId: p.id)
363            self.posts[p.id] <-! p
364        }
365        access(Poster) fun createNewsFeedPost(content: String, title: String, publishedDate: UFix64, image: FantastecSwapDataProperties.Media?, buttonUrl: String, buttonText: String) {
366            let p <- create Post(
367                content: content,
368                author: self.owner!.address,
369                image: image
370            )
371            let metadataItemId: UInt64 = 1
372            let metadata = FantastecSwapDataProperties.NewsFeed(metadataItemId, title, publishedDate, buttonUrl, buttonText)
373            p.addMetadata("NewsFeed", metadata)
374            emit NewsFeedPostCreated(owner: self.owner!.address, postId: p.id)
375            let oldPost <- self.posts[p.id] <-! p
376            destroy oldPost
377        }
378        access(all) fun borrowPost(_ id: UInt64): &Post? {
379            return (&self.posts[id] as &Post?)
380        }
381        access(Poster) fun removePost(_ id: UInt64) {
382            let p <- self.posts.remove(key: id) ?? panic("Post with that id does not exist")
383            emit PostDestroyed(owner: self.owner!.address, postId: p.id)
384            destroy p
385        }
386        access(Poster) fun likePost(theirAddress: Address, id: UInt64) {
387            if self.likedPosts[id] == true {
388                panic("You already liked this post")
389            }
390            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
391            let theirPost = socialProfileRef.borrowPost(id) ?? panic("Post does not exist with that id")
392            theirPost.incrementLike()
393            self.likedPosts[id] = true
394            emit PostLiked(owner: theirAddress, postId: id, liker: self.owner!.address)
395        }
396        access(Poster) fun unlikePost(theirAddress: Address, id: UInt64) {
397            if self.likedPosts[id] == false ||  self.likedPosts[id] == nil {
398                panic("Post cannot be unliked as it was not previously liked")
399            }
400            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
401            let theirPost = socialProfileRef.borrowPost(id) ?? panic("Post does not exist with that id")
402            theirPost.decrementLike()
403            self.likedPosts.remove(key:id)
404            emit PostUnliked(owner: theirAddress, postId: id, liker: self.owner!.address)
405        }
406        access(all) fun getLikedPosts(): [UInt64] {
407            return self.likedPosts.keys
408        }
409        access(all) fun getPostIds(): [UInt64] {
410            return self.posts.keys
411        } 
412
413        /* Comments */
414        access(Poster) fun createComment(theirAddress: Address, postId: UInt64, content: String) {
415            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
416            let theirPost = socialProfileRef.borrowPost(postId) ?? panic("Post does not exist with that id")
417
418            let commentId = SocialProfileV3.nextCommentId
419            SocialProfileV3.nextCommentId = SocialProfileV3.nextCommentId + 1
420
421            let comment = Comment(id: commentId, author: self.owner!.address, content: content)
422            theirPost.addComment(comment: comment)
423
424            emit CommentCreated(owner: theirAddress, postId: theirPost.id, commenter: self.owner!.address, commentId: commentId)
425        }
426
427        access(Poster) fun deleteComment(theirAddress: Address, postId: UInt64, commentId: UInt64){
428            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
429            let theirPost = socialProfileRef.borrowPost(postId) ?? panic("Post does not exist with that id")
430            let _comment: Comment? = theirPost.getComment(commentId)
431            // check comment exists
432            if _comment == nil {
433                log("comment not found")
434                return 
435            }
436            let comment: Comment = _comment!
437            if comment.author != self.owner!.address {
438                panic("Comment was not created by you")
439            }
440            theirPost.removeComment(comment: comment)
441            emit CommentDestroyed(owner: theirAddress, postId: theirPost.id, commenter: self.owner!.address, commentId: commentId)
442        }
443
444        access(Poster) fun likeComment(theirAddress: Address, postId: UInt64, commentId: UInt64) {
445            if self.likedComments[commentId] == true {
446                panic("You already liked this comment")
447            }
448            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
449            let theirPost: &Post = socialProfileRef.borrowPost(postId) ?? panic("Post does not exist with that id")
450            // Call the method to like the comment within the post
451            theirPost.likeComment(commentId)
452            self.likedComments.insert(key: commentId, true)
453            emit CommentLiked(owner: theirAddress, postId: postId, commentId: commentId, liker: self.owner!.address)
454        }
455
456        access(Poster) fun unlikeComment(theirAddress: Address, postId: UInt64, commentId: UInt64) {
457             if self.likedComments[commentId] == nil {
458                panic("You havent liked this comment so you can not unlike")
459            }
460            let socialProfileRef = getAccount(theirAddress).capabilities.get<&{SocialProfileV3.SocialProfilePublic}>(SocialProfileV3.SocialProfilePublicPath).borrow()!
461            let theirPost = socialProfileRef.borrowPost(postId) ?? panic("Post does not exist with that id")
462            theirPost.unlikeComment(commentId)
463            self.likedComments.remove(key: commentId)
464            emit CommentUnliked(owner: theirAddress, postId: postId, commentId: commentId, liker: self.owner!.address)
465        }
466  
467        /* Internal Contract Mutators */
468        access(contract) fun incrementFollower() {
469           self.followers = self.followers + 1 
470        }
471
472        access(contract) fun decrementFollower() {
473            if self.followers > 0 {
474                self.followers = self.followers - 1
475            } else {
476                panic("Follower count cannot be less than zero")
477            }
478        }
479
480        /* Metadata */
481        access(Poster) fun addMetadata(
482        _ type: String,
483        _ metadata: {FantastecSwapDataProperties.MetadataElement},
484        ) {
485            if (self.metadata[type] == nil) {
486                self.metadata[type] = []
487            }
488            self.metadata[type] = FantastecSwapDataProperties.addToMetadata(type, self.metadata[type]!, metadata)
489            self.emitUpdateEvent("metadata add - ".concat(type))
490        }
491
492        access(Poster) fun removeMetadata(
493        _ type: String,
494        _ id: UInt64?,
495        ) {
496            if (self.metadata[type] == nil) {
497                self.metadata[type] = []
498            }
499            self.metadata[type] = FantastecSwapDataProperties.removeFromMetadata(type, self.metadata[type]!, id)
500            self.emitUpdateEvent("metadata remove - ".concat(type))
501        }
502        
503        access(all) fun emitInstalledEvent() {
504            emit Installed(owner: self.owner!.address)
505        }
506
507        access(all) fun emitDestroyedEvent(_ address: Address) {
508            emit Destroyed(owner: address)
509        }
510
511        init() {
512            self.posts <- {}
513            self.likedPosts = {}
514            self.likedComments = {}
515            self.avatar = ""
516            self.bio = ""
517            self.followers = 0
518            self.following = {}
519            self.metadata = {}
520            self.username = ""
521            self.name = ""
522            self.coverMedia = nil
523        }
524    }
525
526    access(all) fun createSocialProfile(): @SocialProfile {
527        return <-create SocialProfile()
528    }
529
530    init() {
531        self.SocialProfileStoragePath = /storage/SocialProfile 
532        self.SocialProfilePublicPath = /public/SocialProfile
533        self.nextCommentId = 1
534    }
535}
536