wod 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/README.md +47 -0
- data/Rakefile +2 -0
- data/bin/wod +11 -0
- data/lib/wod.rb +6 -0
- data/lib/wod/client.rb +81 -0
- data/lib/wod/command.rb +59 -0
- data/lib/wod/commands/app.rb +15 -0
- data/lib/wod/commands/auth.rb +113 -0
- data/lib/wod/commands/base.rb +18 -0
- data/lib/wod/commands/devices.rb +50 -0
- data/lib/wod/commands/help.rb +76 -0
- data/lib/wod/commands/version.rb +7 -0
- data/lib/wod/helpers.rb +29 -0
- data/lib/wod/version.rb +3 -0
- data/wod.gemspec +23 -0
- metadata +83 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
The Wizard Of Dev center interacts with the Apple Dev Center web pages so you don't have to!
|
2
|
+
|
3
|
+
Install
|
4
|
+
---
|
5
|
+
|
6
|
+
$ gem install wod
|
7
|
+
|
8
|
+
Usage
|
9
|
+
---
|
10
|
+
$ wod help
|
11
|
+
|
12
|
+
=== General Commands
|
13
|
+
|
14
|
+
help # show this usage
|
15
|
+
version # show the gem version
|
16
|
+
|
17
|
+
login # log in with your apple credentials
|
18
|
+
logout # clear local authentication credentials
|
19
|
+
|
20
|
+
devices # list your registered devices
|
21
|
+
devices:add <name> <udid> # add a new device
|
22
|
+
devices:remove <name> # remove device (Still counts against device limit)
|
23
|
+
|
24
|
+
Examples
|
25
|
+
---
|
26
|
+
|
27
|
+
List Devices:
|
28
|
+
|
29
|
+
$ wod devices
|
30
|
+
Steve Jobs iPad 3 | 554f3fg54bc953547ry7a6bd62c678c11e912345
|
31
|
+
Jon Ive's iPhone 5 | 2d84d56ceg52c49379537413d3b9865ae2b12345
|
32
|
+
|
33
|
+
Add Device:
|
34
|
+
|
35
|
+
$ wod devices:add "Dave's iPod Touch" 2d84d56ceg52c49379537413d3b9865ae2b12345
|
36
|
+
|
37
|
+
Remove Device:
|
38
|
+
|
39
|
+
$ wod devices:remove "Jon Ive's iPhone 5"
|
40
|
+
|
41
|
+
|
42
|
+
Roadmap
|
43
|
+
---
|
44
|
+
|
45
|
+
More features to come as I need them or as other people fork and add em ;)
|
46
|
+
|
47
|
+
Proudly brought to you by the wizard of identity crises (whatupdave/snappycode/wizard of id)
|
data/Rakefile
ADDED
data/bin/wod
ADDED
data/lib/wod.rb
ADDED
data/lib/wod/client.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'mechanize'
|
2
|
+
require 'wod/helpers'
|
3
|
+
require 'wod/version'
|
4
|
+
|
5
|
+
module Wod
|
6
|
+
class DevCenterPage
|
7
|
+
attr_reader :page
|
8
|
+
|
9
|
+
def initialize page
|
10
|
+
@page = page
|
11
|
+
end
|
12
|
+
|
13
|
+
def logged_in?
|
14
|
+
(page.title =~ /sign in/i) == nil && (page.search("span").find {|s| s.text == "Log in"}) == nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def search arg
|
18
|
+
@page.search arg
|
19
|
+
end
|
20
|
+
|
21
|
+
def form arg
|
22
|
+
@page.form arg
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Client
|
27
|
+
include Wod::Helpers
|
28
|
+
|
29
|
+
attr_reader :name
|
30
|
+
|
31
|
+
def initialize username, password
|
32
|
+
@username = username
|
33
|
+
@password = password
|
34
|
+
end
|
35
|
+
|
36
|
+
def cookies_file
|
37
|
+
"#{home_directory}/.wod/cookie_jar"
|
38
|
+
end
|
39
|
+
|
40
|
+
def create_agent
|
41
|
+
agent = Mechanize.new
|
42
|
+
agent.cookie_jar.load cookies_file if File.exists?(cookies_file)
|
43
|
+
agent
|
44
|
+
end
|
45
|
+
|
46
|
+
def agent
|
47
|
+
@agent ||= create_agent
|
48
|
+
end
|
49
|
+
|
50
|
+
def login_and_reopen url
|
51
|
+
page = DevCenterPage.new agent.get("https://developer.apple.com/devcenter/ios/index.action")
|
52
|
+
|
53
|
+
unless page.logged_in?
|
54
|
+
puts "Creating session"
|
55
|
+
login_page = page.page.links.find { |l| l.text == 'Log in'}.click
|
56
|
+
|
57
|
+
f = login_page.form("appleConnectForm")
|
58
|
+
f.theAccountName = @username
|
59
|
+
f.theAccountPW = @password
|
60
|
+
f.submit
|
61
|
+
end
|
62
|
+
|
63
|
+
page = DevCenterPage.new agent.get url
|
64
|
+
raise InvalidCredentials unless page.logged_in?
|
65
|
+
agent.cookie_jar.save_as cookies_file
|
66
|
+
page
|
67
|
+
end
|
68
|
+
|
69
|
+
def get url
|
70
|
+
page = DevCenterPage.new agent.get(url)
|
71
|
+
page = login_and_reopen(url) unless page.logged_in?
|
72
|
+
page
|
73
|
+
end
|
74
|
+
|
75
|
+
def logged_in?
|
76
|
+
page = get "https://developer.apple.com/devcenter/ios/index.action"
|
77
|
+
page.logged_in?
|
78
|
+
end
|
79
|
+
|
80
|
+
end
|
81
|
+
end
|
data/lib/wod/command.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require 'wod/helpers'
|
2
|
+
require 'wod/commands/base'
|
3
|
+
|
4
|
+
Dir["#{File.dirname(__FILE__)}/commands/*.rb"].each { |c| require c }
|
5
|
+
|
6
|
+
module Wod
|
7
|
+
module Command
|
8
|
+
class InvalidCommand < RuntimeError; end
|
9
|
+
class CommandFailed < RuntimeError; end
|
10
|
+
|
11
|
+
extend Wod::Helpers
|
12
|
+
|
13
|
+
def self.run(command, args, retries=0)
|
14
|
+
begin
|
15
|
+
run_internal 'auth:reauthorize', args.dup if retries > 0
|
16
|
+
run_internal command, args.dup
|
17
|
+
rescue InvalidCommand
|
18
|
+
error "Unknown command. Run 'wod help' for usage information."
|
19
|
+
rescue Wod::InvalidCredentials
|
20
|
+
if retries < 3
|
21
|
+
STDERR.puts "Authentication failure"
|
22
|
+
run command, args, retries + 1
|
23
|
+
else
|
24
|
+
error "Authentication failure"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.run_internal(command, args, wod=nil)
|
31
|
+
klass, method = parse command
|
32
|
+
runner = klass.new args, wod
|
33
|
+
raise InvalidCommand unless runner.respond_to?(method)
|
34
|
+
runner.send method
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.parse(command)
|
38
|
+
parts = command.split(':')
|
39
|
+
case parts.size
|
40
|
+
when 1
|
41
|
+
begin
|
42
|
+
return eval("Wod::Command::#{command.capitalize}"), :index
|
43
|
+
rescue NameError, NoMethodError
|
44
|
+
return Wod::Command::App, command.to_sym
|
45
|
+
end
|
46
|
+
else
|
47
|
+
begin
|
48
|
+
const = Wod::Command
|
49
|
+
command = parts.pop
|
50
|
+
parts.each { |part| const = const.const_get(part.capitalize) }
|
51
|
+
return const, command.to_sym
|
52
|
+
rescue NameError
|
53
|
+
raise InvalidCommand
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module Wod::Command
|
4
|
+
class App < Base
|
5
|
+
def login
|
6
|
+
Wod::Command.run_internal "auth:reauthorize", args.dup
|
7
|
+
end
|
8
|
+
|
9
|
+
def logout
|
10
|
+
Wod::Command.run_internal "auth:delete_credentials", args.dup
|
11
|
+
puts "Local credentials cleared."
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module Wod::Command
|
4
|
+
class Auth < Base
|
5
|
+
attr_accessor :credentials
|
6
|
+
|
7
|
+
def client
|
8
|
+
@client = Wod::Client.new(user, password)
|
9
|
+
end
|
10
|
+
|
11
|
+
# just a stub; will raise if not authenticated
|
12
|
+
def check
|
13
|
+
client.logged_in?
|
14
|
+
end
|
15
|
+
|
16
|
+
def reauthorize
|
17
|
+
@credentials = ask_for_and_save_credentials
|
18
|
+
end
|
19
|
+
|
20
|
+
def user
|
21
|
+
get_credentials
|
22
|
+
@credentials[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
def password
|
26
|
+
get_credentials
|
27
|
+
@credentials[1]
|
28
|
+
end
|
29
|
+
|
30
|
+
def credentials_file
|
31
|
+
"#{home_directory}/.wod/credentials"
|
32
|
+
end
|
33
|
+
|
34
|
+
def get_credentials
|
35
|
+
return if @credentials
|
36
|
+
unless @credentials = read_credentials
|
37
|
+
ask_for_and_save_credentials
|
38
|
+
end
|
39
|
+
@credentials
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_credentials
|
43
|
+
File.exists?(credentials_file) and File.read(credentials_file).split("\n")
|
44
|
+
end
|
45
|
+
|
46
|
+
def echo_off
|
47
|
+
system "stty -echo"
|
48
|
+
end
|
49
|
+
|
50
|
+
def echo_on
|
51
|
+
system "stty echo"
|
52
|
+
end
|
53
|
+
|
54
|
+
def ask_for_credentials
|
55
|
+
puts "Enter your apple credentials"
|
56
|
+
|
57
|
+
print "Apple ID: "
|
58
|
+
user = ask
|
59
|
+
|
60
|
+
print "Password: "
|
61
|
+
password = ask_for_password
|
62
|
+
|
63
|
+
[user, password]
|
64
|
+
end
|
65
|
+
|
66
|
+
def ask_for_password
|
67
|
+
echo_off
|
68
|
+
password = ask
|
69
|
+
puts
|
70
|
+
echo_on
|
71
|
+
return password
|
72
|
+
end
|
73
|
+
|
74
|
+
def ask_for_and_save_credentials
|
75
|
+
begin
|
76
|
+
@credentials = ask_for_credentials
|
77
|
+
write_credentials
|
78
|
+
check
|
79
|
+
rescue ::Wod::InvalidCredentials
|
80
|
+
delete_credentials
|
81
|
+
@client = nil
|
82
|
+
@credentials = nil
|
83
|
+
puts "Authentication failed."
|
84
|
+
return if retry_login?
|
85
|
+
exit 1
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def retry_login?
|
90
|
+
@login_attempts ||= 0
|
91
|
+
@login_attempts += 1
|
92
|
+
@login_attempts < 3
|
93
|
+
end
|
94
|
+
|
95
|
+
def write_credentials
|
96
|
+
FileUtils.mkdir_p(File.dirname(credentials_file))
|
97
|
+
f = File.open(credentials_file, 'w')
|
98
|
+
f.chmod(0600)
|
99
|
+
f.puts self.credentials
|
100
|
+
f.close
|
101
|
+
set_credentials_permissions
|
102
|
+
end
|
103
|
+
|
104
|
+
def set_credentials_permissions
|
105
|
+
FileUtils.chmod 0700, File.dirname(credentials_file)
|
106
|
+
FileUtils.chmod 0600, credentials_file
|
107
|
+
end
|
108
|
+
|
109
|
+
def delete_credentials
|
110
|
+
FileUtils.rm_rf "#{home_directory}/.wod/"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
module Wod::Command
|
4
|
+
class Base
|
5
|
+
include Wod::Helpers
|
6
|
+
|
7
|
+
attr_accessor :args
|
8
|
+
|
9
|
+
def initialize(args, wod=nil)
|
10
|
+
@args = args
|
11
|
+
@wod = wod
|
12
|
+
end
|
13
|
+
|
14
|
+
def wod
|
15
|
+
@wod ||= Wod::Command.run_internal('auth:client', args)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
|
2
|
+
module Wod::Command
|
3
|
+
class Devices < Base
|
4
|
+
def index
|
5
|
+
page = wod.get "https://developer.apple.com/ios/manage/devices/index.action"
|
6
|
+
|
7
|
+
names = page.search("td.name span").map(&:text)
|
8
|
+
udids = page.search("td.id").map(&:text)
|
9
|
+
|
10
|
+
devices_left = page.search(".devicesannounce strong").first.text
|
11
|
+
devices = names.map.with_index{|name, i| {:name => name, :udid => udids[i] } }
|
12
|
+
|
13
|
+
display_formatted devices, [:name, :udid]
|
14
|
+
puts
|
15
|
+
puts "#{devices.size} devices registered. #{devices_left}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def add
|
19
|
+
name = args.shift
|
20
|
+
udid = args.shift
|
21
|
+
|
22
|
+
page = wod.get "https://developer.apple.com/ios/manage/devices/add.action"
|
23
|
+
|
24
|
+
form = page.form "add"
|
25
|
+
form["deviceNameList[0]"] = name
|
26
|
+
form["deviceNumberList[0]"] = udid
|
27
|
+
|
28
|
+
form.submit
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove
|
32
|
+
name = args.shift
|
33
|
+
|
34
|
+
page = wod.get "http://developer.apple.com/ios/manage/devices/index.action"
|
35
|
+
|
36
|
+
device_span = page.search("span:contains('#{name}')")
|
37
|
+
if device_span.empty?
|
38
|
+
error "Device not found"
|
39
|
+
end
|
40
|
+
|
41
|
+
tr = device_span.first.parent.parent
|
42
|
+
row_identifier = tr.search("input[name='__checkbox_selectedValues']").first[:value]
|
43
|
+
|
44
|
+
form = page.form "removeDevice"
|
45
|
+
checkbox = form.checkboxes.find {|c| c[:value] == row_identifier}
|
46
|
+
checkbox.check
|
47
|
+
form.submit
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module Wod::Command
|
2
|
+
class Help < Base
|
3
|
+
class HelpGroup < Array
|
4
|
+
attr_reader :title
|
5
|
+
|
6
|
+
def initialize(title)
|
7
|
+
@title = title
|
8
|
+
end
|
9
|
+
|
10
|
+
def command(name, description)
|
11
|
+
self << [name, description]
|
12
|
+
end
|
13
|
+
|
14
|
+
def space
|
15
|
+
self << ['', '']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.groups
|
20
|
+
@groups ||= []
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.group(title, &block)
|
24
|
+
groups << begin
|
25
|
+
group = HelpGroup.new(title)
|
26
|
+
yield group
|
27
|
+
group
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.create_default_groups!
|
32
|
+
return if @defaults_created
|
33
|
+
@defaults_created = true
|
34
|
+
group 'General Commands' do |group|
|
35
|
+
group.command 'help', 'show this usage'
|
36
|
+
group.command 'version', 'show the gem version'
|
37
|
+
group.space
|
38
|
+
group.command 'login', 'log in with your apple credentials'
|
39
|
+
group.command 'logout', 'clear local authentication credentials'
|
40
|
+
group.space
|
41
|
+
group.command 'devices', 'list your registered devices'
|
42
|
+
group.command 'devices:add <name> <udid>', 'add a new device'
|
43
|
+
group.command 'devices:remove <name>', 'remove device (Still counts against device limit)'
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def index
|
48
|
+
puts usage
|
49
|
+
end
|
50
|
+
|
51
|
+
def usage
|
52
|
+
longest_command_length = self.class.groups.map do |group|
|
53
|
+
group.map { |g| g.first.length }
|
54
|
+
end.flatten.max
|
55
|
+
|
56
|
+
self.class.groups.inject(StringIO.new) do |output, group|
|
57
|
+
output.puts "=== %s" % group.title
|
58
|
+
output.puts
|
59
|
+
|
60
|
+
group.each do |command, description|
|
61
|
+
if command.empty?
|
62
|
+
output.puts
|
63
|
+
else
|
64
|
+
output.puts "%-*s # %s" % [longest_command_length, command, description]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
output.puts
|
69
|
+
output
|
70
|
+
end.string
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
Wod::Command::Help.create_default_groups!
|
data/lib/wod/helpers.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Wod
|
2
|
+
module Helpers
|
3
|
+
def home_directory
|
4
|
+
ENV['HOME']
|
5
|
+
end
|
6
|
+
|
7
|
+
def error(msg)
|
8
|
+
STDERR.puts(msg)
|
9
|
+
exit 1
|
10
|
+
end
|
11
|
+
|
12
|
+
def ask
|
13
|
+
gets.strip
|
14
|
+
end
|
15
|
+
|
16
|
+
def display_formatted hashes, columns
|
17
|
+
column_lengths = columns.map do |c|
|
18
|
+
hashes.map { |h| h[c].length }.max
|
19
|
+
end
|
20
|
+
sorted = hashes.sort_by {|h| h[columns.first] }
|
21
|
+
|
22
|
+
sorted.each.with_index do |h, i|
|
23
|
+
puts " " + columns.map.with_index{ |c, i| h[c].ljust(column_lengths[i]) }.join(" | ")
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
data/lib/wod/version.rb
ADDED
data/wod.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "wod/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "wod"
|
7
|
+
s.version = Wod::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Dave Newman"]
|
10
|
+
s.email = ["dave@whatupdave.com"]
|
11
|
+
s.homepage = "http://github.com/snappycode/wod"
|
12
|
+
s.summary = %q{The Wizard Of Dev center}
|
13
|
+
s.description = %q{Command line tool for interacting with the Apple Dev Center}
|
14
|
+
|
15
|
+
s.rubyforge_project = "wod"
|
16
|
+
|
17
|
+
s.add_dependency "mechanize"
|
18
|
+
|
19
|
+
s.files = `git ls-files`.split("\n")
|
20
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
21
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
22
|
+
s.require_paths = ["lib"]
|
23
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: wod
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.0.1
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dave Newman
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2011-04-01 00:00:00 +11:00
|
14
|
+
default_executable:
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: mechanize
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
description: Command line tool for interacting with the Apple Dev Center
|
28
|
+
email:
|
29
|
+
- dave@whatupdave.com
|
30
|
+
executables:
|
31
|
+
- wod
|
32
|
+
extensions: []
|
33
|
+
|
34
|
+
extra_rdoc_files: []
|
35
|
+
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- Gemfile
|
39
|
+
- README.md
|
40
|
+
- Rakefile
|
41
|
+
- bin/wod
|
42
|
+
- lib/wod.rb
|
43
|
+
- lib/wod/client.rb
|
44
|
+
- lib/wod/command.rb
|
45
|
+
- lib/wod/commands/app.rb
|
46
|
+
- lib/wod/commands/auth.rb
|
47
|
+
- lib/wod/commands/base.rb
|
48
|
+
- lib/wod/commands/devices.rb
|
49
|
+
- lib/wod/commands/help.rb
|
50
|
+
- lib/wod/commands/version.rb
|
51
|
+
- lib/wod/helpers.rb
|
52
|
+
- lib/wod/version.rb
|
53
|
+
- wod.gemspec
|
54
|
+
has_rdoc: true
|
55
|
+
homepage: http://github.com/snappycode/wod
|
56
|
+
licenses: []
|
57
|
+
|
58
|
+
post_install_message:
|
59
|
+
rdoc_options: []
|
60
|
+
|
61
|
+
require_paths:
|
62
|
+
- lib
|
63
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
70
|
+
none: false
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: "0"
|
75
|
+
requirements: []
|
76
|
+
|
77
|
+
rubyforge_project: wod
|
78
|
+
rubygems_version: 1.5.3
|
79
|
+
signing_key:
|
80
|
+
specification_version: 3
|
81
|
+
summary: The Wizard Of Dev center
|
82
|
+
test_files: []
|
83
|
+
|