Smart Contract
SocialProfileV3
A.4bbff461fa8f6192.SocialProfileV3
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