├── .DS_Store ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── README.md ├── app.rb └── init_access.rb /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/graphcommons/gc-instagram/ebc4885d5c89f2182d3cc182ca95e6ca0cb199fe/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | secret_keys 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | gem 'sinatra' 4 | gem 'instagram' 5 | gem 'curb' 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | curb (0.8.8) 5 | faraday (0.9.1) 6 | multipart-post (>= 1.2, < 3) 7 | faraday_middleware (0.9.2) 8 | faraday (>= 0.7.4, < 0.10) 9 | hashie (3.4.2) 10 | instagram (1.1.6) 11 | faraday (>= 0.7, < 0.10) 12 | faraday_middleware (>= 0.8, < 0.10) 13 | hashie (>= 0.4.0) 14 | multi_json (~> 1.0, >= 1.0.3) 15 | multi_json (1.11.2) 16 | multipart-post (2.0.0) 17 | rack (1.6.4) 18 | rack-protection (1.5.3) 19 | rack 20 | sinatra (1.4.6) 21 | rack (~> 1.4) 22 | rack-protection (~> 1.4) 23 | tilt (>= 1.3, < 3) 24 | tilt (1.4.1) 25 | 26 | PLATFORMS 27 | ruby 28 | 29 | DEPENDENCIES 30 | curb 31 | instagram 32 | sinatra 33 | 34 | BUNDLED WITH 35 | 1.10.6 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This program generates a network map on Graph Commons based on your recent Instagram photos and their likes and comments. It aggregates people, photos, likes, comments from my last 200 posts and turns actions into connections, demonstrating how to use [Graph Commons API](https://graphcommons.com/dev) to generate graphs programmatically. 2 | 3 | #### Components 4 | - `init_access.rb` is a Sinatra server to obtain your Instagram access token. 5 | - `app.rb` is the actual program that queries Instagram and creates a graph on Graph Commons. 6 | 7 | #### Obtain Access Tokens 8 | In order to use this program, you will need two access tokens: 9 | 10 | 1. Create an Instagram Application and obtain its access token: https://instagram.com/developer/clients/manage/ 11 | 12 | 2. Get your API key for Graph Commons from your profile: https://graphcommons.com/me/edit 13 | 14 | #### Setup 15 | - `git clone git@github.com:graphcommons/gc-instagram.git` 16 | - `cd gc-instagram` 17 | - `bundle install` (Make sure you have `bundler` installed) 18 | 19 | #### Using 20 | 1.Run `init_access.rb` with ENV variables for Instagram Client ID and Client Secret 21 | ``` 22 | IG_CLIENT_ID= IG_CLIENT_SECRET= ruby init_access.rb 23 | ``` 24 | 2.Open browser `localhost:4567` and click on the link to *retrieve your Instagram access token* 25 | 26 | 3.Run `app.rb` with four ENV variables: 27 | - Instagram Client Id 28 | - Instagram Client Secret 29 | - Instagram Access Token 30 | - Graph Commons API key 31 | ``` 32 | IG_CLIENT_ID= IG_CLIENT_SECRET= IG_ACCESS_TOKEN= GC_API_KEY= ruby app.rb 33 | ``` 34 | 35 | 4.If all works well, you should see the URL of the generated graph, open it in the browser and enjoy your Instagram graph. 36 | 37 | #### Sample Output 38 | Here's the graph generated with this program: 39 | https://graphcommons.com/graphs/7c244715-6de4-45c5-a1dd-7959b1aee17d 40 | 41 | #### Where to go from here 42 | Learn more about the Graph Commons API documentation https://graphcommons.github.io/api-v1 43 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'instagram' 2 | require 'curl' 3 | require 'date' 4 | 5 | # Using 6 | # IG_CLIENT_ID= IG_CLIENT_SECRET= IG_ACCESS_TOKEN= GC_API_KEY= ruby app.rb 7 | 8 | Instagram.configure do |config| 9 | config.client_id = ENV["IG_CLIENT_ID"] 10 | config.client_secret = ENV["IG_CLIENT_SECRET"] 11 | end 12 | 13 | @apikey = ENV["IG_ACCESS_TOKEN"] 14 | 15 | # avoid duplicate users 16 | person_hash = Hash.new 17 | 18 | # node signals are collected here 19 | nodes_array = Array.new 20 | nodetypes_array = Array.new 21 | 22 | # edge signals are collected here 23 | edges_array = Array.new 24 | edgetypes_array = Array.new 25 | 26 | # media items are collected here 27 | media_items = Array.new 28 | 29 | # post frequency 30 | post_times = Array.new 31 | 32 | # reactions per post, per follower 33 | total_likes = 0 34 | total_comments = 0 35 | graph_description = "" # for metrics 36 | 37 | # 5K rate limiting per hour per user https://www.instagram.com/developer/limits/ 38 | # Assumed: 3.33 posts per day per user: 39 | # 3.33 posts per day * 30 day ~= 100 media items (api calls for post extended information) 40 | # 300 / 20 recent media per call = 15 api calls for all post ids 41 | num_media_items = 100 42 | max_page = (num_media_items/20).to_i - 1 43 | 44 | client = Instagram.client(:access_token => @apikey) 45 | 46 | current_user = client.user 47 | puts current_user.username 48 | total_posts = current_user.counts["media"] 49 | followings = current_user.counts["follows"] 50 | followers = current_user.counts["followed_by"] 51 | puts "#{total_posts} posts" 52 | puts "#{followings} followings" 53 | puts "#{followers} followers" 54 | graph_description << "#{total_posts} posts
" 55 | graph_description << "#{followings} followings
" 56 | graph_description << "#{followers} followers
" 57 | graph_description << "---
" 58 | 59 | puts "---" 60 | 61 | for i in 0..max_page 62 | if i == 0 63 | recent_media = client.user_recent_media 64 | page_max_id = recent_media.pagination.next_max_id 65 | else 66 | recent_media = client.user_recent_media(:max_id => page_max_id) unless page_max_id.nil? 67 | page_max_id = recent_media.pagination.next_max_id 68 | end 69 | media_items = media_items + recent_media 70 | puts "#{media_items.size} posts" 71 | end 72 | 73 | media_items.each do |media_item| 74 | 75 | # create the "Photo" node 76 | nodes_array << { 77 | :action => "node_create", 78 | :name => media_item.id, 79 | :type => "Photo", 80 | :description => media_item.caption.nil? ? "Untitled" : media_item.caption.text, 81 | :image => media_item.images.standard_resolution.url, 82 | :reference => media_item.link, 83 | :properties => { 84 | :likes => media_item.likes["count"], 85 | :comments => media_item.comments["count"], 86 | :posted_at => media_item.created_time, 87 | :location_name => media_item.location ? media_item.location.name : nil, 88 | :location_lat => media_item.location ? media_item.location.latitude : nil, 89 | :location_lon => media_item.location ? media_item.location.longitude : nil, 90 | :location_id => media_item.location ? media_item.location.id : nil 91 | } 92 | } 93 | total_likes = total_likes + media_item.likes["count"].to_i 94 | total_comments = total_comments + media_item.comments["count"].to_i 95 | 96 | # media_item limited to 4 likes & comments, 97 | # so make extended call for each media 98 | media_likes = client.media_likes(media_item.id) 99 | media_comments = client.media_comments(media_item.id) 100 | 101 | # Turn likes to graph signals 102 | unless media_likes.nil? 103 | media_likes.each do |person| # media_item.likes.data.each do |person| 104 | # create the "Person" node, skip if already added 105 | if person_hash[person.username].nil? 106 | puts "#{person.username} #{person.id} LIKED" 107 | nodes_array << { 108 | :action => "node_create", 109 | :name => person.username, 110 | :type => "Person", 111 | :description => person.id, 112 | :image => person.profile_picture, 113 | :reference => "https://instagram.com/#{person.username}" 114 | } 115 | person_hash[person.username] = true 116 | end 117 | 118 | # Person -[LIKED]-> Photo 119 | edges_array << { 120 | :action => "edge_create", 121 | :from_name => person.username, 122 | :from_type => "Person", 123 | :to_name => media_item.id, 124 | :to_type => "Photo", 125 | :name => "LIKED" 126 | } 127 | end 128 | end 129 | 130 | # Turn comments to graph signals 131 | unless media_comments.nil? 132 | media_comments.each do |comment| # media_item.comments.data.each do |comment| 133 | person = comment.from 134 | if person_hash[person.username].nil? 135 | puts "#{person.username} #{person.id} COMMENTED" 136 | nodes_array << { 137 | :action => "node_create", 138 | :name => person.username, 139 | :type => "Person", 140 | :description => person.id, 141 | :image => person.profile_picture, 142 | :reference => "https://instagram.com/#{person.username}" 143 | } 144 | person_hash[person.username] = true 145 | end 146 | 147 | # Person -[COMMENTED]-> Photo 148 | edges_array << { 149 | :action => "edge_create", 150 | :from_name => person.username, 151 | :from_type => "Person", 152 | :to_name => media_item.id, 153 | :to_type => "Photo", 154 | :name => "COMMENTED", 155 | :properties => { 156 | :text => comment.text, 157 | :id => comment.id, 158 | :posted_at => comment.created_time 159 | } 160 | } 161 | end 162 | end 163 | 164 | post_times << Time.at(media_item.created_time.to_i).to_datetime 165 | end 166 | 167 | puts "---" 168 | 169 | # Calcualte posts per day 170 | daily_posts = post_times.group_by {|t| t.strftime("%Y-%m-%d")} 171 | posts_per_day = [] 172 | daily_posts.each {|k,v| posts_per_day << v.size } 173 | avg_post_per_day = (posts_per_day.inject{ |sum, el| sum + el }.to_f / posts_per_day.size).round(2) 174 | puts "#{avg_post_per_day} avg posts per day" 175 | graph_description << "#{avg_post_per_day} avg posts per day
" 176 | 177 | graph_description << "---
" 178 | 179 | # Calcualte avg reactions per post 180 | total_media_items = media_items.size.to_f 181 | avg_likes_per_post = (total_likes / total_media_items).round(2) 182 | avg_comments_per_post = (total_comments / total_media_items).round(2) 183 | puts "#{avg_likes_per_post} avg likes per post" 184 | puts "#{avg_comments_per_post} avg comments per post" 185 | graph_description << "#{avg_likes_per_post} avg likes per post
" 186 | graph_description << "#{avg_comments_per_post} avg comments per post
" 187 | 188 | graph_description << "---
" 189 | 190 | # Calcualte raw reactions per follower (non follower reactions not separated) 191 | avg_likes_per_follower = (total_likes / followers.to_f).round(2) 192 | avg_comments_per_follower = (total_comments / followers.to_f).round(2) 193 | puts "#{avg_likes_per_follower} avg likes per follower" 194 | puts "#{avg_comments_per_follower} avg comments per follower" 195 | graph_description << "#{avg_likes_per_follower} avg likes per follower
" 196 | graph_description << "#{avg_comments_per_follower} avg comments per follower
" 197 | 198 | puts "---" 199 | 200 | # Set color for nodetypes and eddgetypes 201 | nodetypes_array << { 202 | :action => "nodetype_create", 203 | :name => "Person", 204 | :color => "#3473C6" 205 | } 206 | nodetypes_array << { 207 | :action => "nodetype_create", 208 | :name => "Photo", 209 | :color => "#C5A7A3", 210 | :hide_name => true, 211 | :image_as_icon => true 212 | } 213 | edgetypes_array << { 214 | :action => "edgetype_create", 215 | :name => "LIKED", 216 | :color => "#6296DC" 217 | } 218 | edgetypes_array << { 219 | :action => "edgetype_create", 220 | :name => "COMMENTED", 221 | :color => "#FFC400" 222 | } 223 | 224 | puts "Creating the graph..." 225 | host = "http://localhost:3000" 226 | # building the body of the request to generate the graph 227 | # note the node and edge arrays are concatenated into the signals key 228 | generate_graph = { 229 | :name => "Instagram graph for #{current_user.username}", 230 | :status => 0, 231 | :subtitle => "Network of photos and people who liked and commented", 232 | :description => graph_description, 233 | :signals => nodes_array + edges_array + nodetypes_array + edgetypes_array 234 | } 235 | 236 | # dont forget to set the API KEY in the header" 237 | c = Curl::Easy.http_post("#{host}/api/v1/graphs", generate_graph.to_json) do |curl| 238 | curl.headers['Content-Type'] = 'application/json' 239 | curl.headers['Authentication'] = ENV["GC_API_KEY"] 240 | end 241 | 242 | # output the response, just print out the id 243 | response_hash = JSON.parse(c.body_str, {:symbolize_names => true}) 244 | puts "#{host}/graphs/#{response_hash[:graph][:id]}" 245 | -------------------------------------------------------------------------------- /init_access.rb: -------------------------------------------------------------------------------- 1 | require 'instagram' 2 | require 'sinatra' 3 | 4 | # Using 5 | # IG_CLIENT_ID= IG_CLIENT_SECRET= ruby init_access.rb 6 | 7 | CALLBACK_URL = "http://localhost:4567/oauth/callback" 8 | 9 | Instagram.configure do |config| 10 | config.client_id = ENV["IG_CLIENT_ID"] 11 | config.client_secret = ENV["IG_CLIENT_SECRET"] 12 | end 13 | 14 | get "/" do 15 | 'Connect with Instagram' 16 | end 17 | 18 | get "/oauth/connect" do 19 | redirect Instagram.authorize_url(:redirect_uri => CALLBACK_URL, :scope => "comments relationships likes") 20 | end 21 | 22 | get "/oauth/callback" do 23 | response = Instagram.get_access_token(params[:code], :redirect_uri => CALLBACK_URL) 24 | 'use this access token: ' + response.access_token 25 | end 26 | --------------------------------------------------------------------------------