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.
- data/.DS_Store +0 -0
- data/.gitignore +19 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +172 -0
- data/Rakefile +1 -0
- data/glass-rails.gemspec +33 -0
- data/lib/.DS_Store +0 -0
- data/lib/generators/.DS_Store +0 -0
- data/lib/generators/glass.rb +22 -0
- data/lib/generators/glass/.DS_Store +0 -0
- data/lib/generators/glass/install/install_generator.rb +38 -0
- data/lib/generators/glass/install/templates/glass_timeline_item_migration.rb +21 -0
- data/lib/generators/glass/install/templates/google-oauth.yml +15 -0
- data/lib/generators/glass/install/templates/google_account.rb +28 -0
- data/lib/generators/glass/install/templates/initializer.rb +24 -0
- data/lib/generators/glass/install/templates/notifications_controller.rb +7 -0
- data/lib/generators/glass/model/model_generator.rb +19 -0
- data/lib/generators/glass/model/templates/model.rb +28 -0
- data/lib/generators/glass/templates/.DS_Store +0 -0
- data/lib/generators/glass/templates/templates/image_full.html.erb +9 -0
- data/lib/generators/glass/templates/templates/image_left_with_section_right.html.erb +8 -0
- data/lib/generators/glass/templates/templates/image_left_with_table_right.html.erb +11 -0
- data/lib/generators/glass/templates/templates/list.html.erb +7 -0
- data/lib/generators/glass/templates/templates/simple.html.erb +7 -0
- data/lib/generators/glass/templates/templates/table.html.erb +8 -0
- data/lib/generators/glass/templates/templates/two_column.html.erb +12 -0
- data/lib/generators/glass/templates/templates/two_column_with_emphasis_left.html.erb +14 -0
- data/lib/generators/glass/templates/templates_generator.rb +16 -0
- data/lib/glass-rails.rb +7 -0
- data/lib/glass.rb +24 -0
- data/lib/glass/.DS_Store +0 -0
- data/lib/glass/api_keys.rb +29 -0
- data/lib/glass/client.rb +95 -0
- data/lib/glass/engine.rb +5 -0
- data/lib/glass/menu_item.rb +33 -0
- data/lib/glass/rails/version.rb +5 -0
- data/lib/glass/subscription.rb +46 -0
- data/lib/glass/subscription_notification.rb +75 -0
- data/lib/glass/template.rb +45 -0
- data/lib/glass/timeline_item.rb +259 -0
- data/spec/models/glass/template_spec.rb +12 -0
- data/spec/models/glass/timeline_item_spec.rb +9 -0
- data/spec/spec_helper.rb +55 -0
- 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
|
Binary file
|
@@ -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
|
data/lib/glass-rails.rb
ADDED
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
|
data/lib/glass/.DS_Store
ADDED
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
|
data/lib/glass/client.rb
ADDED
@@ -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
|
data/lib/glass/engine.rb
ADDED
@@ -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,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
|