abbu 0.4.0 → 0.8.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.
- checksums.yaml +4 -4
- data/README.md +166 -5
- data/bin/abbu +54 -8
- data/docs/ABBU.md +146 -3
- data/docs/API_STABILITY.md +189 -0
- data/docs/CHANGELOG.md +53 -0
- data/docs/CONTRIBUTING.md +12 -0
- data/docs/FORMAT_COMPATIBILITY.md +97 -0
- data/docs/RELEASING.md +75 -0
- data/docs/TODO.md +3 -0
- data/lib/abbu/archive.rb +14 -0
- data/lib/abbu/contact.rb +3 -2
- data/lib/abbu/exporters/vcard_document.rb +54 -0
- data/lib/abbu/exporters/vcard_encoding.rb +54 -0
- data/lib/abbu/exporters/vcard_exporter.rb +30 -31
- data/lib/abbu/group.rb +28 -0
- data/lib/abbu/group_catalog.rb +32 -0
- data/lib/abbu/live_store.rb +13 -0
- data/lib/abbu/parsers/sqlite_parser.rb +6 -3
- data/lib/abbu/query.rb +19 -0
- data/lib/abbu/source.rb +53 -0
- data/lib/abbu/source_catalog.rb +32 -0
- data/lib/abbu/timestamp_range.rb +40 -0
- data/lib/abbu/version.rb +1 -1
- data/lib/abbu.rb +1 -0
- data/sig/group.rbs +36 -0
- data/sig/query.rbs +21 -0
- data/sig/source.rbs +25 -0
- data/sig/vcard_exporter.rbs +10 -0
- metadata +16 -2
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
require 'uri'
|
|
5
5
|
|
|
6
|
+
require_relative 'vcard_document'
|
|
7
|
+
|
|
6
8
|
module Abbu
|
|
7
9
|
module Exporters
|
|
8
10
|
class VcardExporter
|
|
@@ -11,21 +13,21 @@ module Abbu
|
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def to_file(path)
|
|
14
|
-
File.
|
|
16
|
+
File.binwrite(path, generate)
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def to_stdout
|
|
18
|
-
|
|
20
|
+
print generate
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
private
|
|
22
24
|
|
|
23
25
|
def generate
|
|
24
|
-
@contacts.map { |c| vcard_for(c) }.join
|
|
26
|
+
@contacts.map { |c| vcard_for(c) }.join
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def vcard_for(contact) # rubocop:disable Metrics/MethodLength
|
|
28
|
-
lines =
|
|
30
|
+
lines = VcardDocument.new
|
|
29
31
|
|
|
30
32
|
append_name_fields(lines, contact)
|
|
31
33
|
append_emails(lines, contact)
|
|
@@ -39,13 +41,12 @@ module Abbu
|
|
|
39
41
|
append_notes(lines, contact)
|
|
40
42
|
append_photo(lines, contact)
|
|
41
43
|
|
|
42
|
-
lines
|
|
43
|
-
lines.join("\n")
|
|
44
|
+
lines.to_s
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def append_name_fields(lines, contact)
|
|
47
|
-
lines << "FN:#{contact.full_name}"
|
|
48
|
-
lines << "N:#{name_components(contact)
|
|
48
|
+
lines << "FN:#{VcardEncoding.text(contact.full_name)}"
|
|
49
|
+
lines << "N:#{VcardEncoding.structured(name_components(contact))}"
|
|
49
50
|
append_nickname(lines, contact)
|
|
50
51
|
append_company(lines, contact)
|
|
51
52
|
append_title(lines, contact)
|
|
@@ -57,25 +58,26 @@ module Abbu
|
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
def append_nickname(lines, contact)
|
|
60
|
-
lines << "NICKNAME:#{contact.nickname}" if contact.nickname
|
|
61
|
+
lines << "NICKNAME:#{VcardEncoding.text(contact.nickname)}" if contact.nickname
|
|
61
62
|
end
|
|
62
63
|
|
|
63
64
|
def append_company(lines, contact)
|
|
64
|
-
lines << "ORG:#{contact.company}" if contact.company
|
|
65
|
+
lines << "ORG:#{VcardEncoding.text(contact.company)}" if contact.company
|
|
65
66
|
end
|
|
66
67
|
|
|
67
68
|
def append_title(lines, contact)
|
|
68
|
-
lines << "TITLE:#{contact.job_title}" if contact.job_title
|
|
69
|
+
lines << "TITLE:#{VcardEncoding.text(contact.job_title)}" if contact.job_title
|
|
69
70
|
end
|
|
70
71
|
|
|
71
72
|
def append_phonetic_names(lines, contact)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
%i[first middle last].each do |part|
|
|
74
|
+
value = contact.public_send(:"phonetic_#{part}_name")
|
|
75
|
+
lines << "X-PHONETIC-#{part.upcase}-NAME:#{VcardEncoding.text(value)}" if value
|
|
76
|
+
end
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
def append_verification_code(lines, contact)
|
|
78
|
-
lines << "X-VERIFICATION-CODE:#{contact.verification_code}" if contact.verification_code
|
|
80
|
+
lines << "X-VERIFICATION-CODE:#{VcardEncoding.text(contact.verification_code)}" if contact.verification_code
|
|
79
81
|
end
|
|
80
82
|
|
|
81
83
|
def append_photo(lines, contact)
|
|
@@ -90,46 +92,47 @@ module Abbu
|
|
|
90
92
|
end
|
|
91
93
|
|
|
92
94
|
def append_notes(lines, contact)
|
|
93
|
-
contact.notes.each { |n| lines << "NOTE:#{n}" }
|
|
95
|
+
contact.notes.each { |n| lines << "NOTE:#{VcardEncoding.text(n)}" }
|
|
94
96
|
end
|
|
95
97
|
|
|
96
98
|
def append_emails(lines, contact)
|
|
97
99
|
contact.emails.each do |e|
|
|
98
|
-
|
|
99
|
-
lines << "EMAIL;TYPE=#{label}:#{e[:address]}"
|
|
100
|
+
lines.labeled('EMAIL', VcardEncoding.text(e[:address]), e, default_type: 'INTERNET')
|
|
100
101
|
end
|
|
101
102
|
end
|
|
102
103
|
|
|
103
104
|
def append_phones(lines, contact)
|
|
104
105
|
contact.phones.each do |p|
|
|
105
|
-
|
|
106
|
-
lines << "TEL;TYPE=#{label}:#{p[:number]}"
|
|
106
|
+
lines.labeled('TEL', VcardEncoding.text(p[:number]), p, default_type: 'VOICE')
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
109
|
|
|
110
110
|
def append_addresses(lines, contact)
|
|
111
111
|
contact.addresses.each do |a|
|
|
112
|
-
|
|
113
|
-
lines
|
|
112
|
+
value = VcardEncoding.structured([nil, nil, a[:street], a[:city], a[:state], a[:zip], a[:country]])
|
|
113
|
+
lines.labeled('ADR', value, a)
|
|
114
114
|
end
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def append_urls(lines, contact)
|
|
118
118
|
contact.urls.each do |u|
|
|
119
|
-
lines
|
|
119
|
+
lines.labeled('URL', VcardEncoding.uri(u[:url]), u)
|
|
120
120
|
end
|
|
121
121
|
end
|
|
122
122
|
|
|
123
123
|
def append_social_profiles(lines, contact)
|
|
124
124
|
contact.social_profiles.each do |sp|
|
|
125
|
-
|
|
125
|
+
type = sp[:service] ? ";TYPE=#{VcardEncoding.token(sp[:service])}" : ''
|
|
126
|
+
lines << "X-SOCIALPROFILE#{type}:#{VcardEncoding.text(sp[:username])}"
|
|
126
127
|
end
|
|
127
128
|
end
|
|
128
129
|
|
|
129
130
|
def append_instant_messages(lines, contact)
|
|
130
131
|
contact.instant_messages.each do |im|
|
|
131
132
|
service = im[:service]&.downcase || 'unknown'
|
|
132
|
-
|
|
133
|
+
raise ArgumentError, 'IM service must be a URI scheme' unless service.match?(/\A[a-z][a-z0-9+.-]*\z/)
|
|
134
|
+
|
|
135
|
+
lines.labeled('IMPP', "#{service}:#{VcardEncoding.uri(im[:address])}", im)
|
|
133
136
|
end
|
|
134
137
|
end
|
|
135
138
|
|
|
@@ -138,12 +141,8 @@ module Abbu
|
|
|
138
141
|
lines << "X-LUNAR-BDAY:#{format_vcard_date(contact.lunar_birthday)}" if contact.lunar_birthday
|
|
139
142
|
return unless contact.anniversary
|
|
140
143
|
|
|
141
|
-
|
|
142
|
-
lines
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
def anniversary_label(anniversary)
|
|
146
|
-
anniversary[:raw_label] || anniversary[:label] || 'Anniversary'
|
|
144
|
+
entry = { label: 'Anniversary' }.merge(contact.anniversary.compact)
|
|
145
|
+
lines.labeled('X-ABDATE', format_vcard_date(contact.anniversary), entry)
|
|
147
146
|
end
|
|
148
147
|
|
|
149
148
|
def format_vcard_date(date)
|
data/lib/abbu/group.rb
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# lib/abbu/group.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'query'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
# A snapshot of an observed SQLite group membership, local to one input file.
|
|
8
|
+
# Names and record keys are evidence, not globally unique Apple identifiers.
|
|
9
|
+
class Group
|
|
10
|
+
attr_reader :record_id, :name, :source, :contacts
|
|
11
|
+
|
|
12
|
+
def initialize(record_id:, name:, source:, contacts:)
|
|
13
|
+
@record_id = record_id
|
|
14
|
+
@name = name&.dup&.freeze
|
|
15
|
+
@source = source.transform_values { |value| value&.dup&.freeze }.freeze
|
|
16
|
+
@contacts = Query.new(contacts.to_a.uniq).freeze
|
|
17
|
+
freeze
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def include?(contact)
|
|
21
|
+
contacts.any? { |member| member.equal?(contact) }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def to_h
|
|
25
|
+
{ record_id: record_id, name: name, source: source, contact_count: contacts.count }
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# lib/abbu/group_catalog.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'group'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
class GroupCatalog
|
|
8
|
+
def initialize(contacts)
|
|
9
|
+
@contacts = contacts
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def groups
|
|
13
|
+
entries_by_key = memberships.group_by { |contact, membership| [contact.source[:path], membership[:record_id]] }
|
|
14
|
+
groups = entries_by_key.values.map { |entries| build_group(entries) }
|
|
15
|
+
groups.sort_by { |group| [group.source[:relative_path], group.record_id] }.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def memberships
|
|
21
|
+
@contacts.flat_map do |contact|
|
|
22
|
+
contact.group_memberships.map { |membership| [contact, membership] }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def build_group(entries)
|
|
27
|
+
contact, membership = entries.first
|
|
28
|
+
Group.new(record_id: membership[:record_id], name: membership[:name], source: contact.source,
|
|
29
|
+
contacts: entries.map(&:first))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
data/lib/abbu/live_store.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
require 'pathname'
|
|
5
5
|
require 'sqlite3'
|
|
6
6
|
require_relative 'parsers/sqlite_parser'
|
|
7
|
+
require_relative 'source_catalog'
|
|
7
8
|
|
|
8
9
|
module Abbu
|
|
9
10
|
class LiveStore
|
|
@@ -47,6 +48,18 @@ module Abbu
|
|
|
47
48
|
end
|
|
48
49
|
end
|
|
49
50
|
|
|
51
|
+
def sources
|
|
52
|
+
@sources ||= SourceCatalog.new(database_paths, root_path: @path, contacts: contacts).sources
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def groups
|
|
56
|
+
@groups ||= sources.flat_map(&:groups).freeze
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def groups_for(contact)
|
|
60
|
+
groups.select { |group| group.include?(contact) }
|
|
61
|
+
end
|
|
62
|
+
|
|
50
63
|
private
|
|
51
64
|
|
|
52
65
|
def validate!
|
|
@@ -105,12 +105,14 @@ module Abbu
|
|
|
105
105
|
|
|
106
106
|
def groups_for(db, record_id)
|
|
107
107
|
query = <<-SQL
|
|
108
|
-
SELECT g.ZFIRSTNAME
|
|
108
|
+
SELECT g.Z_PK, g.ZFIRSTNAME
|
|
109
109
|
FROM Z_ABCDCONTACTGROUP j
|
|
110
110
|
JOIN ZABCDRECORD g ON j.Z_GROUP = g.Z_PK
|
|
111
111
|
WHERE j.Z_CONTACT = ?
|
|
112
112
|
SQL
|
|
113
|
-
optional_rows(db, record_id, 'Z_ABCDCONTACTGROUP', query).map
|
|
113
|
+
optional_rows(db, record_id, 'Z_ABCDCONTACTGROUP', query).map do |row|
|
|
114
|
+
{ record_id: row['Z_PK'], name: row['ZFIRSTNAME'] }
|
|
115
|
+
end
|
|
114
116
|
end
|
|
115
117
|
|
|
116
118
|
def urls_for(db, record_id)
|
|
@@ -237,7 +239,8 @@ module Abbu
|
|
|
237
239
|
contact.emails = emails_for(db, record_id)
|
|
238
240
|
contact.phones = phones_for(db, record_id)
|
|
239
241
|
contact.addresses = addresses_for(db, record_id)
|
|
240
|
-
contact.
|
|
242
|
+
contact.group_memberships = groups_for(db, record_id)
|
|
243
|
+
contact.groups = contact.group_memberships.map { |membership| membership[:name] }
|
|
241
244
|
contact.urls = urls_for(db, record_id)
|
|
242
245
|
contact.notes = notes_for(db, record_id)
|
|
243
246
|
contact.related_names = related_names_for(db, record_id)
|
data/lib/abbu/query.rb
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# lib/abbu/query.rb
|
|
2
2
|
# frozen_string_literal: true
|
|
3
3
|
|
|
4
|
+
require_relative 'timestamp_range'
|
|
5
|
+
|
|
4
6
|
module Abbu
|
|
5
7
|
class Query
|
|
6
8
|
include Enumerable
|
|
@@ -49,6 +51,23 @@ module Abbu
|
|
|
49
51
|
@contacts.dup
|
|
50
52
|
end
|
|
51
53
|
|
|
54
|
+
def in_group(group)
|
|
55
|
+
self.class.new(select { |contact| group.include?(contact) })
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def modified_since(value)
|
|
59
|
+
date_range(:modified_at, since: value)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def created_since(value)
|
|
63
|
+
date_range(:created_at, since: value)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def date_range(field, since: nil, before: nil)
|
|
67
|
+
range = TimestampRange.new(field, since: since, before: before)
|
|
68
|
+
self.class.new(select { |contact| range.include?(contact.public_send(field)) })
|
|
69
|
+
end
|
|
70
|
+
|
|
52
71
|
private
|
|
53
72
|
|
|
54
73
|
def matches_criteria?(contact, criteria)
|
data/lib/abbu/source.rb
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# lib/abbu/source.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require_relative 'group_catalog'
|
|
5
|
+
require_relative 'query'
|
|
6
|
+
|
|
7
|
+
module Abbu
|
|
8
|
+
# An observed input container, not an inferred Apple account or provider.
|
|
9
|
+
# Metadata and membership are frozen; contacts retain their existing mutability.
|
|
10
|
+
class Source
|
|
11
|
+
attr_reader :path, :relative_path, :kind, :identifier, :files, :contacts, :group_names, :groups
|
|
12
|
+
|
|
13
|
+
def initialize(path:, relative_path:, identifier:, files:, contacts:)
|
|
14
|
+
@path = copy_string(path)
|
|
15
|
+
@relative_path = copy_string(relative_path)
|
|
16
|
+
@identifier = copy_string(identifier)
|
|
17
|
+
@kind = identifier.nil? ? 'root' : 'source'
|
|
18
|
+
@files = snapshot_files(files)
|
|
19
|
+
@contacts = Query.new(contacts.to_a.dup).freeze
|
|
20
|
+
@group_names = snapshot_group_names
|
|
21
|
+
@groups = GroupCatalog.new(@contacts).groups
|
|
22
|
+
freeze
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# No currently supported evidence identifies an account provider.
|
|
26
|
+
def provider
|
|
27
|
+
nil
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def groups_for(contact)
|
|
31
|
+
groups.select { |group| group.include?(contact) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def to_h
|
|
35
|
+
{ path: path, relative_path: relative_path, kind: kind, identifier: identifier,
|
|
36
|
+
provider: provider, files: files, contact_count: contacts.count, group_names: group_names }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def copy_string(value)
|
|
42
|
+
value&.dup&.freeze
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def snapshot_files(files)
|
|
46
|
+
files.map { |file| file.transform_values { |value| copy_string(value) }.freeze }.freeze
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def snapshot_group_names
|
|
50
|
+
@contacts.flat_map(&:groups).compact.uniq.sort.map { |name| copy_string(name) }.freeze
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# lib/abbu/source_catalog.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'pathname'
|
|
5
|
+
require_relative 'source'
|
|
6
|
+
require_relative 'utils/source_descriptor'
|
|
7
|
+
|
|
8
|
+
module Abbu
|
|
9
|
+
# Internal adapter over the same file descriptors used by the parsers.
|
|
10
|
+
class SourceCatalog
|
|
11
|
+
def initialize(paths, root_path:, contacts:)
|
|
12
|
+
@root_path = Pathname.new(root_path).expand_path
|
|
13
|
+
@files = paths.map { |path| Utils::SourceDescriptor.new(path, root_path: root_path).to_h }
|
|
14
|
+
.sort_by { |file| file.fetch(:relative_path) }
|
|
15
|
+
@contacts_by_path = contacts.group_by { |contact| contact.source&.fetch(:path, nil) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def sources
|
|
19
|
+
@files.group_by { |file| file.fetch(:identifier) }.map do |identifier, files|
|
|
20
|
+
relative_path = identifier.nil? ? '.' : "Sources/#{identifier}"
|
|
21
|
+
Source.new(path: @root_path.join(relative_path).to_s, relative_path: relative_path,
|
|
22
|
+
identifier: identifier, files: files, contacts: contacts_for(files))
|
|
23
|
+
end.sort_by(&:relative_path).freeze
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def contacts_for(files)
|
|
29
|
+
files.flat_map { |file| @contacts_by_path.fetch(file.fetch(:path), []) }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# lib/abbu/timestamp_range.rb
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'date'
|
|
5
|
+
|
|
6
|
+
module Abbu
|
|
7
|
+
# Internal filter over observed timestamps, never a claim about human edits.
|
|
8
|
+
class TimestampRange
|
|
9
|
+
FORMAT = /\A\d{4}-\d{2}-\d{2}T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)\z/
|
|
10
|
+
|
|
11
|
+
def initialize(field, since: nil, before: nil)
|
|
12
|
+
raise ArgumentError, 'field must be :created_at or :modified_at' unless %i[created_at modified_at].include?(field)
|
|
13
|
+
raise ArgumentError, 'at least one timestamp bound is required' if since.nil? && before.nil?
|
|
14
|
+
|
|
15
|
+
@since = parse(since) unless since.nil?
|
|
16
|
+
@before = parse(before) unless before.nil?
|
|
17
|
+
validate_order
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def include?(value)
|
|
21
|
+
!value.nil? && (@since.nil? || value >= @since) && (@before.nil? || value < @before)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def validate_order
|
|
27
|
+
raise ArgumentError, 'since must be earlier than before' if @since && @before && @since >= @before
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def parse(value)
|
|
31
|
+
return value if value.is_a?(Time)
|
|
32
|
+
|
|
33
|
+
unless value.is_a?(String) && FORMAT.match?(value)
|
|
34
|
+
raise ArgumentError, 'timestamp must be a Time or ISO 8601 datetime with explicit timezone'
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
DateTime.iso8601(value, Date::GREGORIAN).to_time
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
data/lib/abbu/version.rb
CHANGED
data/lib/abbu.rb
CHANGED
|
@@ -9,6 +9,7 @@ require_relative 'abbu/archive'
|
|
|
9
9
|
require_relative 'abbu/live_store'
|
|
10
10
|
require_relative 'abbu/query'
|
|
11
11
|
require_relative 'abbu/schema_inspector'
|
|
12
|
+
require_relative 'abbu/source'
|
|
12
13
|
require_relative 'abbu/parsers/sqlite_parser'
|
|
13
14
|
require_relative 'abbu/parsers/plist_parser'
|
|
14
15
|
require_relative 'abbu/exporters/csv_exporter'
|
data/sig/group.rbs
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# sig/group.rbs
|
|
2
|
+
module Abbu
|
|
3
|
+
class Contact
|
|
4
|
+
attr_accessor group_memberships: Array[{ record_id: Integer, name: String? }]
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
class Group
|
|
8
|
+
attr_reader record_id: Integer
|
|
9
|
+
attr_reader name: String?
|
|
10
|
+
attr_reader source: Hash[Symbol, String?]
|
|
11
|
+
attr_reader contacts: Query
|
|
12
|
+
def initialize: (record_id: Integer, name: String?, source: Hash[Symbol, String?],
|
|
13
|
+
contacts: Enumerable[Contact]) -> void
|
|
14
|
+
def include?: (Contact contact) -> bool
|
|
15
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Source
|
|
19
|
+
attr_reader groups: Array[Group]
|
|
20
|
+
def groups_for: (Contact contact) -> Array[Group]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class Archive
|
|
24
|
+
def groups: () -> Array[Group]
|
|
25
|
+
def groups_for: (Contact contact) -> Array[Group]
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
class LiveStore
|
|
29
|
+
def groups: () -> Array[Group]
|
|
30
|
+
def groups_for: (Contact contact) -> Array[Group]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Query
|
|
34
|
+
def in_group: (Group group) -> Query
|
|
35
|
+
end
|
|
36
|
+
end
|
data/sig/query.rbs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# sig/query.rbs
|
|
2
|
+
module Abbu
|
|
3
|
+
class Contact
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
class Query
|
|
7
|
+
include Enumerable[Contact]
|
|
8
|
+
|
|
9
|
+
def initialize: (Enumerable[Contact] contacts) -> void
|
|
10
|
+
def each: () { (Contact) -> void } -> Array[Contact]
|
|
11
|
+
| () -> Enumerator[Contact, Array[Contact]]
|
|
12
|
+
def where: (Hash[Symbol, untyped] criteria) -> Query
|
|
13
|
+
def search: (String term) -> Query
|
|
14
|
+
def find_by_email: (String email) -> Query
|
|
15
|
+
def find_by_phone: (String phone) -> Query
|
|
16
|
+
def to_a: () -> Array[Contact]
|
|
17
|
+
def created_since: (Time | String value) -> Query
|
|
18
|
+
def modified_since: (Time | String value) -> Query
|
|
19
|
+
def date_range: (:created_at | :modified_at field, ?since: Time | String | nil, ?before: Time | String | nil) -> Query
|
|
20
|
+
end
|
|
21
|
+
end
|
data/sig/source.rbs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# sig/source.rbs
|
|
2
|
+
module Abbu
|
|
3
|
+
class Source
|
|
4
|
+
attr_reader path: String
|
|
5
|
+
attr_reader relative_path: String
|
|
6
|
+
attr_reader kind: String
|
|
7
|
+
attr_reader identifier: String?
|
|
8
|
+
attr_reader files: Array[Hash[Symbol, String?]]
|
|
9
|
+
attr_reader contacts: Query
|
|
10
|
+
attr_reader group_names: Array[String]
|
|
11
|
+
|
|
12
|
+
def initialize: (path: String, relative_path: String, identifier: String?,
|
|
13
|
+
files: Array[Hash[Symbol, String?]], contacts: Enumerable[Contact]) -> void
|
|
14
|
+
def provider: () -> nil
|
|
15
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Archive
|
|
19
|
+
def sources: () -> Array[Source]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class LiveStore
|
|
23
|
+
def sources: () -> Array[Source]
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: abbu
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.8.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Stan Carver II
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-10-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: sqlite3
|
|
@@ -220,8 +220,11 @@ files:
|
|
|
220
220
|
- README.md
|
|
221
221
|
- bin/abbu
|
|
222
222
|
- docs/ABBU.md
|
|
223
|
+
- docs/API_STABILITY.md
|
|
223
224
|
- docs/CHANGELOG.md
|
|
224
225
|
- docs/CONTRIBUTING.md
|
|
226
|
+
- docs/FORMAT_COMPATIBILITY.md
|
|
227
|
+
- docs/RELEASING.md
|
|
225
228
|
- docs/TODO.md
|
|
226
229
|
- examples/deduplicate_contacts.rb
|
|
227
230
|
- examples/export_to_api.rb
|
|
@@ -236,7 +239,11 @@ files:
|
|
|
236
239
|
- lib/abbu/diagnostic.rb
|
|
237
240
|
- lib/abbu/exporters/csv_exporter.rb
|
|
238
241
|
- lib/abbu/exporters/json_exporter.rb
|
|
242
|
+
- lib/abbu/exporters/vcard_document.rb
|
|
243
|
+
- lib/abbu/exporters/vcard_encoding.rb
|
|
239
244
|
- lib/abbu/exporters/vcard_exporter.rb
|
|
245
|
+
- lib/abbu/group.rb
|
|
246
|
+
- lib/abbu/group_catalog.rb
|
|
240
247
|
- lib/abbu/image_extractor.rb
|
|
241
248
|
- lib/abbu/live_store.rb
|
|
242
249
|
- lib/abbu/parse_error.rb
|
|
@@ -244,12 +251,19 @@ files:
|
|
|
244
251
|
- lib/abbu/parsers/sqlite_parser.rb
|
|
245
252
|
- lib/abbu/query.rb
|
|
246
253
|
- lib/abbu/schema_inspector.rb
|
|
254
|
+
- lib/abbu/source.rb
|
|
255
|
+
- lib/abbu/source_catalog.rb
|
|
256
|
+
- lib/abbu/timestamp_range.rb
|
|
247
257
|
- lib/abbu/utils/contact_identity.rb
|
|
248
258
|
- lib/abbu/utils/deduplicator.rb
|
|
249
259
|
- lib/abbu/utils/image_resolver.rb
|
|
250
260
|
- lib/abbu/utils/label_normalizer.rb
|
|
251
261
|
- lib/abbu/utils/source_descriptor.rb
|
|
252
262
|
- lib/abbu/version.rb
|
|
263
|
+
- sig/group.rbs
|
|
264
|
+
- sig/query.rbs
|
|
265
|
+
- sig/source.rbs
|
|
266
|
+
- sig/vcard_exporter.rbs
|
|
253
267
|
- tasks/abbu.rake
|
|
254
268
|
homepage: https://github.com/scarver2/abbu
|
|
255
269
|
licenses:
|