tact 1.2.23 → 2.0.17
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/.gitignore +1 -0
- data/README.md +2 -1
- data/Rakefile +95 -2
- data/bin/tact +1 -1
- data/db/migrate/20170515014719_create_tables.rb +17 -0
- data/db/migrate/20170515015242_add_foreign_keys.rb +6 -0
- data/db/migrate/20170515130759_change_type_on_phone_numbers.rb +5 -0
- data/lib/tact.rb +50 -1
- data/lib/tact/authorizable.rb +10 -0
- data/lib/tact/card.rb +3 -7
- data/lib/tact/contact.rb +8 -70
- data/lib/tact/email.rb +5 -52
- data/lib/tact/google_client.rb +156 -0
- data/lib/tact/phone_number.rb +12 -69
- data/lib/tact/rolodex.rb +45 -32
- data/lib/tact/tact.rb +35 -23
- data/lib/tact/version.rb +1 -1
- data/tact.gemspec +9 -0
- metadata +55 -6
- data/lib/tact/database.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0c97568e1e4da1c76294dd69bdfc31af58cd5ded
|
4
|
+
data.tar.gz: 887f421efa7a7fb0a5272f798706518403a4fbb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d65d88384b495953b11699693eae7179b22a0af7cdfa818079768da6b26e8f7e7e72d1a6046eb1350eb0eec9e404721956a471906860dd4c7ae75400cf78adfd
|
7
|
+
data.tar.gz: 611fcd81f8c5d8d0b451065607195adde2380be76db5640dd0f3d908f2f4133d83b12fce69cc33a97ecee86caac9f34e594383e82463661a42e0bc4a41ee2da8
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -11,6 +11,7 @@ A command line rolodex.
|
|
11
11
|
```
|
12
12
|
-v Current version
|
13
13
|
-h Help
|
14
|
+
-s Sync Google contacts
|
14
15
|
|
15
16
|
<param> Search by name
|
16
17
|
-p <param> Search by number
|
@@ -28,7 +29,7 @@ A command line rolodex.
|
|
28
29
|
|
29
30
|
## Contributing
|
30
31
|
|
31
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
32
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/olishmollie/tact.
|
32
33
|
|
33
34
|
|
34
35
|
## License
|
data/Rakefile
CHANGED
@@ -1,2 +1,95 @@
|
|
1
|
-
require
|
2
|
-
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
task :environment do
|
5
|
+
require 'tact'
|
6
|
+
end
|
7
|
+
|
8
|
+
namespace :generate do
|
9
|
+
desc "Create an empty migration in db/migrate, e.g., rake generate:migration NAME=create_tasks"
|
10
|
+
task :migration => :environment do
|
11
|
+
unless ENV.has_key?('NAME')
|
12
|
+
raise "Must specificy migration name, e.g., rake generate:migration NAME=create_tasks"
|
13
|
+
end
|
14
|
+
|
15
|
+
name = ENV['NAME'].camelize
|
16
|
+
filename = "%s_%s.rb" % [Time.now.strftime('%Y%m%d%H%M%S'), ENV['NAME'].underscore]
|
17
|
+
path = File.join('db', 'migrate', filename)
|
18
|
+
|
19
|
+
if File.exist?(path)
|
20
|
+
raise "ERROR: File '#{path}' already exists"
|
21
|
+
end
|
22
|
+
|
23
|
+
puts "Creating #{path}"
|
24
|
+
File.open(path, 'w+') do |f|
|
25
|
+
f.write(<<-EOF.strip_heredoc)
|
26
|
+
class #{name} < ActiveRecord::Migration
|
27
|
+
def change
|
28
|
+
end
|
29
|
+
end
|
30
|
+
EOF
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
task :make_tact_dir do
|
36
|
+
if !File.exists?("#{File.expand_path('~')}/.tact")
|
37
|
+
FileUtils.mkdir("#{File.expand_path('~')}/.tact")
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
namespace :db do
|
42
|
+
desc "Create databases"
|
43
|
+
task :create => [:environment, :make_tact_dir] do
|
44
|
+
SQLite3::Database.new(DEV_DB)
|
45
|
+
SQLite3::Database.new(TEST_DB)
|
46
|
+
end
|
47
|
+
|
48
|
+
desc "Drop databases"
|
49
|
+
task :drop => :environment do
|
50
|
+
FileUtils.rm DEV_DB
|
51
|
+
FileUtils.rm TEST_DB
|
52
|
+
end
|
53
|
+
|
54
|
+
desc "Run migrations"
|
55
|
+
task :migrate => :environment do
|
56
|
+
ActiveRecord::Migrator.migrations_paths << File.dirname(__FILE__) + 'db/migrate'
|
57
|
+
ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
|
58
|
+
ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration|
|
59
|
+
ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc 'Rolls the schema back to the previous version (specify steps w/ STEP=n).'
|
64
|
+
task :rollback => :environment do
|
65
|
+
step = ENV['STEP'] ? ENV['STEP'].to_i : 1
|
66
|
+
ActiveRecord::Migrator.rollback MIGRATIONS_DIR, step
|
67
|
+
end
|
68
|
+
|
69
|
+
desc "Retrieves the current schema version number"
|
70
|
+
task :version do
|
71
|
+
puts "Current version: #{ActiveRecord::Migrator.current_version}"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
desc "Run specs in test environment"
|
76
|
+
task :spec => :environment do
|
77
|
+
sh 'rspec'
|
78
|
+
end
|
79
|
+
|
80
|
+
desc "Open console with this library required"
|
81
|
+
task :console do
|
82
|
+
sh 'irb -I lib -r tact.rb'
|
83
|
+
end
|
84
|
+
|
85
|
+
desc "Uninstall local version of gem"
|
86
|
+
task :uninstall do
|
87
|
+
sh 'yes | gem uninstall tact'
|
88
|
+
sh 'rm tact-*' unless Dir.glob('./tact-*').empty?
|
89
|
+
end
|
90
|
+
|
91
|
+
desc "Build and install local gem version"
|
92
|
+
task :build => :uninstall do
|
93
|
+
sh 'gem build tact.gemspec && gem install tact'
|
94
|
+
end
|
95
|
+
|
data/bin/tact
CHANGED
@@ -0,0 +1,17 @@
|
|
1
|
+
class CreateTables < ActiveRecord::Migration[4.2]
|
2
|
+
def change
|
3
|
+
create_table :contacts do |t|
|
4
|
+
t.string :first_name, limit: 20
|
5
|
+
t.string :last_name, limit: 20
|
6
|
+
|
7
|
+
t.timestamps
|
8
|
+
end
|
9
|
+
create_table :phone_numbers do |t|
|
10
|
+
t.string :number, limit: 15
|
11
|
+
t.string :type, limit: 10
|
12
|
+
end
|
13
|
+
create_table :emails do |t|
|
14
|
+
t.string :address, limit: 50
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/tact.rb
CHANGED
@@ -1 +1,50 @@
|
|
1
|
-
require
|
1
|
+
# require needed gems
|
2
|
+
require 'fileutils'
|
3
|
+
require 'active_record'
|
4
|
+
require 'sqlite3'
|
5
|
+
require 'colored'
|
6
|
+
|
7
|
+
# require lib files
|
8
|
+
require 'tact/authorizable'
|
9
|
+
require 'tact/card'
|
10
|
+
require 'tact/contact'
|
11
|
+
require 'tact/email'
|
12
|
+
require 'tact/google_client'
|
13
|
+
require 'tact/phone_number'
|
14
|
+
require 'tact/rolodex'
|
15
|
+
require 'tact/tact'
|
16
|
+
require 'tact/version'
|
17
|
+
|
18
|
+
|
19
|
+
APP_ROOT ||= File.join(File.dirname(__FILE__), '../')
|
20
|
+
DEV_DB ||= File.join(File.expand_path('~'), '.tact', 'tact.sqlite3')
|
21
|
+
TEST_DB ||= File.join(File.expand_path('~'), '.tact', 'tact_test.sqlite3')
|
22
|
+
MIGRATIONS_DIR ||= 'db/migrate'
|
23
|
+
CLIENT_SECRET ||= File.join(APP_ROOT, 'client_secret.json')
|
24
|
+
|
25
|
+
# tells AR what db file to use
|
26
|
+
if ENV['GEM_ENV'] == 'test'
|
27
|
+
ActiveRecord::Base.establish_connection(
|
28
|
+
:adapter => 'sqlite3',
|
29
|
+
:database => TEST_DB
|
30
|
+
)
|
31
|
+
else
|
32
|
+
ActiveRecord::Base.establish_connection(
|
33
|
+
:adapter => 'sqlite3',
|
34
|
+
:database => DEV_DB
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Make tact directory
|
39
|
+
if !File.exists?("#{File.expand_path('~')}/.tact")
|
40
|
+
FileUtils.mkdir("#{File.expand_path('~')}/.tact")
|
41
|
+
end
|
42
|
+
|
43
|
+
# Create database
|
44
|
+
SQLite3::Database.new(DEV_DB)
|
45
|
+
SQLite3::Database.new(TEST_DB)
|
46
|
+
|
47
|
+
# Run migrations
|
48
|
+
ActiveRecord::Migrator.migrations_paths << APP_ROOT + 'db/migrate'
|
49
|
+
ActiveRecord::Migration.verbose = false
|
50
|
+
ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths)
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module Authorizable
|
2
|
+
def authorize
|
3
|
+
system("oauth2l fetch --json #{CLIENT_SECRET} https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/contacts.readonly")
|
4
|
+
end
|
5
|
+
|
6
|
+
def authorized?
|
7
|
+
credentials = File.join(File.expand_path('~'), '.oauth2l.token')
|
8
|
+
File.exists?(credentials) && !File.zero?(credentials)
|
9
|
+
end
|
10
|
+
end
|
data/lib/tact/card.rb
CHANGED
@@ -1,21 +1,17 @@
|
|
1
|
-
require_relative 'contact'
|
2
|
-
|
3
1
|
module Tact
|
4
2
|
class Card
|
5
3
|
attr_reader :contact
|
6
4
|
def initialize(contact, index="*")
|
7
5
|
@contact = contact
|
8
6
|
@index = index
|
9
|
-
@phone_numbers = contact.phone_numbers
|
10
|
-
@emails = contact.emails
|
11
7
|
end
|
12
8
|
|
13
9
|
def to_s
|
14
10
|
string = "=" * 40 + "\n"
|
15
11
|
string += "[#{@index}]".red + " #{@contact.to_s}"
|
16
|
-
|
17
|
-
|
12
|
+
contact.phone_numbers.each_with_index {|number, i| string += "\s\s" + "[#{i + 1}] " + number.to_s + "\n"}
|
13
|
+
contact.emails.each_with_index {|address, i| string += "\s\s\s\s" + "[#{i + 1}] " + address.to_s + "\n"}
|
18
14
|
string += "=" * 40 + "\n"
|
19
15
|
end
|
20
16
|
end
|
21
|
-
end
|
17
|
+
end
|
data/lib/tact/contact.rb
CHANGED
@@ -1,74 +1,12 @@
|
|
1
|
-
require 'colored'
|
2
|
-
require_relative 'phone_number'
|
3
|
-
require_relative "email"
|
4
|
-
|
5
1
|
module Tact
|
6
|
-
class Contact
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
@@db = Database.new.db
|
11
|
-
|
12
|
-
def self.from_hash(hash)
|
13
|
-
contact = self.new(hash["first_name"], hash["last_name"], hash["id"])
|
14
|
-
end
|
15
|
-
|
16
|
-
def self.all
|
17
|
-
contact_hashes = @@db.execute("select * from contacts order by last_name asc, first_name asc;")
|
18
|
-
contact_hashes.each_with_index.map {|c_hash, index| self.from_hash(c_hash) }
|
19
|
-
end
|
20
|
-
|
21
|
-
def self.find_by_id(id)
|
22
|
-
contact_hashes = @@db.execute("select * from contacts where id = ?", [id])
|
23
|
-
self.from_hash(contact_hashes[0]) if !contact_hashes.empty?
|
24
|
-
end
|
25
|
-
|
26
|
-
def self.find_by_first_name(first_name)
|
27
|
-
results = @@db.execute("select * from contacts where upper(first_name) = upper(?);", [first_name])
|
28
|
-
results.map {|c_hash| self.from_hash(c_hash) }
|
29
|
-
end
|
30
|
-
|
31
|
-
def self.find_by_last_name(last_name)
|
32
|
-
results = @@db.execute("select * from contacts where upper(last_name) = upper(?);", [last_name])
|
33
|
-
results.map {|c_hash| self.from_hash(c_hash) }
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.delete(id)
|
37
|
-
@@db.execute("delete from contacts where id = ?;", [id])
|
38
|
-
@@db.execute("select changes();")[0]["changes()"] == 1 ? true : false
|
39
|
-
end
|
40
|
-
|
41
|
-
def initialize(first_name, last_name, primary_key=nil)
|
42
|
-
@id = primary_key
|
43
|
-
@first_name = first_name.downcase.capitalize
|
44
|
-
@last_name = last_name.downcase.capitalize
|
45
|
-
end
|
46
|
-
|
47
|
-
def save
|
48
|
-
begin
|
49
|
-
if @id == nil
|
50
|
-
if @@db.execute("insert into contacts (first_name, last_name) values (?, ?);", [@first_name, @last_name])
|
51
|
-
@id = @@db.execute("select last_insert_rowid()")[0]["last_insert_rowid()"]
|
52
|
-
self
|
53
|
-
else
|
54
|
-
false
|
55
|
-
end
|
56
|
-
else
|
57
|
-
@@db.execute("update contacts set first_name = ?, last_name = ? where id = ?;", [@first_name, @last_name, @id]) ? self : false
|
58
|
-
end
|
59
|
-
rescue
|
60
|
-
puts "Error: Contact already exists".red
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def phone_numbers
|
65
|
-
number_hashes = @@db.execute("select * from phone_numbers where contact_id = ? order by type asc;", [@id])
|
66
|
-
number_hashes.map {|n_hash| PhoneNumber.from_hash(n_hash) }
|
67
|
-
end
|
2
|
+
class Contact < ActiveRecord::Base
|
3
|
+
has_many :phone_numbers, dependent: :destroy
|
4
|
+
has_many :emails, dependent: :destroy
|
5
|
+
validates :last_name, uniqueness: { scope: :first_name }
|
68
6
|
|
69
|
-
|
70
|
-
|
71
|
-
|
7
|
+
before_validation do
|
8
|
+
first_name.upcase!
|
9
|
+
last_name.upcase!
|
72
10
|
end
|
73
11
|
|
74
12
|
def full_name
|
@@ -79,4 +17,4 @@ module Tact
|
|
79
17
|
"#{full_name}\n".green.bold
|
80
18
|
end
|
81
19
|
end
|
82
|
-
end
|
20
|
+
end
|
data/lib/tact/email.rb
CHANGED
@@ -1,57 +1,10 @@
|
|
1
|
-
require_relative 'database'
|
2
|
-
|
3
1
|
module Tact
|
4
|
-
class Email
|
5
|
-
|
6
|
-
attr_accessor :address, :index
|
7
|
-
|
8
|
-
@@db = Database.new.db
|
9
|
-
|
10
|
-
def self.from_hash(hash)
|
11
|
-
self.new(hash["address"], hash["contact_id"], hash["id"])
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.all
|
15
|
-
email_hashes = @@db.execute("select * from emails;")
|
16
|
-
email_hashes.map {|e_hash| Email.from_hash(e_hash) }
|
17
|
-
end
|
18
|
-
|
19
|
-
def self.find_by_id(id)
|
20
|
-
email_hashes = @@db.execute("select * from emails where id = ?", [id])
|
21
|
-
self.from_hash(email_hashes[0]) if !email_hashes.empty?
|
22
|
-
end
|
23
|
-
|
24
|
-
def self.find_by_address(address)
|
25
|
-
email_hashes = @@db.execute("select * from emails where address = ?", [address])
|
26
|
-
email_hashes.map {|e_hash| self.from_hash(e_hash) }
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.delete(id)
|
30
|
-
@@db.execute("delete from emails where id = ?;", [id])
|
31
|
-
@@db.execute("select changes();")[0]["changes()"] == 1 ? true : false
|
32
|
-
end
|
33
|
-
|
34
|
-
def initialize(address, contact_id, primary_key=nil)
|
35
|
-
@id = primary_key
|
36
|
-
@address = address
|
37
|
-
@contact_id = contact_id
|
38
|
-
end
|
39
|
-
|
40
|
-
def save
|
41
|
-
if @id == nil
|
42
|
-
if @@db.execute("insert into emails (address, contact_id) values (?, ?);", [@address, @contact_id])
|
43
|
-
@id = @@db.execute("select last_insert_rowid();")[0]["last_insert_rowid()"]
|
44
|
-
self
|
45
|
-
else
|
46
|
-
false
|
47
|
-
end
|
48
|
-
else
|
49
|
-
@@db.execute("update emails set address = ?, contact_id = ? where id = ?;", [@address, @contact_id, @id]) ? self : false
|
50
|
-
end
|
51
|
-
end
|
2
|
+
class Email < ActiveRecord::Base
|
3
|
+
belongs_to :contact
|
52
4
|
|
53
5
|
def to_s
|
54
|
-
"<#{
|
6
|
+
"<#{address}>"
|
55
7
|
end
|
8
|
+
|
56
9
|
end
|
57
|
-
end
|
10
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module Tact
|
4
|
+
module GoogleContacts
|
5
|
+
|
6
|
+
class Entry
|
7
|
+
attr_reader :info
|
8
|
+
|
9
|
+
def self.all
|
10
|
+
collection
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(info)
|
14
|
+
@info = info
|
15
|
+
end
|
16
|
+
|
17
|
+
def first_name
|
18
|
+
names[:givenName]
|
19
|
+
end
|
20
|
+
|
21
|
+
def last_name
|
22
|
+
names[:familyName]
|
23
|
+
end
|
24
|
+
|
25
|
+
def phone_numbers
|
26
|
+
if info[:phoneNumbers]
|
27
|
+
info[:phoneNumbers].map do |phone_number|
|
28
|
+
PhoneNumber.new(
|
29
|
+
number: phone_number[:value],
|
30
|
+
kind: phone_number[:type]
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def emails
|
37
|
+
if info[:emailAddresses]
|
38
|
+
info[:emailAddresses].map do |email_address|
|
39
|
+
Email.new(address: email_address[:value])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def names
|
47
|
+
info[:names][0]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.collection
|
51
|
+
@@collection ||= Fetcher.info_list.reduce(EntriesCollection.new) do |collection, info|
|
52
|
+
collection << new(info)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private_class_method :collection
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
class EntriesCollection
|
61
|
+
include Enumerable
|
62
|
+
|
63
|
+
def initialize(entries=[])
|
64
|
+
@entries = entries
|
65
|
+
end
|
66
|
+
|
67
|
+
def <<(entry)
|
68
|
+
@entries << entry
|
69
|
+
end
|
70
|
+
|
71
|
+
def each
|
72
|
+
@entries.each { |c| yield(c) }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
class Syncer
|
78
|
+
|
79
|
+
def initialize(entry)
|
80
|
+
@entry = entry
|
81
|
+
@new_numbers = []
|
82
|
+
@new_emails = []
|
83
|
+
end
|
84
|
+
|
85
|
+
def sync
|
86
|
+
contact = find_contact || Contact.new(
|
87
|
+
first_name: entry.first_name.upcase,
|
88
|
+
last_name: (entry.last_name || "").upcase
|
89
|
+
)
|
90
|
+
merge_properties(contact)
|
91
|
+
contact.save
|
92
|
+
end
|
93
|
+
|
94
|
+
def find_contact
|
95
|
+
Contact.find_by(
|
96
|
+
first_name: entry.first_name.upcase,
|
97
|
+
last_name: (entry.last_name || "").upcase
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
def merge_properties(contact)
|
102
|
+
get_new_phone_numbers(contact)
|
103
|
+
get_new_emails(contact)
|
104
|
+
add_new_phone_numbers(contact)
|
105
|
+
add_new_emails(contact)
|
106
|
+
end
|
107
|
+
|
108
|
+
def add_new_phone_numbers(contact)
|
109
|
+
contact.phone_numbers << new_numbers
|
110
|
+
end
|
111
|
+
|
112
|
+
def add_new_emails(contact)
|
113
|
+
contact.emails << new_emails
|
114
|
+
end
|
115
|
+
|
116
|
+
def get_new_phone_numbers(contact)
|
117
|
+
entry.phone_numbers.each do |number|
|
118
|
+
if !contact.phone_numbers.any? { |n| n.number == number.number }
|
119
|
+
new_numbers << number
|
120
|
+
end
|
121
|
+
end if entry.phone_numbers
|
122
|
+
end
|
123
|
+
|
124
|
+
def get_new_emails(contact)
|
125
|
+
entry.emails.each do |email|
|
126
|
+
if !contact.emails.any? { |e| e.address == email.address }
|
127
|
+
new_emails << email
|
128
|
+
end
|
129
|
+
end if entry.emails
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
attr_reader :entry, :new_numbers, :new_emails
|
134
|
+
|
135
|
+
end
|
136
|
+
|
137
|
+
|
138
|
+
class Fetcher
|
139
|
+
|
140
|
+
def self.info_list
|
141
|
+
info = JSON.parse(json, symbolize_names: true)
|
142
|
+
if info[:error]
|
143
|
+
puts "ERROR: Please authorize your Google account.".red
|
144
|
+
exit
|
145
|
+
end
|
146
|
+
info[:connections]
|
147
|
+
end
|
148
|
+
|
149
|
+
def self.json
|
150
|
+
`curl -H "$(oauth2l header --json #{CLIENT_SECRET} https://www.googleapis.com/auth/contacts https://www.googleapis.com/auth/contacts.readonly)"\
|
151
|
+
https://people.googleapis.com/v1/people/me/connections?requestMask.includeField=person.names,person.phone_numbers,person.email_addresses`
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
data/lib/tact/phone_number.rb
CHANGED
@@ -1,78 +1,21 @@
|
|
1
|
-
require 'colored'
|
2
|
-
require_relative 'database'
|
3
|
-
|
4
1
|
module Tact
|
5
|
-
class PhoneNumber
|
6
|
-
|
7
|
-
attr_accessor :number, :index, :type
|
8
|
-
|
9
|
-
@@db = Database.new.db
|
10
|
-
|
11
|
-
def self.format_number(number)
|
12
|
-
n = number.gsub(/[^\d]/, "")
|
13
|
-
chars = n.chars
|
14
|
-
if chars.count == 10
|
15
|
-
chars.insert(3, '-')
|
16
|
-
chars.insert(7, '-')
|
17
|
-
elsif chars.count == 11
|
18
|
-
chars.insert(0, '+')
|
19
|
-
chars.insert(2, ' ')
|
20
|
-
chars.insert(6, '-')
|
21
|
-
chars.insert(10, '-')
|
22
|
-
elsif chars.count == 7
|
23
|
-
chars.insert(3, '-')
|
24
|
-
else
|
25
|
-
return n
|
26
|
-
end
|
27
|
-
return chars.join
|
28
|
-
end
|
29
|
-
|
30
|
-
def self.from_hash(hash)
|
31
|
-
self.new(hash["type"], hash["number"], hash["contact_id"], hash["id"])
|
32
|
-
end
|
33
|
-
|
34
|
-
def self.all
|
35
|
-
number_hashes = @@db.execute("select * from phone_numbers;")
|
36
|
-
number_hashes.map {|n_hash| self.from_hash(n_hash) }
|
37
|
-
end
|
38
|
-
|
39
|
-
def self.delete(id)
|
40
|
-
@@db.execute("delete from phone_numbers where id = ?;", [id])
|
41
|
-
@@db.execute("select changes();")[0]["changes()"] == 1 ? true : false
|
42
|
-
end
|
2
|
+
class PhoneNumber < ActiveRecord::Base
|
3
|
+
belongs_to :contact
|
43
4
|
|
44
|
-
|
45
|
-
|
46
|
-
|
5
|
+
after_initialize :format_number
|
6
|
+
before_save :format_number
|
7
|
+
before_save do
|
8
|
+
self.kind = kind ? kind.downcase.capitalize : "Cell"
|
47
9
|
end
|
48
10
|
|
49
|
-
def
|
50
|
-
|
51
|
-
number_hashes.map {|n_hash| self.from_hash(n_hash) }
|
52
|
-
end
|
53
|
-
|
54
|
-
def initialize(type, number, contact_id, primary_key=nil)
|
55
|
-
@id = primary_key
|
56
|
-
@type = type.downcase.capitalize
|
57
|
-
@number = number.gsub(/\D/, "")
|
58
|
-
@contact_id = contact_id
|
11
|
+
def to_s
|
12
|
+
"#{kind.yellow}: #{number.bold}"
|
59
13
|
end
|
60
14
|
|
61
|
-
def
|
62
|
-
|
63
|
-
|
64
|
-
@id = @@db.execute("select last_insert_rowid();")[0]["last_insert_rowid()"]
|
65
|
-
self
|
66
|
-
else
|
67
|
-
false
|
68
|
-
end
|
69
|
-
else
|
70
|
-
@@db.execute("update phone_numbers set type = ?, number = ?, contact_id = ? where id = ?;", [@type, @number, @contact_id]) ? self : false
|
71
|
-
end
|
15
|
+
def format_number
|
16
|
+
n = number.gsub(/[^\d]/, "")
|
17
|
+
self.number = n.gsub(/\A(?:\d{1,3})?(\d{3})(\d{3})(\d{4})\z/, '(\1) \2-\3')
|
72
18
|
end
|
73
19
|
|
74
|
-
def to_s
|
75
|
-
"#{type.yellow}: #{PhoneNumber.format_number(number).bold}"
|
76
|
-
end
|
77
20
|
end
|
78
|
-
end
|
21
|
+
end
|
data/lib/tact/rolodex.rb
CHANGED
@@ -1,72 +1,76 @@
|
|
1
|
-
require 'sqlite3'
|
2
|
-
require 'colored'
|
3
|
-
require_relative 'card'
|
4
|
-
require_relative 'database'
|
5
|
-
|
6
1
|
module Tact
|
7
2
|
class Rolodex
|
8
3
|
|
9
4
|
def initialize
|
10
5
|
@cards = load_cards
|
11
|
-
@db = Database.new.db
|
12
6
|
end
|
13
7
|
|
14
8
|
def load_cards
|
15
|
-
cards = Contact.all.each_with_index.map do |contact, index|
|
9
|
+
cards = Contact.all.order(last_name: :asc, first_name: :asc).each_with_index.map do |contact, index|
|
16
10
|
Card.new(contact, index + 1)
|
17
11
|
end
|
18
12
|
cards
|
19
13
|
end
|
20
14
|
|
21
15
|
def add_contact(first_name, last_name)
|
22
|
-
|
16
|
+
begin
|
17
|
+
Contact.create!(
|
18
|
+
first_name: first_name,
|
19
|
+
last_name: last_name
|
20
|
+
)
|
21
|
+
rescue
|
22
|
+
puts 'Error: Contact already exists'.red
|
23
|
+
end
|
23
24
|
end
|
24
25
|
|
25
|
-
def add_phone_number(contact_index,
|
26
|
+
def add_phone_number(contact_index, kind, number)
|
26
27
|
contact = find_contact(contact_index)
|
27
|
-
PhoneNumber.
|
28
|
+
PhoneNumber.create(
|
29
|
+
kind: kind,
|
30
|
+
number: number,
|
31
|
+
contact: contact
|
32
|
+
)
|
28
33
|
end
|
29
34
|
|
30
35
|
def add_email(contact_index, address)
|
31
36
|
contact = find_contact(contact_index)
|
32
|
-
Email.
|
37
|
+
Email.create(
|
38
|
+
address: address,
|
39
|
+
contact: contact
|
40
|
+
)
|
33
41
|
end
|
34
42
|
|
35
43
|
def delete_contact(contact_index)
|
36
|
-
|
37
|
-
Contact.delete(contact.id)
|
44
|
+
find_contact(contact_index).destroy
|
38
45
|
end
|
39
46
|
|
40
47
|
def delete_phone_number(contact_index, num_index)
|
41
|
-
|
42
|
-
PhoneNumber.delete(phone_number.id)
|
48
|
+
find_phone_number(contact_index, num_index).destroy
|
43
49
|
end
|
44
50
|
|
45
51
|
def delete_email(contact_index, email_index)
|
46
|
-
|
47
|
-
Email.delete(email.id)
|
52
|
+
find_email(contact_index, email_index).destroy
|
48
53
|
end
|
49
54
|
|
50
55
|
def edit_contact_name(contact_index, new_first_name, new_last_name)
|
51
56
|
contact = find_contact(contact_index)
|
52
|
-
contact.
|
53
|
-
|
54
|
-
|
57
|
+
contact.update_attributes(
|
58
|
+
first_name: new_first_name,
|
59
|
+
last_name: new_last_name
|
60
|
+
)
|
55
61
|
end
|
56
62
|
|
57
63
|
def edit_phone_number(contact_index, num_index, new_type, new_number)
|
58
|
-
new_type = new_type.downcase.capitalize
|
59
|
-
new_number = new_number.gsub(/\D/, "")
|
60
64
|
phone_number = find_phone_number(contact_index, num_index)
|
61
|
-
phone_number.
|
62
|
-
|
63
|
-
|
65
|
+
phone_number.update_attributes(
|
66
|
+
type: new_type,
|
67
|
+
number: new_number
|
68
|
+
)
|
64
69
|
end
|
65
70
|
|
66
71
|
def edit_email(contact_index, email_index, new_address)
|
67
72
|
email = find_email(contact_index, email_index)
|
68
|
-
email.address
|
69
|
-
email.save
|
73
|
+
email.update_attributes(address: new_address)
|
70
74
|
end
|
71
75
|
|
72
76
|
def find_contact(contact_index)
|
@@ -98,21 +102,26 @@ module Tact
|
|
98
102
|
end
|
99
103
|
end
|
100
104
|
|
105
|
+
# TODO: Add specs for find methods
|
101
106
|
def find_by_name(param)
|
102
|
-
param = param.split(" ").map {|name| name.
|
107
|
+
param = param.split(" ").map {|name| name.upcase }.join(" ")
|
103
108
|
search_results = []
|
104
109
|
@cards.each {|card| search_results.push(card) if card.contact.full_name.include?(param)}
|
105
110
|
search_results
|
106
111
|
end
|
107
112
|
|
108
113
|
def find_by_number(param)
|
109
|
-
|
110
|
-
|
114
|
+
phone_numbers = PhoneNumber.includes(:contact).where('number LIKE ?', "%#{param}%")
|
115
|
+
phone_numbers.map do |phone_number|
|
116
|
+
convert_to_card(phone_number.contact.id)
|
117
|
+
end
|
111
118
|
end
|
112
119
|
|
113
120
|
def find_by_email(param)
|
114
|
-
|
115
|
-
|
121
|
+
emails = Email.includes(:contact).where('address LIKE ?', "%#{param}%")
|
122
|
+
emails.map do |email|
|
123
|
+
convert_to_card(email.contact.id)
|
124
|
+
end
|
116
125
|
end
|
117
126
|
|
118
127
|
def convert_to_card(contact_id)
|
@@ -120,6 +129,10 @@ module Tact
|
|
120
129
|
results[0]
|
121
130
|
end
|
122
131
|
|
132
|
+
def length
|
133
|
+
@cards.count
|
134
|
+
end
|
135
|
+
|
123
136
|
def to_s
|
124
137
|
string = ""
|
125
138
|
@cards.each {|card| string += card.to_s}
|
data/lib/tact/tact.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
require_relative "rolodex.rb"
|
2
1
|
require 'optparse'
|
3
|
-
|
4
|
-
require 'colored'
|
2
|
+
require_relative 'authorizable.rb'
|
5
3
|
|
6
4
|
module Tact
|
7
5
|
class Tact
|
6
|
+
include Authorizable
|
7
|
+
|
8
8
|
def initialize(args)
|
9
9
|
@dex = Rolodex.new
|
10
10
|
@options = {}
|
@@ -20,12 +20,15 @@ module Tact
|
|
20
20
|
opt.on('-d', '--delete', 'Delete entry') do
|
21
21
|
@options[:delete] = true
|
22
22
|
end
|
23
|
-
opt.on('-p', '--phone', '
|
23
|
+
opt.on('-p', '--phone', 'Phone number') do
|
24
24
|
@options[:number] = true
|
25
25
|
end
|
26
|
-
opt.on('-e', '--email', '
|
26
|
+
opt.on('-e', '--email', 'Email') do
|
27
27
|
@options[:email] = true
|
28
28
|
end
|
29
|
+
opt.on('-s', '--sync', 'Sync Google contacts') do
|
30
|
+
@options[:sync] = true
|
31
|
+
end
|
29
32
|
opt.on("-v", "--version", "Version") do
|
30
33
|
@options[:version] = true
|
31
34
|
end
|
@@ -41,23 +44,24 @@ module Tact
|
|
41
44
|
end
|
42
45
|
|
43
46
|
def help_message
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
47
|
+
<<-EOF
|
48
|
+
|
49
|
+
-v Current version
|
50
|
+
-h Help
|
51
|
+
-s Sync with Google Contacts
|
48
52
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
53
|
+
<param> Search by name
|
54
|
+
-p <param> Search by number
|
55
|
+
-e <param> Search by email
|
56
|
+
-n <first> <last> Adds new name
|
57
|
+
-np <index> <type> <num> Adds contact number
|
58
|
+
-ne <index> <address> Adds contact email
|
59
|
+
-d <index> Deletes contact
|
60
|
+
-dp <index> <num_index> Deletes contact number
|
61
|
+
-de <index> <e_index> Deletes contact email
|
62
|
+
-u <index> <first> <last> Edits contact name
|
63
|
+
-up <index> <num_index> <type> <num> Edits contact number
|
64
|
+
-ue <index> <e_index> <address> Edits contact email
|
61
65
|
|
62
66
|
EOF
|
63
67
|
end
|
@@ -117,7 +121,15 @@ module Tact
|
|
117
121
|
new_address = @args[2]
|
118
122
|
@dex.edit_email(contact_index, email_index, new_address)
|
119
123
|
end
|
120
|
-
|
124
|
+
|
125
|
+
elsif @options[:sync]
|
126
|
+
authorize unless authorized?
|
127
|
+
puts "Syncing Google contacts..."
|
128
|
+
GoogleContacts::Entry.all.each do |entry|
|
129
|
+
syncer = GoogleContacts::Syncer.new(entry)
|
130
|
+
syncer.sync
|
131
|
+
end
|
132
|
+
|
121
133
|
elsif @options[:help]
|
122
134
|
puts help_message
|
123
135
|
elsif @options[:version]
|
@@ -202,4 +214,4 @@ module Tact
|
|
202
214
|
end
|
203
215
|
end
|
204
216
|
end
|
205
|
-
end
|
217
|
+
end
|
data/lib/tact/version.rb
CHANGED
data/tact.gemspec
CHANGED
@@ -16,12 +16,21 @@ Gem::Specification.new do |spec|
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
17
17
|
f.match(%r{^(test|spec|features)/})
|
18
18
|
end
|
19
|
+
spec.files << 'client_secret.json'
|
20
|
+
|
19
21
|
spec.executables = ["tact"]
|
20
22
|
spec.require_paths = ["lib"]
|
21
23
|
|
22
24
|
spec.add_runtime_dependency "sqlite3", "~> 1.3"
|
23
25
|
spec.add_runtime_dependency "colored", "~> 1.2"
|
26
|
+
spec.add_runtime_dependency "activerecord", "~> 5.0"
|
24
27
|
|
25
28
|
spec.add_development_dependency "bundler", "~> 1.13"
|
26
29
|
spec.add_development_dependency "rake", "~> 10.0"
|
30
|
+
spec.add_development_dependency "rspec", "~> 3.6"
|
31
|
+
spec.add_development_dependency "database_cleaner", "~> 1.4"
|
32
|
+
|
33
|
+
spec.requirements << "oauth2l"
|
34
|
+
|
35
|
+
spec.post_install_message = "Thanks for installing tact! Run `tact -h` for help. Note: If you would like to sync your contacts with Google, please install oauth2l: 'https://github.com/google/oauth2l'"
|
27
36
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tact
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.0.17
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- ollieshmollie
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-06-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sqlite3
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '1.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: activerecord
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '5.0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '5.0'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: bundler
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +80,34 @@ dependencies:
|
|
66
80
|
- - "~>"
|
67
81
|
- !ruby/object:Gem::Version
|
68
82
|
version: '10.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.6'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.6'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: database_cleaner
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '1.4'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '1.4'
|
69
111
|
description:
|
70
112
|
email:
|
71
113
|
- oliverduncan@icloud.com
|
@@ -82,11 +124,16 @@ files:
|
|
82
124
|
- bin/console
|
83
125
|
- bin/setup
|
84
126
|
- bin/tact
|
127
|
+
- client_secret.json
|
128
|
+
- db/migrate/20170515014719_create_tables.rb
|
129
|
+
- db/migrate/20170515015242_add_foreign_keys.rb
|
130
|
+
- db/migrate/20170515130759_change_type_on_phone_numbers.rb
|
85
131
|
- lib/tact.rb
|
132
|
+
- lib/tact/authorizable.rb
|
86
133
|
- lib/tact/card.rb
|
87
134
|
- lib/tact/contact.rb
|
88
|
-
- lib/tact/database.rb
|
89
135
|
- lib/tact/email.rb
|
136
|
+
- lib/tact/google_client.rb
|
90
137
|
- lib/tact/phone_number.rb
|
91
138
|
- lib/tact/rolodex.rb
|
92
139
|
- lib/tact/tact.rb
|
@@ -96,7 +143,8 @@ homepage: https://www.github.com/ollieshmollie/tact
|
|
96
143
|
licenses:
|
97
144
|
- MIT
|
98
145
|
metadata: {}
|
99
|
-
post_install_message:
|
146
|
+
post_install_message: 'Thanks for installing tact! Run `tact -h` for help. Note: If
|
147
|
+
you would like to sync your contacts with Google, please install oauth2l: ''https://github.com/google/oauth2l'''
|
100
148
|
rdoc_options: []
|
101
149
|
require_paths:
|
102
150
|
- lib
|
@@ -110,9 +158,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
158
|
- - ">="
|
111
159
|
- !ruby/object:Gem::Version
|
112
160
|
version: '0'
|
113
|
-
requirements:
|
161
|
+
requirements:
|
162
|
+
- oauth2l
|
114
163
|
rubyforge_project:
|
115
|
-
rubygems_version: 2.
|
164
|
+
rubygems_version: 2.6.11
|
116
165
|
signing_key:
|
117
166
|
specification_version: 4
|
118
167
|
summary: A command line rolodex.
|
data/lib/tact/database.rb
DELETED
@@ -1,47 +0,0 @@
|
|
1
|
-
require 'sqlite3'
|
2
|
-
|
3
|
-
module Tact
|
4
|
-
class Database
|
5
|
-
attr_reader :db
|
6
|
-
|
7
|
-
def initialize
|
8
|
-
home_dir = File.expand_path("~")
|
9
|
-
Dir.mkdir("#{home_dir}/.tact") unless File.exists?("#{home_dir}/.tact")
|
10
|
-
@db = SQLite3::Database.new("#{File.expand_path("~")}/.tact/tact.sqlite3")
|
11
|
-
@db.results_as_hash = true
|
12
|
-
@db.execute("pragma foreign_keys = on")
|
13
|
-
create_tables
|
14
|
-
end
|
15
|
-
|
16
|
-
def create_tables
|
17
|
-
contact_table = <<~eof
|
18
|
-
CREATE TABLE IF NOT EXISTS contacts(
|
19
|
-
id integer primary key,
|
20
|
-
first_name varchar(255) not null,
|
21
|
-
last_name varchar(255) not null,
|
22
|
-
constraint name_unique unique (first_name, last_name)
|
23
|
-
);
|
24
|
-
eof
|
25
|
-
phone_numbers_table = <<~eof
|
26
|
-
CREATE TABLE IF NOT EXISTS phone_numbers(
|
27
|
-
id integer primary key,
|
28
|
-
type varchar(255),
|
29
|
-
number varchar(255),
|
30
|
-
contact_id int,
|
31
|
-
foreign key (contact_id) references contacts(id) on delete cascade
|
32
|
-
);
|
33
|
-
eof
|
34
|
-
emails_table = <<~eof
|
35
|
-
CREATE TABLE IF NOT EXISTS emails(
|
36
|
-
id integer primary key,
|
37
|
-
address varchar(255),
|
38
|
-
contact_id,
|
39
|
-
foreign key (contact_id) references contacts(id) on delete cascade
|
40
|
-
);
|
41
|
-
eof
|
42
|
-
@db.execute(contact_table)
|
43
|
-
@db.execute(phone_numbers_table)
|
44
|
-
@db.execute(emails_table)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|