-
Notifications
You must be signed in to change notification settings - Fork 0
/
facebook.go
executable file
·724 lines (650 loc) · 27.1 KB
/
facebook.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
// Social Harvest is a social media analytics platform.
// Copyright (C) 2014 Tom Maiaroto, Shift8Creative, LLC (http://www.socialharvest.io)
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package harvester
import (
"github.com/SocialHarvest/harvester/lib/config"
geohash "github.com/SocialHarvestVendors/geohash-golang"
"github.com/SocialHarvestVendors/go-querystring/query"
//"github.com/mitchellh/mapstructure"
"bytes"
"encoding/json"
"log"
"net"
"net/http"
"net/url"
//"sync"
"time"
)
type PagingResult struct {
Next string `json:"next" url:"next"`
Previous string `json:"previous" url:"previous"`
}
type FacebookParams struct {
IncludeEntities string `url:"include_entities,omitempty"`
Limit string `url:"limit,omitempty"`
Count string `url:"count,omitempty"`
Type string `url:"type,omitempty"`
Lang string `url:"lang,omitempty"`
Q string `url:"q,omitempty"`
AccessToken string `url:"access_token,omitempty"`
Until string `url:"until,omitempty"`
Since string `url:"since,omitempty"`
//Previous string // Facebook uses __previous ...not sure if MakeParams() supports that...and not sure we even need to go backwards anyway.
//Paging *PagingResult
//HasNextPage bool
}
type MessageTag struct {
Id string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
}
type FacebookPost struct {
// "id" must exist in response. note the leading comma.
Id string `json:"id,required"`
From struct {
Id string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
} `json:"from"`
To struct {
Data []struct {
Id string `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
} `json:"data"`
} `json:"to"`
CreatedTime string `json:"created_time"`
UpdatedTime string `json:"updated_time"`
Message string `json:"message"`
Description string `json:"description"`
Caption string `json:"caption"`
Picture string `json:"picture"`
Source string `json:"source"`
Link string `json:"link"`
Shares struct {
Count int `json:"count"`
} `json:"shares"`
Name string `json:"name"`
// Should always be "post" right? No, facebook also includes "status" and "link" and "photo" in there, even with the type param set to post. Seems like something changed/broke.
Type string `json:"type"`
// This can tell us if the user is posting from a mobile device...with some logic. Or just which client apps/SaaS' are most popular to post from (also true for Twitter and could be good data to have).
Application struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Id string `json:"id"`
} `json:"application"`
MessageTags map[string][]*MessageTag `json:"message_tags"`
StoryTags map[string][]*MessageTag `json:"story_tags"`
Story string `json:"story"`
// Typically accompanies items of type photo.
ObjectId string `json:"object_id"`
// TODO Comments []struct{}
// Comments have paging though. So care needs to be taken...Do we make more requests and get all comments? Do we limit?
// What about API request limits?
// This only exists on user/page /feed items...and it'll usually be "shared_story" but sometimes I've seen "mobile_status_update" ... which tells us the user is on a mobile device.
// Is it important to keep? I don't know. Probably not right now.
StatusType string `json:"status_type"`
}
// Facebook accounts can be for a user or a page
type FacebookAccount struct {
// "id" must exist in response. note the leading comma.
Id string `json:"id,required"`
About string `json:"about"`
Category string `json:"category"`
Checkins int `json:"checkins"`
CompanyOverview string `json:"company_overview"`
Description string `json:"description"`
Founded string `json:"founded"`
GeneralInfo string `json:"general_info"`
Likes int `json:"likes"`
Link string `json:"link"`
Location struct {
Street string `json:"street"`
City string `json:"city"`
State string `json:"state"`
Zip string `json:"zip"`
Country string `json:"country"`
Longitude float64 `json:"longitude"`
Latitude float64 `json:"latitude"`
} `json:"location"`
Name string `json:"name"`
Phone string `json:"phone"`
TalkingAboutCount int `json:"talking_about_count"`
WereHereCount int `json:"were_here_count"`
Username string `json:"username"`
Website string `json:"website"`
Products string `json:"products"`
// User specific (the above is a mix of page and user)
Gender string `json:"gender"`
Locale string `json:"locale"`
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
}
var fbToken string
var fbHttpClient *http.Client
var fbGraphApiBaseUrl = "https://graph.facebook.com/"
// Set the appToken for future use (global)
func NewFacebook(servicesConfig config.ServicesConfig) {
fbToken = servicesConfig.Facebook.AppToken
fbHttpClient = &http.Client{
Transport: &TimeoutTransport{
Transport: http.Transport{
Dial: func(netw, addr string) (net.Conn, error) {
//log.Printf("dial to %s://%s", netw, addr)
return net.Dial(netw, addr) // Regular ass dial.
},
DisableKeepAlives: true,
//DisableCompression: true,
},
// Facebook's payload (especially with 100 results) will take a little while to download
RoundTripTimeout: time.Second * 10,
},
}
}
// If the territory has a different appToken to use
func NewFacebookTerritoryCredentials(territory string) {
for _, t := range harvestConfig.Territories {
if t.Name == territory {
if t.Services.Facebook.AppToken != "" {
// TODO: This actually should be passed on each harvest. Because otherwise it'd overwrite the harvest wide token.
services.facebookAppToken = t.Services.Facebook.AppToken
}
}
}
}
// Takes an array of Post structs and converts it to JSON and logs to file (to be picked up by Fluentd, Logstash, Ik, etc.)
func FacebookPostsOut(posts []FacebookPost, territoryName string, params FacebookParams) (int, string, time.Time) {
var itemsHarvested = 0
var latestId = ""
var latestTime time.Time
for _, post := range posts {
postCreatedTime, err := time.Parse("2006-01-02T15:04:05-0700", post.CreatedTime)
// Only take posts that have a time (and an ID from Facebook)
if err == nil && len(post.Id) > 0 {
itemsHarvested++
// If this is the most recent post in the results, set it's date and id (to be returned) so we can continue where we left off in future harvests
if latestTime.IsZero() || postCreatedTime.Unix() > latestTime.Unix() {
latestTime = postCreatedTime
latestId = post.Id
}
hostName := ""
if len(post.Link) > 0 {
pUrl, _ := url.Parse(post.Link)
hostName = pUrl.Host
}
// Generate a harvest_id to avoid potential dupes (a unique index is placed on this field and all insert errors ignored).
harvestId := GetHarvestMd5(post.Id + "facebook" + territoryName)
//log.Println(harvestId)
// contributor row (who created the message)
// NOTE: This is synchronous...but that's ok because while I'd love to use channels and make a bunch of requests at once, there's rate limits from these APIs...
// Plus the contributor info tells us a few things about the message, such as locale. Other series will use this data.
var contributor = FacebookAccount{}
contributor = FacebookGetUserInfo(post.From.Id, params)
var contributorGender = 0
if contributor.Gender == "male" {
contributorGender = 1
}
if contributor.Gender == "female" {
contributorGender = -1
}
var contributorName = contributor.Name
if len(contributor.FirstName) > 0 {
contributorName = contributor.FirstName + " " + contributor.LastName
}
var contributorType = "person"
if len(contributor.CompanyOverview) > 0 || len(contributor.Founded) > 0 || len(contributor.Category) > 0 {
contributorType = "company"
}
// Reverse code to get city, state, country, etc.
var contributorCountry = ""
var contributorRegion = ""
var contributorCity = ""
var contributorCityPopulation = int32(0)
// This isn't always available with Geobed information and while many counties will be, they still need to be decoded with the Geonames data set (id numbers to string names).
// When Geobed updates, then Social Harvest can add county information in again. "State" (US state) has also changed to "Region" due to the data sets being used.
// A little consistency has been lost, but geocoding is all internal now. Not a bad trade off.
// var contributorCounty = ""
if contributor.Location.Latitude != 0.0 && contributor.Location.Latitude != 0.0 {
reverseLocation := services.geocoder.ReverseGeocode(contributor.Location.Latitude, contributor.Location.Longitude)
contributorRegion = reverseLocation.Region
contributorCity = reverseLocation.City
contributorCountry = reverseLocation.Country
contributorCityPopulation = reverseLocation.Population
// contributorCounty = reverseLocation.County
}
// Geohash
var locationGeoHash = geohash.Encode(contributor.Location.Latitude, contributor.Location.Longitude)
// This is produced with empty lat/lng values - don't store it.
if locationGeoHash == "7zzzzzzzzzzz" {
locationGeoHash = ""
}
// TODO: Category (use a classifier in the future for this?)
// message row
messageRow := config.SocialHarvestMessage{
Time: postCreatedTime,
HarvestId: harvestId,
Territory: territoryName,
Network: "facebook",
MessageId: post.Id,
ContributorId: post.From.Id,
ContributorScreenName: post.From.Name,
ContributorName: contributorName,
ContributorGender: contributorGender,
ContributorType: contributorType,
ContributorLang: LocaleToLanguageISO(contributor.Locale),
ContributorLongitude: contributor.Location.Longitude,
ContributorLatitude: contributor.Location.Latitude,
ContributorGeohash: locationGeoHash,
ContributorCity: contributorCity,
ContributorCityPopulation: contributorCityPopulation,
ContributorRegion: contributorRegion,
ContributorCountry: contributorCountry,
ContributorLikes: contributor.Likes,
Message: post.Message,
FacebookShares: post.Shares.Count,
Category: contributor.Category,
Sentiment: services.sentimentAnalyzer.Classify(post.Message),
IsQuestion: Btoi(IsQuestion(post.Message, harvestConfig.QuestionRegex)),
}
StoreHarvestedData(messageRow)
LogJson(messageRow, "messages")
// Keywords are stored on the same collection as hashtags - but under a `keyword` field instead of `tag` field as to not confuse the two.
// Limit to words 4 characters or more and only return 8 keywords. This could greatly increase the database size if not limited.
keywords := GetKeywords(post.Message, 4, 8)
if len(keywords) > 0 {
for _, keyword := range keywords {
if keyword != "" {
keywordHarvestId := GetHarvestMd5(post.Id + "facebook" + territoryName + keyword)
// Again, keyword share the same series/table/collection
hashtag := config.SocialHarvestHashtag{
Time: postCreatedTime,
HarvestId: keywordHarvestId,
Territory: territoryName,
Network: "facebook",
MessageId: post.Id,
ContributorId: post.From.Id,
ContributorScreenName: post.From.Name,
ContributorName: contributorName,
ContributorGender: contributorGender,
ContributorType: contributorType,
ContributorLang: LocaleToLanguageISO(contributor.Locale),
ContributorLongitude: contributor.Location.Longitude,
ContributorLatitude: contributor.Location.Latitude,
ContributorGeohash: locationGeoHash,
ContributorCity: contributorCity,
ContributorCityPopulation: contributorCityPopulation,
ContributorRegion: contributorRegion,
ContributorCountry: contributorCountry,
Keyword: keyword,
}
StoreHarvestedData(hashtag)
LogJson(hashtag, "hashtags")
}
}
}
// shared links row
// TODO: expand short urls (Facebook doesn't do it for us unfortunately)
if len(post.Link) > 0 {
sharedLinksRow := config.SocialHarvestSharedLink{
Time: postCreatedTime,
HarvestId: harvestId,
Territory: territoryName,
Network: "facebook",
MessageId: post.Id,
ContributorId: post.From.Id,
ContributorScreenName: post.From.Name,
ContributorName: contributorName,
ContributorGender: contributorGender,
ContributorType: contributorType,
ContributorLang: LocaleToLanguageISO(contributor.Locale),
ContributorLongitude: contributor.Location.Longitude,
ContributorLatitude: contributor.Location.Latitude,
ContributorGeohash: locationGeoHash,
ContributorCity: contributorCity,
ContributorCityPopulation: contributorCityPopulation,
ContributorRegion: contributorRegion,
ContributorCountry: contributorCountry,
Type: post.Type,
Preview: post.Picture,
Source: post.Source,
Url: post.Link,
ExpandedUrl: ExpandUrl(post.Link),
Host: hostName,
}
StoreHarvestedData(sharedLinksRow)
LogJson(sharedLinksRow, "shared_links")
}
// mentions row (note the harvest id in the following - any post that has multiple subobjects to be stored separately will need a different harvest id, else only one of those subobjects would be stored)
for _, tag := range post.StoryTags {
for _, mention := range tag {
// The harvest id is going to have to be a little different in this case too...Otherwise, we would only get one mention per post.
storyTagsMentionHarvestId := GetHarvestMd5(post.Id + mention.Id + territoryName)
// TODO: Keep an eye on this, it may add too many API requests...
var mentionedContributor = FacebookAccount{}
mentionedContributor = FacebookGetUserInfo(mention.Id, params)
var mentionedGender = 0
if mentionedContributor.Gender == "male" {
mentionedGender = 1
}
if mentionedContributor.Gender == "female" {
mentionedGender = -1
}
var mentionedName = mentionedContributor.Name
if len(mentionedContributor.FirstName) > 0 {
mentionedName = mentionedContributor.FirstName + " " + mentionedContributor.LastName
}
var mentionedType = "person"
if len(mentionedContributor.CompanyOverview) > 0 || len(mentionedContributor.Founded) > 0 || len(mentionedContributor.Category) > 0 {
mentionedType = "company"
}
var mentionedLocationGeoHash = geohash.Encode(mentionedContributor.Location.Latitude, mentionedContributor.Location.Longitude)
// This is produced with empty lat/lng values - don't store it.
if mentionedLocationGeoHash == "7zzzzzzzzzzz" {
mentionedLocationGeoHash = ""
}
mentionRow := config.SocialHarvestMention{
Time: postCreatedTime,
HarvestId: storyTagsMentionHarvestId,
Territory: territoryName,
Network: "facebook",
MessageId: post.Id,
ContributorId: post.From.Id,
ContributorScreenName: post.From.Name,
ContributorName: contributorName,
ContributorGender: contributorGender,
ContributorType: contributorType,
ContributorLongitude: contributor.Location.Longitude,
ContributorLatitude: contributor.Location.Latitude,
ContributorGeohash: locationGeoHash,
ContributorLang: LocaleToLanguageISO(contributor.Locale),
MentionedId: mention.Id,
MentionedScreenName: mention.Name,
MentionedName: mentionedName,
MentionedGender: mentionedGender,
MentionedType: mentionedType,
MentionedLongitude: mentionedContributor.Location.Longitude,
MentionedLatitude: mentionedContributor.Location.Latitude,
MentionedGeohash: mentionedLocationGeoHash,
MentionedLang: LocaleToLanguageISO(mentionedContributor.Locale),
}
StoreHarvestedData(mentionRow)
LogJson(mentionRow, "mentions")
}
}
// Also try MessageTags (which exist on user and page feeds, whereas StoryTags are available on public posts search)
for _, tag := range post.MessageTags {
for _, mention := range tag {
// Same here, the harvest id is going to have to be a little different in this case too...Otherwise, we would only get one mention per post.
MessageTagsMentionHarvestId := GetHarvestMd5(post.Id + mention.Id + territoryName)
// TODO: Keep an eye on this, it may add too many API requests...
// TODO: this is repeated. don't repeat.
var mentionedContributor = FacebookAccount{}
mentionedContributor = FacebookGetUserInfo(mention.Id, params)
var mentionedGender = 0
if mentionedContributor.Gender == "male" {
mentionedGender = 1
}
if mentionedContributor.Gender == "female" {
mentionedGender = -1
}
var mentionedName = mentionedContributor.Name
if len(mentionedContributor.FirstName) > 0 {
mentionedName = mentionedContributor.FirstName + " " + mentionedContributor.LastName
}
var mentionedType = "person"
if len(mentionedContributor.CompanyOverview) > 0 || len(mentionedContributor.Founded) > 0 || len(mentionedContributor.Category) > 0 {
mentionedType = "company"
}
var mentionedLocationGeoHash = geohash.Encode(mentionedContributor.Location.Latitude, mentionedContributor.Location.Longitude)
// This is produced with empty lat/lng values - don't store it.
if mentionedLocationGeoHash == "7zzzzzzzzzzz" {
mentionedLocationGeoHash = ""
}
mentionRow := config.SocialHarvestMention{
Time: postCreatedTime,
HarvestId: MessageTagsMentionHarvestId,
Territory: territoryName,
Network: "facebook",
MessageId: post.Id,
ContributorId: post.From.Id,
ContributorScreenName: post.From.Name,
ContributorName: contributorName,
ContributorGender: contributorGender,
ContributorType: contributorType,
ContributorLongitude: contributor.Location.Longitude,
ContributorLatitude: contributor.Location.Latitude,
ContributorGeohash: locationGeoHash,
ContributorLang: LocaleToLanguageISO(contributor.Locale),
MentionedId: mention.Id,
MentionedScreenName: mention.Name,
MentionedName: mentionedName,
MentionedGender: mentionedGender,
MentionedType: mentionedType,
MentionedLongitude: mentionedContributor.Location.Longitude,
MentionedLatitude: mentionedContributor.Location.Latitude,
MentionedGeohash: mentionedLocationGeoHash,
MentionedLang: LocaleToLanguageISO(mentionedContributor.Locale),
}
StoreHarvestedData(mentionRow)
LogJson(mentionRow, "mentions")
}
}
} else {
log.Println("Could not parse the time from the Facebook post, so I'm throwing it away!")
log.Println(err)
}
}
// return the number of items harvested
return itemsHarvested, latestId, latestTime
}
// -------------- API CALLS
// Searches public posts on Facebook
func FacebookSearch(territoryName string, harvestState config.HarvestState, params FacebookParams) (FacebookParams, config.HarvestState) {
// Look for access_token override, if not present, use default fbToken from config
if params.AccessToken == "" {
params.AccessToken = fbToken
}
// If that happens to be empty, just return.
if params.AccessToken == "" {
return params, harvestState
}
// Concatenate and build the searchUrl
var buffer bytes.Buffer
buffer.WriteString(fbGraphApiBaseUrl)
buffer.WriteString("/search?")
// convert struct to querystring params
v, err := query.Values(params)
if err != nil {
return params, harvestState
}
buffer.WriteString(v.Encode())
searchUrl := buffer.String()
buffer.Reset()
// set up the request
req, err := http.NewRequest("GET", searchUrl, nil)
if err != nil {
return params, harvestState
}
// doo it
resp, err := fbHttpClient.Do(req)
if err != nil {
return params, harvestState
}
defer resp.Body.Close()
// now to parse response, store and contine along.
data := struct {
Posts []FacebookPost `json:"data"`
Paging struct {
Previous string `json:"previous"`
Next string `json:"next"`
} `json:"paging"`
}{}
dec := json.NewDecoder(resp.Body)
dec.Decode(&data)
//log.Println(data)
// close the response now, we don't need it anymore - otherwise it'll stay open while we write to the database. best to close it.
resp.Body.Close()
// parse the querystring of "next" so we can get the "until" value for params
if data.Paging.Next != "" {
u, err := url.Parse(data.Paging.Next)
if err == nil {
m, _ := url.ParseQuery(u.RawQuery)
if _, ok := m["until"]; ok {
params.Until = m["until"][0]
} else {
// By setting this empty, we'll know not to loop again. This is up to date and should be the last request for this harvest.
params.Until = ""
}
} else {
// log.Println(err)
}
}
// Only attempt to store if we have some results.
if len(data.Posts) > 0 {
// Save, then return updated params and harvest state for next round (if there is another one)
harvestState.ItemsHarvested, harvestState.LastId, harvestState.LastTime = FacebookPostsOut(data.Posts, territoryName, params)
}
return params, harvestState
}
// Gets the public posts for a given user or page id (or name actually)
func FacebookFeed(territoryName string, harvestState config.HarvestState, account string, params FacebookParams) (FacebookParams, config.HarvestState) {
// XBox page feed for example...
// https://graph.facebook.com/xbox
// 16547831022
// Look for access_token override, if not present, use default fbToken from config
if params.AccessToken == "" {
params.AccessToken = fbToken
}
// If that happens to be empty, just return.
if params.AccessToken == "" {
return params, harvestState
}
var buffer bytes.Buffer
buffer.WriteString(fbGraphApiBaseUrl)
buffer.WriteString(account)
buffer.WriteString("/feed?")
// convert struct to querystring params
v, err := query.Values(params)
if err != nil {
return params, harvestState
}
buffer.WriteString(v.Encode())
feedUrl := buffer.String()
buffer.Reset()
// set up the request
req, err := http.NewRequest("GET", feedUrl, nil)
if err != nil {
return params, harvestState
}
// doo it
resp, err := fbHttpClient.Do(req)
if err != nil {
return params, harvestState
}
defer resp.Body.Close()
// now to parse response, store and contine along.
data := struct {
Posts []FacebookPost `json:"data"`
Paging struct {
Previous string `json:"previous"`
Next string `json:"next"`
} `json:"paging"`
}{}
dec := json.NewDecoder(resp.Body)
dec.Decode(&data)
//log.Println(data)
// close the response now, we don't need it anymore - otherwise it'll stay open while we write to the database. best to close it.
resp.Body.Close()
// parse the querystring of "next" so we can get the "until" value for params
if data.Paging.Next != "" {
u, err := url.Parse(data.Paging.Next)
if err == nil {
m, _ := url.ParseQuery(u.RawQuery)
if _, ok := m["until"]; ok {
params.Until = m["until"][0]
} else {
// By setting this empty, we'll know not to loop again. This is up to date and should be the last request for this harvest.
params.Until = ""
}
} else {
// log.Println(err)
}
}
// Only attempt to store if we have some results.
if len(data.Posts) > 0 {
// Save, then return updated params and harvest state for next round (if there is another one)
harvestState.ItemsHarvested, harvestState.LastId, harvestState.LastTime = FacebookPostsOut(data.Posts, territoryName, params)
}
return params, harvestState
}
// Gets basic info about an account on Facebook
func FacebookGetUserInfo(id string, params FacebookParams) FacebookAccount {
var account FacebookAccount
if id != "" {
var buffer bytes.Buffer
buffer.WriteString(fbGraphApiBaseUrl)
buffer.WriteString(id)
buffer.WriteString("?")
// convert struct to querystring params (for now, only pass the access_token, the other stuff doesn't matter for our use here)
userInfoParams := FacebookParams{AccessToken: params.AccessToken}
v, err := query.Values(userInfoParams)
if err != nil {
return account
}
buffer.WriteString(v.Encode())
userInfoUrl := buffer.String()
buffer.Reset()
// set up the request
req, err := http.NewRequest("GET", userInfoUrl, nil)
if err != nil {
return account
}
// doo it
resp, err := fbHttpClient.Do(req)
if err != nil {
return account
}
defer resp.Body.Close()
dec := json.NewDecoder(resp.Body)
dec.Decode(&account)
// close the response now, we don't need it anymore - it should close right after this anyway because of the defer... but these calls are atomic. so just to be safe.
// i've had too much trouble with unclosed http requests. i'm happy to be paranoid and safe rather than sorry, because i've been sorry and sore before.
resp.Body.Close()
}
return account
}
// Harvests Facebook account details to track changes in likes, etc. (only for public pages)
func FacebookAccountDetails(territoryName string, account string) {
params := FacebookParams{}
contributor := FacebookGetUserInfo(account, params)
now := time.Now()
// The harvest id in this case will be unique by time / account / network / territory, since there is no post id or anything else like that
harvestId := GetHarvestMd5(account + now.String() + "facebook" + territoryName)
row := config.SocialHarvestContributorGrowth{
Time: now,
HarvestId: harvestId,
Territory: territoryName,
Network: "facebook",
ContributorId: contributor.Id,
Likes: contributor.Likes,
TalkingAbout: contributor.TalkingAboutCount,
WereHere: contributor.WereHereCount,
Checkins: contributor.Checkins,
}
StoreHarvestedData(row)
LogJson(row, "contributor_growth")
return
}