glass-rails 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. data/.DS_Store +0 -0
  2. data/.gitignore +19 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +172 -0
  6. data/Rakefile +1 -0
  7. data/glass-rails.gemspec +33 -0
  8. data/lib/.DS_Store +0 -0
  9. data/lib/generators/.DS_Store +0 -0
  10. data/lib/generators/glass.rb +22 -0
  11. data/lib/generators/glass/.DS_Store +0 -0
  12. data/lib/generators/glass/install/install_generator.rb +38 -0
  13. data/lib/generators/glass/install/templates/glass_timeline_item_migration.rb +21 -0
  14. data/lib/generators/glass/install/templates/google-oauth.yml +15 -0
  15. data/lib/generators/glass/install/templates/google_account.rb +28 -0
  16. data/lib/generators/glass/install/templates/initializer.rb +24 -0
  17. data/lib/generators/glass/install/templates/notifications_controller.rb +7 -0
  18. data/lib/generators/glass/model/model_generator.rb +19 -0
  19. data/lib/generators/glass/model/templates/model.rb +28 -0
  20. data/lib/generators/glass/templates/.DS_Store +0 -0
  21. data/lib/generators/glass/templates/templates/image_full.html.erb +9 -0
  22. data/lib/generators/glass/templates/templates/image_left_with_section_right.html.erb +8 -0
  23. data/lib/generators/glass/templates/templates/image_left_with_table_right.html.erb +11 -0
  24. data/lib/generators/glass/templates/templates/list.html.erb +7 -0
  25. data/lib/generators/glass/templates/templates/simple.html.erb +7 -0
  26. data/lib/generators/glass/templates/templates/table.html.erb +8 -0
  27. data/lib/generators/glass/templates/templates/two_column.html.erb +12 -0
  28. data/lib/generators/glass/templates/templates/two_column_with_emphasis_left.html.erb +14 -0
  29. data/lib/generators/glass/templates/templates_generator.rb +16 -0
  30. data/lib/glass-rails.rb +7 -0
  31. data/lib/glass.rb +24 -0
  32. data/lib/glass/.DS_Store +0 -0
  33. data/lib/glass/api_keys.rb +29 -0
  34. data/lib/glass/client.rb +95 -0
  35. data/lib/glass/engine.rb +5 -0
  36. data/lib/glass/menu_item.rb +33 -0
  37. data/lib/glass/rails/version.rb +5 -0
  38. data/lib/glass/subscription.rb +46 -0
  39. data/lib/glass/subscription_notification.rb +75 -0
  40. data/lib/glass/template.rb +45 -0
  41. data/lib/glass/timeline_item.rb +259 -0
  42. data/spec/models/glass/template_spec.rb +12 -0
  43. data/spec/models/glass/timeline_item_spec.rb +9 -0
  44. data/spec/spec_helper.rb +55 -0
  45. metadata +226 -0
