ki 0.4.10 → 0.4.11
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.
- checksums.yaml +4 -4
- data/Gemfile.lock +16 -8
- data/MIDDLEWARE.md +27 -7
- data/README.md +45 -2
- data/REALTIME.md +48 -0
- data/TODO.md +19 -0
- data/ki.gemspec +1 -0
- data/lib/ki.rb +5 -0
- data/lib/ki/base_request.rb +7 -0
- data/lib/ki/channel_manager.rb +58 -0
- data/lib/ki/ki.rb +1 -0
- data/lib/ki/ki_cli.rb +1 -1
- data/lib/ki/ki_config.rb +18 -2
- data/lib/ki/middleware/api_handler.rb +11 -16
- data/lib/ki/middleware/helpers/redirect_to_helper.rb +13 -0
- data/lib/ki/middleware/realtime.rb +81 -0
- data/lib/ki/model.rb +6 -4
- data/lib/ki/modules/restrictions.rb +4 -7
- data/lib/ki/orm.rb +38 -2
- data/lib/ki/utils/redirect_to_helper.rb +0 -0
- data/lib/ki/version.rb +1 -1
- data/spec/examples/json.northpole.ro/.bowerrc +3 -0
- data/spec/examples/json.northpole.ro/Gemfile +1 -0
- data/spec/examples/json.northpole.ro/bower.json +17 -0
- data/spec/examples/json.northpole.ro/config.ru +3 -0
- data/spec/examples/json.northpole.ro/config.yml +4 -0
- data/spec/examples/json.northpole.ro/config.yml.backup +18 -0
- data/spec/examples/json.northpole.ro/config/deploy.rb +2 -1
- data/spec/examples/json.northpole.ro/config/deploy/production.rb +4 -38
- data/spec/examples/json.northpole.ro/public/app/.bowerrc +3 -0
- data/spec/examples/json.northpole.ro/public/app/.gitignore +9 -0
- data/spec/examples/json.northpole.ro/public/app/.jshintrc +13 -0
- data/spec/examples/json.northpole.ro/public/app/.travis.yml +14 -0
- data/spec/examples/json.northpole.ro/public/app/LICENSE +22 -0
- data/spec/examples/json.northpole.ro/public/app/README.md +82 -0
- data/spec/examples/json.northpole.ro/public/app/bower.json +20 -0
- data/spec/examples/json.northpole.ro/public/app/e2e/pages/ContactUser.js +22 -0
- data/spec/examples/json.northpole.ro/public/app/e2e/pages/UserDetails.js +11 -0
- data/spec/examples/json.northpole.ro/public/app/e2e/pages/UserList.js +12 -0
- data/spec/examples/json.northpole.ro/public/app/e2e/protractor.conf.js +26 -0
- data/spec/examples/json.northpole.ro/public/app/e2e/scenarios/users.js +34 -0
- data/spec/examples/json.northpole.ro/public/app/gulpfile.js +104 -0
- data/spec/examples/json.northpole.ro/public/app/karma.conf.js +35 -0
- data/spec/examples/json.northpole.ro/public/app/package.json +30 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/avatar-1.svg +11 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/avatar-4.svg +16 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/avatars.svg +244 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/google_plus.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/hangouts.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/ic_fullscreen_48px.svg +4 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/ic_fullscreen_exit_48px.svg +4 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/ic_music_note_48px.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/ic_note_add_48px.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/ic_view_list_48px.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/icon.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/mail.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/manggo.svg +1095 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/menu.svg +4 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/phone.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/share.svg +3 -0
- data/spec/examples/json.northpole.ro/public/app/src/assets/svg/twitter.svg +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/css/app.css +138 -0
- data/spec/examples/json.northpole.ro/public/app/src/css/app.css.map +7 -0
- data/spec/examples/json.northpole.ro/public/app/src/css/app.sass +145 -0
- data/spec/examples/json.northpole.ro/public/app/src/index.html +157 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/MainController.coffee +167 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/app.coffee +58 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/blobs/BlobsController.coffee +115 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/blobs/blobs.html +48 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/tutorial/tutorial.html +15 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/users/UserService.coffee +12 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/users/Users.coffee +1 -0
- data/spec/examples/json.northpole.ro/public/app/src/js/users/users.html +8 -0
- data/spec/examples/json.northpole.ro/public/javascripts/jnorthpole.coffee +40 -7
- data/spec/examples/json.northpole.ro/public/javascripts/realtime.coffee +28 -0
- data/spec/examples/json.northpole.ro/public/{javascripts/music → music}/angular-youtube-embed.js +0 -0
- data/spec/examples/json.northpole.ro/public/music/index.html +126 -0
- data/spec/examples/json.northpole.ro/public/music/music.coffee +99 -0
- data/spec/examples/json.northpole.ro/public/music/music.sass +63 -0
- data/spec/examples/json.northpole.ro/public/stylesheets/app.sass +3 -0
- data/spec/examples/json.northpole.ro/views/faq.haml +7 -0
- data/spec/examples/json.northpole.ro/views/layout.haml +1 -0
- data/spec/examples/json.northpole.ro/views/music.haml +2 -0
- data/spec/examples/json.northpole.ro/views/websocket.haml +7 -0
- data/spec/lib/ki/channel_manager_spec.rb +82 -0
- data/spec/lib/ki/helpers_spec.rb +11 -0
- data/spec/lib/ki/ki_config_spec.rb +28 -0
- data/spec/lib/ki/middleware/admin_generator_spec.rb +8 -0
- data/spec/lib/ki/middleware/init_middleware_spec.rb +21 -0
- data/spec/lib/ki/middleware/realtime_spec.rb +96 -0
- data/spec/lib/ki/model_spec.rb +28 -7
- data/spec/lib/ki/modules/model_helper_spec.rb +31 -0
- data/spec/lib/ki/orm_spec.rb +26 -0
- metadata +211 -9
- data/spec/examples/json.northpole.ro/public/javascripts/docs.min.js +0 -16
- data/spec/examples/json.northpole.ro/views/awsum.haml +0 -108
@@ -9,6 +9,7 @@ module Ki
|
|
9
9
|
# json output from the url, instead it will redirect to the url given
|
10
10
|
class ApiHandler
|
11
11
|
include BaseMiddleware
|
12
|
+
include Helpers::RedirectTo
|
12
13
|
|
13
14
|
def call(env)
|
14
15
|
req = BaseRequest.new env
|
@@ -26,26 +27,20 @@ module Ki
|
|
26
27
|
fail InvalidUrlError.new("invalid url '#{req.path}'", 404)
|
27
28
|
end
|
28
29
|
|
29
|
-
model = klass.new(req
|
30
|
-
|
31
|
-
render model
|
32
|
-
else
|
33
|
-
redirect_to req.params['redirect_to'] # TODO: check for injection
|
34
|
-
end
|
30
|
+
model = klass.new(req)
|
31
|
+
render model
|
35
32
|
rescue ApiError => e
|
36
33
|
render e
|
37
34
|
end
|
38
35
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
resp['Content-Type'] = 'application/json'
|
48
|
-
resp.finish
|
36
|
+
def render(model)
|
37
|
+
if model.is_a?(ApiError) || model.params['redirect_to'].nil?
|
38
|
+
resp = Rack::Response.new(model.result.to_json, model.status)
|
39
|
+
resp['Content-Type'] = 'application/json'
|
40
|
+
resp.finish
|
41
|
+
else
|
42
|
+
redirect_to model.params['redirect_to']
|
43
|
+
end
|
49
44
|
end
|
50
45
|
end
|
51
46
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module Ki
|
2
|
+
module Middleware #:nodoc:
|
3
|
+
class Realtime
|
4
|
+
include BaseMiddleware
|
5
|
+
|
6
|
+
def call(env)
|
7
|
+
req = BaseRequest.new env
|
8
|
+
if req.path.to_s == '/realtime/info'
|
9
|
+
show_stats
|
10
|
+
elsif req.path.to_s == '/realtime' && Faye::WebSocket.websocket?(env)
|
11
|
+
handle_websocket env
|
12
|
+
else
|
13
|
+
@app.call env
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def ws_send(ws, hash)
|
18
|
+
ws.send(hash.to_json.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
def show_stats
|
22
|
+
hash = {
|
23
|
+
sockets: ::Ki::ChannelManager.sockets
|
24
|
+
}
|
25
|
+
resp = Rack::Response.new(hash.to_json, 200)
|
26
|
+
resp['Content-Type'] = 'application/json'
|
27
|
+
resp.finish
|
28
|
+
end
|
29
|
+
|
30
|
+
def handle_websocket(env)
|
31
|
+
ws = Faye::WebSocket.new(env)
|
32
|
+
|
33
|
+
socket = ::Ki::ChannelManager.connect
|
34
|
+
ws_send(ws, socket)
|
35
|
+
|
36
|
+
ws.on :message do |event|
|
37
|
+
on_message(ws, socket, event.data)
|
38
|
+
end
|
39
|
+
|
40
|
+
timer = EventMachine::PeriodicTimer.new(1) do
|
41
|
+
msgs = ::Ki::ChannelManager.tick(socket_id: socket['id'])
|
42
|
+
ws_send(ws, { messages: msgs }) if msgs.count > 0
|
43
|
+
end
|
44
|
+
|
45
|
+
ws.on :close do # |event|
|
46
|
+
timer.cancel
|
47
|
+
::Ki::ChannelManager.disconnect socket
|
48
|
+
ws = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
ws.rack_response
|
52
|
+
end
|
53
|
+
|
54
|
+
def on_message(ws, socket, data)
|
55
|
+
json = JSON.parse(data)
|
56
|
+
json['socket_id'] = socket['id']
|
57
|
+
if json['type'] == 'subscribe'
|
58
|
+
channel_manager_action(json, ws, 'subscribe')
|
59
|
+
elsif json['type'] == 'unsubscribe'
|
60
|
+
output = ::Ki::ChannelManager.unsubscribe json
|
61
|
+
ws_send(ws, output)
|
62
|
+
elsif json['type'] == 'publish'
|
63
|
+
channel_manager_action(json, ws, 'publish')
|
64
|
+
else
|
65
|
+
ws_send(ws, { message: 'Please specify a valid type' })
|
66
|
+
end
|
67
|
+
rescue JSON::ParserError
|
68
|
+
ws_send(ws, { message: 'Please send a valid json string' })
|
69
|
+
end
|
70
|
+
|
71
|
+
def channel_manager_action(json, ws, action)
|
72
|
+
if json['channel_name']
|
73
|
+
output = ::Ki::ChannelManager.send(action, json)
|
74
|
+
ws_send(ws, output)
|
75
|
+
else
|
76
|
+
ws_send(ws, { message: 'Please specify a channel_name' })
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
data/lib/ki/model.rb
CHANGED
@@ -4,12 +4,14 @@ module Ki
|
|
4
4
|
extend Restrictions
|
5
5
|
include Callbacks
|
6
6
|
include ModelHelper
|
7
|
+
include Middleware::Helpers::RedirectTo
|
7
8
|
|
8
|
-
attr_accessor :action, :result, :params, :status
|
9
|
+
attr_accessor :action, :result, :params, :status, :req
|
9
10
|
|
10
|
-
def initialize(
|
11
|
-
@
|
12
|
-
@
|
11
|
+
def initialize(req)
|
12
|
+
@req = req
|
13
|
+
@action = req.to_action
|
14
|
+
@params = req.params
|
13
15
|
@status = 200
|
14
16
|
|
15
17
|
fail ForbiddenAction if forbidden_actions.include? @action
|
@@ -28,13 +28,10 @@ module Ki
|
|
28
28
|
private
|
29
29
|
|
30
30
|
def generic_restriction(method_name, attributes)
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
eigen_class = class << self; self; end
|
36
|
-
eigen_class.send(:define_method, method_name) do
|
37
|
-
attributes
|
31
|
+
[:define_method, :define_singleton_method].each do |definition_means|
|
32
|
+
send definition_means, method_name do
|
33
|
+
attributes
|
34
|
+
end
|
38
35
|
end
|
39
36
|
end
|
40
37
|
end
|
data/lib/ki/orm.rb
CHANGED
@@ -103,7 +103,14 @@ module Ki
|
|
103
103
|
#
|
104
104
|
def find(name, hash = {})
|
105
105
|
hash = nourish_hash_id hash
|
106
|
-
|
106
|
+
a = nourish_hash_limit hash
|
107
|
+
hash = a[0]
|
108
|
+
limit = a[1]
|
109
|
+
a = nourish_hash_sort hash
|
110
|
+
hash = a[0]
|
111
|
+
sort = a[1]
|
112
|
+
|
113
|
+
@db[name].find(hash, limit).sort(sort).to_a.stringify_ids
|
107
114
|
end
|
108
115
|
|
109
116
|
# Update a hash from the database
|
@@ -156,7 +163,7 @@ module Ki
|
|
156
163
|
hash = nourish_hash_id hash
|
157
164
|
r = @db[name].remove hash
|
158
165
|
{
|
159
|
-
deleted_item_count: r[
|
166
|
+
deleted_item_count: r['n'] || 0
|
160
167
|
}
|
161
168
|
end
|
162
169
|
|
@@ -198,6 +205,35 @@ module Ki
|
|
198
205
|
end
|
199
206
|
hash
|
200
207
|
end
|
208
|
+
|
209
|
+
def nourish_hash_limit(hash)
|
210
|
+
tmp = {}
|
211
|
+
if hash['__limit']
|
212
|
+
# really need to work on hash_with_indifferent access
|
213
|
+
# if you change how you access the symbol you will have a bad time
|
214
|
+
tmp[:limit] = hash['__limit']
|
215
|
+
hash.delete('__limit')
|
216
|
+
end
|
217
|
+
[hash, tmp]
|
218
|
+
end
|
219
|
+
|
220
|
+
def nourish_hash_sort(hash)
|
221
|
+
tmp = {}
|
222
|
+
if hash['__sort']
|
223
|
+
if hash['__sort'].class != Hash
|
224
|
+
tmp = {}
|
225
|
+
else
|
226
|
+
# TODO: validate for size and number of elements
|
227
|
+
# TODO: validate value
|
228
|
+
# TODO: handle sorting by id
|
229
|
+
hash['__sort'].to_a.each do |e|
|
230
|
+
tmp[e[0].to_sym] = e[1].to_sym
|
231
|
+
end
|
232
|
+
end
|
233
|
+
hash.delete('__sort')
|
234
|
+
end
|
235
|
+
[hash, tmp]
|
236
|
+
end
|
201
237
|
end
|
202
238
|
end
|
203
239
|
end
|
File without changes
|
data/lib/ki/version.rb
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
{
|
2
|
+
"name": "json.northpole.ro",
|
3
|
+
"version": "0.4.10",
|
4
|
+
"homepage": "https://github.com/mess110/ki",
|
5
|
+
"authors": [
|
6
|
+
"Cristian Mircea Messel <mess110@gmail.com>"
|
7
|
+
],
|
8
|
+
"license": "MIT",
|
9
|
+
"ignore": [
|
10
|
+
"**/.*",
|
11
|
+
"node_modules",
|
12
|
+
"bower_components",
|
13
|
+
"public/components/",
|
14
|
+
"test",
|
15
|
+
"tests"
|
16
|
+
]
|
17
|
+
}
|
@@ -2,7 +2,9 @@ development:
|
|
2
2
|
database:
|
3
3
|
name: np_development
|
4
4
|
host: 127.0.0.1
|
5
|
+
# host: 172.17.0.3
|
5
6
|
port: 27017
|
7
|
+
add_middleware: 'Realtime'
|
6
8
|
|
7
9
|
test:
|
8
10
|
database:
|
@@ -14,4 +16,6 @@ production:
|
|
14
16
|
database:
|
15
17
|
name: np
|
16
18
|
host: 127.0.0.1
|
19
|
+
# host: 172.17.0.3
|
17
20
|
port: 27017
|
21
|
+
add_middleware: 'Realtime'
|
@@ -0,0 +1,18 @@
|
|
1
|
+
development:
|
2
|
+
database:
|
3
|
+
name: np_development
|
4
|
+
host: 172.17.0.3
|
5
|
+
port: 27017
|
6
|
+
|
7
|
+
test:
|
8
|
+
database:
|
9
|
+
name: np_test
|
10
|
+
host: 127.0.0.1
|
11
|
+
port: 27017
|
12
|
+
|
13
|
+
production:
|
14
|
+
database:
|
15
|
+
name: np
|
16
|
+
host: 127.0.0.1
|
17
|
+
port: 27017
|
18
|
+
add_middleware: 'Realtime'
|
@@ -30,7 +30,7 @@ set :deploy_to, '/home/kiki/json.northpole.ro'
|
|
30
30
|
|
31
31
|
# Default value for default_env is {}
|
32
32
|
# set :default_env, { path: "/opt/ruby/bin:$PATH" }
|
33
|
-
set :rvm_ruby_version, '2.1.
|
33
|
+
set :rvm_ruby_version, '2.1.2@json.northpole.ro'
|
34
34
|
|
35
35
|
# Default value for keep_releases is 5
|
36
36
|
# set :keep_releases, 5
|
@@ -42,6 +42,7 @@ namespace :deploy do
|
|
42
42
|
on roles(:app), in: :sequence, wait: 5 do
|
43
43
|
# Your restart mechanism here, for example:
|
44
44
|
# execute :touch, release_path.join('tmp/restart.txt')
|
45
|
+
# run "#{sudo} service nginx #{command}"
|
45
46
|
end
|
46
47
|
end
|
47
48
|
|
@@ -1,39 +1,5 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
# server in each group is considered to be the first
|
5
|
-
# unless any hosts have the primary property set.
|
6
|
-
# Don't declare `role :all`, it's a meta role
|
7
|
-
role :app, %w{kiki@northpole.ro}
|
8
|
-
role :web, %w{kiki@northpole.ro}
|
9
|
-
role :db, %w{kiki@northpole.ro}
|
1
|
+
role :app, %w{mace@glassic.northpole.ro}
|
2
|
+
role :web, %w{mace@glassic.northpole.ro}
|
3
|
+
role :db, %w{mace@glassic.northpole.ro}
|
10
4
|
|
11
|
-
|
12
|
-
# ======================
|
13
|
-
# This can be used to drop a more detailed server
|
14
|
-
# definition into the server list. The second argument
|
15
|
-
# something that quacks like a hash can be used to set
|
16
|
-
# extended properties on the server.
|
17
|
-
# server 'example.com', user: 'deploy', roles: %w{web app}, my_property: :my_value
|
18
|
-
|
19
|
-
# you can set custom ssh options
|
20
|
-
# it's possible to pass any option but you need to keep in mind that net/ssh understand limited list of options
|
21
|
-
# you can see them in [net/ssh documentation](http://net-ssh.github.io/net-ssh/classes/Net/SSH.html#method-c-start)
|
22
|
-
# set it globally
|
23
|
-
# set :ssh_options, {
|
24
|
-
# keys: %w(/home/rlisowski/.ssh/id_rsa),
|
25
|
-
# forward_agent: false,
|
26
|
-
# auth_methods: %w(password)
|
27
|
-
# }
|
28
|
-
# and/or per server
|
29
|
-
# server 'example.com',
|
30
|
-
# user: 'user_name',
|
31
|
-
# roles: %w{web app},
|
32
|
-
# ssh_options: {
|
33
|
-
# user: 'user_name', # overrides user setting above
|
34
|
-
# keys: %w(/home/user_name/.ssh/id_rsa),
|
35
|
-
# forward_agent: false,
|
36
|
-
# auth_methods: %w(publickey password)
|
37
|
-
# # password: 'please use keys'
|
38
|
-
# }
|
39
|
-
# setting per server overrides global ssh_options
|
5
|
+
set :deploy_to, '/home/mace/json.northpole.ro'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
language: node_js
|
2
|
+
node_js:
|
3
|
+
- "0.10"
|
4
|
+
|
5
|
+
before_script:
|
6
|
+
- export DISPLAY=:99.0
|
7
|
+
- sh -e /etc/init.d/xvfb start
|
8
|
+
- npm start > /dev/null &
|
9
|
+
- npm run update-webdriver
|
10
|
+
- sleep 1 # give server time to start
|
11
|
+
|
12
|
+
script:
|
13
|
+
- node_modules/.bin/karma start karma.conf.js --no-auto-watch --single-run --reporters=dots --browsers=Firefox
|
14
|
+
- node_modules/.bin/protractor e2e-tests/protractor.conf.js --browser=firefox
|
@@ -0,0 +1,22 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2010-2014 Google, Inc. http://angularjs.org
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
22
|
+
|