1 /** 2 * Copyright © The Bot Blog 2019 3 * License: MIT (https://github.com/TheBotBlog/thebotbloglib/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module thebotbloglib.facebookpost; 7 8 import std.string : strip, toLower; 9 import std.array : split, join; 10 import std.algorithm : count; 11 12 import vibe.d : Json; 13 14 import thebotbloglib.webmanager; 15 import thebotbloglib.facebookservice; 16 import thebotbloglib.graphapi; 17 import thebotbloglib.facebookcomment; 18 import thebotbloglib.facebookreact; 19 20 /// A Facebook post. 21 final class FacebookPost 22 { 23 private: 24 /// The Facebook service associated with the post. 25 FacebookService _service; 26 /// The id of the post. 27 string _id; 28 /// The next comment url. 29 string _nextCommentUrl; 30 31 /// The time the post was created. 32 string _createdTime; 33 /// The message of the post. 34 string _message; 35 /// The link of the post. 36 string _link; 37 /// The picture of the post. 38 string _picture; 39 40 /// All the like reactions. 41 FacebookReact[] _likeReacts; 42 /// All the love reactions. 43 FacebookReact[] _loveReacts; 44 /// All the wow reactions. 45 FacebookReact[] _wowReacts; 46 /// All the haha reactions. 47 FacebookReact[] _hahaReacts; 48 /// All the sad reactions. 49 FacebookReact[] _sadReacts; 50 /// All the angry reactions. 51 FacebookReact[] _angryReacts; 52 53 /// The total amount of reactions. 54 size_t _totalReacts; 55 /// The total amount of positive reactions. 56 size_t _totalPositiveReacts; 57 /// The total amount of inclusive positive reactions. 58 size_t _totalInclusivePositiveReacts; 59 /// The total amount of negative reactions. 60 size_t _totalNegativeReacts; 61 /// The total reaction rate. 62 ptrdiff_t _totalReactionRate; 63 64 package(thebotbloglib) 65 { 66 /** 67 * Creates a new Facebook post. 68 * Params: 69 * service = The Facebook service to associate the post with. 70 * id = The id of the post. 71 */ 72 this(FacebookService service, string id) 73 { 74 _service = service; 75 _id = id; 76 } 77 } 78 79 public: 80 final: 81 @property 82 { 83 /// Gets the id of the post. 84 string id() { return _id; } 85 86 /// Gets the time the post was created. 87 string createdTime() { return _createdTime; } 88 89 /// Gets the message of the post. 90 string message() { return _message; } 91 92 /// Gets the link of the post. 93 string link() { return _link; } 94 95 /// Gets the picture of the post. 96 string picture() { return _picture; } 97 98 /// Gets the like reactions. 99 FacebookReact[] likeReacts() { return _likeReacts; } 100 101 /// Gets the love reactions. 102 FacebookReact[] loveReacts() { return _loveReacts; } 103 104 /// Gets the wow reactions. 105 FacebookReact[] wowReacts() { return _wowReacts; } 106 107 /// Gets the haha reactions. 108 FacebookReact[] hahaReacts() { return _hahaReacts; } 109 110 /// Gets sad reactions. 111 FacebookReact[] sadReacts() { return _sadReacts; } 112 113 /// Gets the angry reactions. 114 FacebookReact[] angryReacts() { return _angryReacts; } 115 116 /// Gets the total reactions. 117 size_t totalReacts() { return _totalReacts; } 118 119 /// Gets the total positive reactions. 120 size_t totalPositiveReacts() { return _totalPositiveReacts; } 121 122 /// Gets the total inclusive positive reactions. 123 size_t totalInclusivePositiveReacts() { return _totalInclusivePositiveReacts; } 124 125 /// Gets the total negative reactions. 126 size_t totalNegativeReacts() { return _totalNegativeReacts; } 127 128 /// Gets the totoal reaction rate. 129 ptrdiff_t totalReactionRate() { return _totalReactionRate; } 130 131 /// Gets a boolean determining whether there are more comments to load. 132 bool hasMoreComments() 133 { 134 return _nextCommentUrl && _nextCommentUrl.strip.length; 135 } 136 } 137 138 /// Updates the post link and picture. 139 void updateLinkAndPhoto() 140 { 141 updatePostInfo(["link", "picture"]); 142 } 143 144 /** 145 * Updates the post information. 146 * Params: 147 * fields = An array of field names to update. 148 * fieldReader = A custom reader for fields. 149 */ 150 void updatePostInfo(string[] fields = null, void delegate(string,Json) fieldReader = null) 151 { 152 _nextCommentUrl = null; 153 154 auto web = new WebManager(_service); 155 156 string[string] parameters; 157 158 if (fields && fields.length) 159 { 160 parameters["fields"] = join(fields, ","); 161 } 162 163 auto resp = web.getRequest!Json(id, null, parameters); 164 165 foreach (k,v; resp.byKeyValue) 166 { 167 switch (k) 168 { 169 case "created_time": 170 _createdTime = v.to!string; 171 break; 172 173 case "name": 174 _message = v.to!string; 175 break; 176 177 case "link": 178 _link = v.to!string; 179 break; 180 181 case "picture": 182 _picture = v.to!string; 183 break; 184 185 default: break; 186 } 187 188 if (fieldReader) 189 { 190 fieldReader(k, v); 191 } 192 } 193 } 194 195 /** 196 * Reads the first set of comments from the post. 197 * Use readNextComments() to read the next set of comments. 198 * Params: 199 * order = The order of the comments. 200 * Returns: 201 * An array with the comments from the first set. 202 */ 203 FacebookComment[] readComments(string order = "chronological") 204 { 205 _nextCommentUrl = null; 206 207 FacebookComment[] comments = []; 208 209 auto web = new WebManager(_service); 210 211 string[string] parameters; 212 parameters["order"] = order; 213 214 auto resp = web.getRequest!GraphAPIComments(id, "comments", parameters); 215 216 fillComments(resp, comments); 217 218 return comments; 219 } 220 221 /** 222 * Reads the next set of comments from the post. 223 * Make sure to check hasMoreComments before calling this. 224 * Returns: 225 * An array with the comments from the next set. 226 */ 227 FacebookComment[] readNextComments() 228 { 229 FacebookComment[] comments = []; 230 231 auto web = new WebManager(_service); 232 233 auto resp = web.getRequestRaw!GraphAPIComments(_nextCommentUrl); 234 235 _nextCommentUrl = null; 236 237 fillComments(resp, comments); 238 239 return comments; 240 } 241 242 /** 243 * Fills the comments. 244 * Params: 245 * resp = The graph api response. 246 * comments = (ref) The comment collection to append to. 247 */ 248 private void fillComments(GraphAPIComments resp, ref FacebookComment[] comments) 249 { 250 if (resp && resp.data && resp.data.length) 251 { 252 foreach (data; resp.data) 253 { 254 auto comment = new FacebookComment(_service); 255 256 comment.id = data.id; 257 comment.createdTime = data.created_time; 258 comment.message = data.message; 259 comment.authorName = data.from ? data.from.name : null; 260 comment.authorId = data.from ? data.from.id : null; 261 262 comments ~= comment; 263 } 264 265 if (resp.paging && resp.paging.next && resp.paging.next.strip.length) 266 { 267 _nextCommentUrl = resp.paging.next; 268 } 269 } 270 } 271 272 /** 273 * Writes a comment on the post. 274 * Params: 275 * message = The message of the comment. This value can be null. 276 * photo = (optional) The photo of the comment. 277 * Returns: 278 * The id of the new comment. 279 */ 280 string writeComment(string message, string photo = null) 281 { 282 auto web = new WebManager(_service); 283 284 string[string] data; 285 286 if (message && message.strip.length) 287 { 288 data["message"] = message; 289 } 290 291 if (photo && photo.strip.length) 292 { 293 data["attachment_url"] = photo; 294 } 295 296 auto resp = web.postRequest!GraphAPIObjectResponse(id, "comments", data); 297 298 if (_service.commentRateLimit) 299 { 300 import core.thread; 301 302 Thread.sleep(_service.commentRateLimit.msecs); 303 } 304 305 return resp.id; 306 } 307 308 /** 309 * Appends a comment to another comment. 310 * Params: 311 * commentId = The id of the comment to append to. 312 * message = The message of the new comment. This value can be null. 313 * photo = (optional) The photo of the new comment. 314 * Returns: 315 * The id of the new comment. 316 */ 317 string appendComment(string commentId, string message, string photo = null) 318 { 319 auto web = new WebManager(_service); 320 321 string[string] data; 322 323 if (message && message.strip.length) 324 { 325 data["message"] = message; 326 } 327 328 if (photo && photo.strip.length) 329 { 330 data["attachment_url"] = photo; 331 } 332 333 auto resp = web.postRequest!GraphAPIObjectResponse(commentId, "comments", data); 334 335 if (_service.commentRateLimit) 336 { 337 import core.thread; 338 339 Thread.sleep(_service.commentRateLimit.msecs); 340 } 341 342 return resp.id; 343 } 344 345 /// Updates the reactions of the post. 346 void updateReacts() 347 { 348 FacebookReact[] reacts = []; 349 350 auto web = new WebManager(_service); 351 352 string[string] data; 353 354 auto resp = web.getRequest!GraphAPIObjectReactions(id, "reactions", data); 355 356 auto nextUrl = fillReacts(resp, reacts); 357 358 while (nextUrl && nextUrl.strip.length) 359 { 360 resp = web.getRequestRaw!GraphAPIObjectReactions(nextUrl); 361 362 nextUrl = fillReacts(resp, reacts); 363 } 364 365 _totalReacts = reacts.count!(r => r.reactionType != "none"); 366 367 _likeReacts = []; 368 _loveReacts = []; 369 _wowReacts = []; 370 _hahaReacts = []; 371 _sadReacts = []; 372 _angryReacts = []; 373 374 foreach (react; reacts) 375 { 376 switch (react.reactionType) 377 { 378 case "like": _likeReacts ~= react; break; 379 case "love": _loveReacts ~= react; break; 380 case "wow": _wowReacts ~= react; break; 381 case "haha": _hahaReacts ~= react; break; 382 case "sad": _sadReacts ~= react; break; 383 case "angry": _angryReacts ~= react; break; 384 385 default: break; 386 } 387 } 388 389 _totalPositiveReacts = (_likeReacts.length + _loveReacts.length); 390 _totalInclusivePositiveReacts = (_totalPositiveReacts + _hahaReacts.length); 391 _totalNegativeReacts = (_sadReacts.length + _angryReacts.length); 392 393 _totalReactionRate = _totalPositiveReacts - _angryReacts.length; 394 } 395 396 private string fillReacts(GraphAPIObjectReactions resp, ref FacebookReact[] reacts) 397 { 398 if (resp && resp.data && resp.data.length) 399 { 400 foreach (data; resp.data) 401 { 402 auto react = new FacebookReact; 403 react.id = data.id; 404 react.name = data.name; 405 react.reactionType = data.type.toLower.strip; 406 407 reacts ~= react; 408 } 409 410 if (resp.paging && resp.paging.next && resp.paging.next.strip.length) 411 { 412 return resp.paging.next; 413 } 414 } 415 416 return null; 417 } 418 }