glass-rails 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|