@@ -0,0 +1,28 @@
1
+ class Glass::<%= model_name.camelize %> < Glass::TimelineItem
2
+
3
+
4
+ defaults_template with: "table.html.erb"
5
+
6
+
7
+
8
+ ## this feature is experimental and not yet ready
9
+ ## for use:
10
+ # manages_template with: :template_manager
11
+
12
+
13
+ #### these are your menu items which you define
14
+ #### for the timeline object.
15
+ ####
16
+
17
+ has_menu_item :custom_action_name, display_name: "this is displayed",
18
+ icon_url: "http://icons.iconarchive.com/icons/enhancedlabs/lha-objects/128/Filetype-URL-icon.png",
19
+ handled_by: :custom_action_handler
20
+
21
+
22
+
23
+
24
+ def custom_action_handler
25
+ ## logic for handling custom action
26
+ end
27
+
28
+ end
@@ -0,0 +1,9 @@
1
+ <article class="photo">
2
+ <img src="<%= @image_url %>" style="max-width:100%;">
3
+ <div class="photo-overlay" />
4
+ <section style="height:180px;top:130px;">
5
+ <p>
6
+ <%= @content %>
7
+ </p>
8
+ </section>
9
+ </article>
@@ -0,0 +1,8 @@
1
+ <article>
2
+ <figure>
3
+ <img src="<%= @image_url %>">
4
+ </figure>
5
+ <section>
6
+ <%= @content %>
7
+ </section>
8
+ </article>
@@ -0,0 +1,11 @@
1
+ <article>
2
+ <figure>
3
+ <img src="<%= @image_url %>">
4
+ </figure>
5
+ <section>
6
+ <table>
7
+ <%= @table_header %>
8
+ <%= @table_body %>
9
+ </table>
10
+ </section>
11
+ </article>
@@ -0,0 +1,7 @@
1
+ <article>
2
+ <section>
3
+ <ul>
4
+ <%= @content %>
5
+ </ul>
6
+ </section>
7
+ </article>
@@ -0,0 +1,7 @@
1
+ <article>
2
+ <section>
3
+ <p>
4
+ <%= @content %>
5
+ </p>
6
+ </section>
7
+ </article>
@@ -0,0 +1,8 @@
1
+ <article>
2
+ <section>
3
+ <table>
4
+ <%= @table_header %>
5
+ <%= @table_rows %>
6
+ </table>
7
+ </section>
8
+ </article>
@@ -0,0 +1,12 @@
1
+ <article>
2
+ <section>
3
+ <div class="layout-two-column">
4
+ <div class="align-center">
5
+ <%= @left_content %>
6
+ </div>
7
+ <div class="align-center">
8
+ <%= @right_content %>
9
+ </div>
10
+ </div>
11
+ </section>
12
+ </article>
@@ -0,0 +1,14 @@
1
+ <article>
2
+ <section>
3
+ <div class="layout-figure">
4
+ <div class="align-center">
5
+ <%= @left_content %>
6
+ </div>
7
+ <div>
8
+ <div class="text-normal">
9
+ <%= @right_content %>
10
+ </div>
11
+ </div>
12
+ </div>
13
+ </section>
14
+ </article>
@@ -0,0 +1,16 @@
1
+ require 'generators/glass'
2
+ require 'rails/generators'
3
+ require 'rails/generators/migration'
4
+
5
+ module Glass
6
+ module Generators
7
+ class TemplatesGenerator < Base
8
+ include Rails::Generators::Migration
9
+
10
+ def copy_glass_templates
11
+ directory("", "app/views/glass")
12
+ end
13
+ end
14
+ private
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ require "glass"
2
+ require "glass/engine"
3
+ require "glass/timeline_item"
4
+ require "glass/subscription_notification"
5
+ require "glass/template"
6
+ require "glass/client"
7
+ require 'glass/subscription'
data/lib/glass.rb ADDED
@@ -0,0 +1,24 @@
1
+ require "rails/all"
2
+ require 'active_support/core_ext/numeric/time'
3
+ require 'active_support/dependencies'
4
+
5
+
6
+ module Glass
7
+
8
+ mattr_accessor :brandname
9
+ @@brandname = "example"
10
+
11
+ mattr_accessor :brandname_styles
12
+ @@brandname_styles = {color: "#8BCDF8",
13
+ font_size: "30px"}
14
+
15
+ mattr_accessor :glass_template_path
16
+ @@glass_template_path = "app/views/glass"
17
+
18
+
19
+ ## devise trick
20
+ def self.setup
21
+ yield self
22
+ end
23
+
24
+ end
Binary file
@@ -0,0 +1,29 @@
1
+ require "yaml"
2
+ module Glass
3
+ ## just a small class to organize the api key logic info
4
+ ## from the yml file in config.
5
+ class ApiKeys
6
+ class APIKeyConfigurationError < StandardError; end;
7
+ attr_accessor :client_id, :client_secret, :google_api_keys
8
+ def initialize
9
+ self.google_api_keys = load_yaml_file
10
+ load_keys
11
+ self
12
+ end
13
+ private
14
+ def load_keys
15
+ if google_api_keys["client_id"].nil? or google_api_keys["client_secret"].nil?
16
+ raise APIKeyConfigurationError
17
+ else
18
+ set_client_keys
19
+ end
20
+ end
21
+ def load_yaml_file
22
+ ::YAML.load(File.read("#{::Rails.root}/config/google-api-keys.yml"))[::Rails.env]
23
+ end
24
+ def set_client_keys
25
+ self.client_id = google_api_keys["client_id"]
26
+ self.client_secret = google_api_keys["client_secret"]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,95 @@
1
+ require "glass/api_keys"
2
+ require "google/api_client"
3
+ module Glass
4
+ class Client
5
+ attr_accessor :access_token, :google_client, :mirror_api,
6
+ :google_account, :refresh_token, :content,
7
+ :mirror_content_type, :timeline_item, :has_expired_token
8
+
9
+
10
+
11
+
12
+ def self.create(timeline_item, opts={})
13
+ client = new(opts.merge({google_account: timeline_item.google_account}))
14
+ client.set_timeline_item(timeline_item)
15
+ client
16
+ end
17
+
18
+
19
+ def initialize(opts)
20
+ self.google_client = ::Google::APIClient.new
21
+ self.mirror_api = google_client.discovered_api("mirror", "v1")
22
+ self.google_account = opts[:google_account]
23
+
24
+ ### this isn't functional yet but this is an idea for
25
+ ### an api for those who wish to opt out of passing in a
26
+ ### google account, by passing in a hash of options
27
+ ###
28
+ ### the tricky aspect of this is how to handle the update
29
+ ### of the token information if the token is expired.
30
+
31
+ self.access_token = opts[:access_token] || google_account.try(:token)
32
+ self.refresh_token = opts[:refresh_token] || google_account.try(:refresh_token)
33
+ self.has_expired_token = opts[:has_expired_token] || google_account.has_expired_token?
34
+
35
+
36
+ setup_with_our_access_tokens
37
+ setup_with_user_access_token
38
+ self
39
+ end
40
+ def set_timeline_item(timeline_object)
41
+ self.timeline_item = timeline_object
42
+ self
43
+ end
44
+ def json_content
45
+ mirror_api.timeline.insert.request_schema.new(self.timeline_item.to_json)
46
+ end
47
+
48
+ ## optional parameter is merged into the content hash
49
+ ## before sending. good for specifying more application
50
+ ## specific stuff like speakableText parameters.
51
+
52
+ def insert(options={})
53
+ body_object = options[:content] || json_content
54
+ inserting_content = { api_method: mirror_api.timeline.insert,
55
+ body_object: body_object }.merge(options)
56
+ google_client.execute(inserting_content)
57
+ end
58
+
59
+ def delete(options={})
60
+ deleting_content = { api_method: mirror_api.timeline.delete,
61
+ parameters: options }
62
+ google_client.execute(deleting_content)
63
+ end
64
+
65
+ private
66
+ def setup_with_our_access_tokens
67
+ api_keys = Glass::ApiKeys.new
68
+ ["client_id", "client_secret"].each do |meth|
69
+ google_client.authorization.send("#{meth}=", api_keys.send(meth))
70
+ end
71
+ end
72
+ def setup_with_user_access_token
73
+ google_client.authorization.update_token!(access_token: access_token,
74
+ refresh_token: refresh_token)
75
+ update_token_if_necessary
76
+ end
77
+
78
+ def update_token_if_necessary
79
+ if self.has_expired_token
80
+ google_account.update_google_tokens(convert_user_data(google_client.authorization.fetch_access_token!))
81
+ end
82
+ end
83
+
84
+ def to_google_time(time)
85
+ Time.now.to_i + time
86
+ end
87
+ def convert_user_data(google_data_hash)
88
+ ea_data_hash = {}
89
+ ea_data_hash["token"] = google_data_hash["access_token"]
90
+ ea_data_hash["expires_at"] = to_google_time(google_data_hash["expires_in"])
91
+ ea_data_hash["id_token"] = google_data_hash["id_token"]
92
+ ea_data_hash
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,5 @@
1
+ module Glass
2
+ class Engine < ::Rails::Engine
3
+ config.glass = Glass
4
+ end
5
+ end
@@ -0,0 +1,33 @@
1
+ module Glass
2
+ class MenuItem
3
+ attr_accessor :action, :id, :display_name, :icon_url
4
+ BUILT_IN_ACTIONS = [:reply, :reply_all, :delete,
5
+ :share, :read_aloud, :voice_call,
6
+ :navigate, :toggle_pinned]
7
+
8
+
9
+ def self.create(action_sym, args)
10
+ args = BUILT_IN_ACTIONS.include?(action_sym) ? args : args.merge({id: action_sym})
11
+ new(args)
12
+ end
13
+
14
+ def initialize(opts={})
15
+ self.action = opts[:action] || "CUSTOM"
16
+ self.id = opts[:id]
17
+ self.display_name = opts[:display_name]
18
+ self.icon_url = opts[:icon_url]
19
+ end
20
+
21
+ def action
22
+ @action ||= "CUSTOM"
23
+ end
24
+
25
+ def serialize
26
+ hash = {action: action}
27
+ hash.merge!({id: id,
28
+ values: [{ displayName: display_name,
29
+ iconUrl: icon_url}]}) if action == "CUSTOM"
30
+ hash
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ module Glass
2
+ module Rails
3
+ VERSION="0.0.1"
4
+ end
5
+ end
@@ -0,0 +1,46 @@
1
+ module Glass
2
+ class Subscription
3
+ DEFAULT_COLLECTION = :timeline
4
+ DEFAULT_OPERATIONS = %w(UPDATE INSERT DELETE)#['UPDATE', 'INSERT', 'DELETE']
5
+
6
+ attr_accessor :google_account, :client, :google_client, :mirror_api
7
+
8
+ def initialize(opts)
9
+ self.google_account = opts[:google_account]
10
+ self.client = Glass::Client.new google_account: google_account
11
+ self.google_client = client.google_client
12
+ self.mirror_api = client.mirror_api
13
+ end
14
+
15
+ ## Insert a subscription
16
+ ## optional parameters:
17
+ ## collection can be :timeline or :locations
18
+ ## operation is an array of operations subscribed to. Valid values are 'UPDATE', 'INSERT', 'DELETE'
19
+ def insert(opts={})
20
+ subscription = mirror_api.subscriptions.insert.request_schema.new(
21
+ collection: opts[:collection] || DEFAULT_COLLECTION,
22
+ userToken: user_token,
23
+ verifyToken: verification_secret,
24
+ callbackUrl: callback_url,
25
+ operation: opts[:operations] || DEFAULT_OPERATIONS)
26
+ result = google_client.execute(api_method: mirror_api.subscriptions.insert,
27
+ body_object: subscription)
28
+ result
29
+ end
30
+
31
+ ## Must be HTTPS
32
+ def callback_url
33
+ Rails.application.routes.url_helpers.glass_notifications_callback_url(protocol: 'https')
34
+ end
35
+
36
+ ## Token string used to identify user in the callback
37
+ def user_token
38
+ google_account.id.to_s
39
+ end
40
+
41
+ ## Secret string used to verify that the callback is by Google
42
+ def verification_secret
43
+ google_account.verification_secret
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,75 @@
1
+ module Glass
2
+ class SubscriptionNotification
3
+ attr_accessor :google_account, :params, :collection,
4
+ :user_actions
5
+
6
+ class VerificationError < StandardError; end
7
+
8
+ def self.create(params)
9
+ notification = new(params)
10
+ notification.handle!
11
+ end
12
+ def initialize(params)
13
+ self.params = params
14
+ self.collection = params[:collection]
15
+ self.user_actions = params[:userActions]
16
+ self.google_account = find_google_account(params)
17
+ verify_authenticity!
18
+ end
19
+
20
+ ## Perform the corresponding notification actions
21
+ def handle!
22
+ if collection == "locations"
23
+ # TODO: This is a location update - should the GoogleAccount handle these updates?
24
+ # When your Glassware receives a location update, send a request to the glass.locations.get endpoint to retrieve the latest known location.
25
+ # Something like: google_account.handle_location_update
26
+ else
27
+ if has_user_action? :share
28
+ # TODO: Someone shared a card with this user's glassware. Who should handle this?
29
+ # The actual reply with attachments with itemId, so we need to fetch that
30
+ # Something like google_account.handle_shared_item(params)
31
+ elsif has_user_action? :reply
32
+ # TODO: Someone replied to a card.
33
+ # itemId => TimelineItem which contains at least: inReplyTo (original item),
34
+ # text (text transcription of reply), and attachments
35
+ else # Custom Action or DELETE
36
+ handle_action(params[:itemId])
37
+ end
38
+ end
39
+ end
40
+
41
+ private
42
+ def has_user_action?(action)
43
+ user_actions.select{|user_action| user_action["type"].downcase == action.to_s}.first
44
+ end
45
+
46
+ ## Handle actions on a timeline_item with a given id (custom actions, delete, etc.)
47
+ def handle_action(item_id)
48
+ timeline_item = find_timeline_item(item_id)
49
+
50
+ # TODO: Should we uniq these? When glass doesn't have connection, it will queue up
51
+ # actions, so users might press the same one multiple times.
52
+ user_actions.uniq.each do |user_action|
53
+ type = user_action[:type] == "CUSTOM" ? user_action[:payload] : user_action[:type]
54
+ timeline_item.send("handles_#{type.downcase}")
55
+ end if user_actions
56
+ end
57
+
58
+ ## Find the associated user from userToken
59
+ def find_google_account(params)
60
+ GoogleAccount.find params[:userToken]
61
+ end
62
+
63
+ ## Find a given timeline item owned by the user
64
+ def find_timeline_item(item_id)
65
+ Glass::TimelineItem.find_by_glass_item_id_and_google_account_id(item_id, google_account.id)
66
+ end
67
+
68
+ ## Verify authenticity of callback before doing anything
69
+ def verify_authenticity!
70
+ unless params[:verifyToken] == google_account.verification_secret
71
+ raise VerificationError.new("received: #{params[:verifyToken]}")
72
+ end
73
+ end
74
+ end
75
+ end