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 }