glass-rails 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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