]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/storage/recipients/CdsiStore.java
1170e6513f7d78569cd832656a91dcf124235161
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / storage / recipients / CdsiStore.java
1 package org.asamk.signal.manager.storage.recipients;
2
3 import org.asamk.signal.manager.storage.Database;
4 import org.asamk.signal.manager.storage.Utils;
5
6 import java.sql.Connection;
7 import java.sql.SQLException;
8 import java.util.HashSet;
9 import java.util.Set;
10 import java.util.stream.Collectors;
11
12 public class CdsiStore {
13
14 private static final String TABLE_CDSI = "cdsi";
15
16 private final Database database;
17
18 public static void createSql(Connection connection) throws SQLException {
19 // When modifying the CREATE statement here, also add a migration in AccountDatabase.java
20 try (final var statement = connection.createStatement()) {
21 statement.executeUpdate("""
22 CREATE TABLE cdsi (
23 _id INTEGER PRIMARY KEY,
24 number TEXT NOT NULL UNIQUE,
25 last_seen_at INTEGER NOT NULL
26 ) STRICT;
27 """);
28 }
29 }
30
31 public CdsiStore(final Database database) {
32 this.database = database;
33 }
34
35 public Set<String> getAllNumbers() {
36 try (final var connection = database.getConnection()) {
37 return getAllNumbers(connection);
38 } catch (SQLException e) {
39 throw new RuntimeException("Failed read from cdsi store", e);
40 }
41 }
42
43 /**
44 * Saves the set of e164 numbers used after a full refresh.
45 *
46 * @param fullNumbers All the e164 numbers used in the last CDS query (previous and new).
47 * @param seenNumbers The E164 numbers that were seen in either the system contacts or recipients table. This is different from fullNumbers in that fullNumbers
48 * includes every number we've ever seen, even if it's not in our contacts anymore.
49 */
50 public void updateAfterFullCdsQuery(Set<String> fullNumbers, Set<String> seenNumbers) {
51 final var lastSeen = System.currentTimeMillis();
52 try (final var connection = database.getConnection()) {
53 final var existingNumbers = getAllNumbers(connection);
54
55 final var removedNumbers = new HashSet<>(existingNumbers) {{
56 removeAll(fullNumbers);
57 }};
58 removeNumbers(connection, removedNumbers);
59
60 final var addedNumbers = new HashSet<>(fullNumbers) {{
61 removeAll(existingNumbers);
62 }};
63 addNumbers(connection, addedNumbers, lastSeen);
64
65 updateLastSeen(connection, seenNumbers, lastSeen);
66 } catch (SQLException e) {
67 throw new RuntimeException("Failed update cdsi store", e);
68 }
69 }
70
71 /**
72 * Updates after a partial CDS query. Will not insert new entries.
73 * Instead, this will simply update the lastSeen timestamp of any entry we already have.
74 *
75 * @param seenNumbers The newly-added E164 numbers that we hadn't previously queried for.
76 */
77 public void updateAfterPartialCdsQuery(Set<String> seenNumbers) {
78 final var lastSeen = System.currentTimeMillis();
79
80 try (final var connection = database.getConnection()) {
81 updateLastSeen(connection, seenNumbers, lastSeen);
82 } catch (SQLException e) {
83 throw new RuntimeException("Failed update cdsi store", e);
84 }
85 }
86
87 private static Set<String> getAllNumbers(final Connection connection) throws SQLException {
88 final var sql = (
89 """
90 SELECT c.number
91 FROM %s c
92 """
93 ).formatted(TABLE_CDSI);
94 try (final var statement = connection.prepareStatement(sql)) {
95 try (var result = Utils.executeQueryForStream(statement, r -> r.getString("number"))) {
96 return result.collect(Collectors.toSet());
97 }
98 }
99 }
100
101 private static void removeNumbers(
102 final Connection connection, final Set<String> numbers
103 ) throws SQLException {
104 final var sql = (
105 """
106 DELETE FROM %s
107 WHERE number = ?
108 """
109 ).formatted(TABLE_CDSI);
110 try (final var statement = connection.prepareStatement(sql)) {
111 for (final var number : numbers) {
112 statement.setString(1, number);
113 statement.executeUpdate();
114 }
115 }
116 }
117
118 private static void addNumbers(
119 final Connection connection, final Set<String> numbers, final long lastSeen
120 ) throws SQLException {
121 final var sql = (
122 """
123 INSERT INTO %s (number, last_seen_at)
124 VALUES (?, ?)
125 ON CONFLICT (number) DO UPDATE SET last_seen_at = excluded.last_seen_at
126 """
127 ).formatted(TABLE_CDSI);
128 try (final var statement = connection.prepareStatement(sql)) {
129 for (final var number : numbers) {
130 statement.setString(1, number);
131 statement.setLong(2, lastSeen);
132 statement.executeUpdate();
133 }
134 }
135 }
136
137 private static void updateLastSeen(
138 final Connection connection, final Set<String> numbers, final long lastSeen
139 ) throws SQLException {
140 final var sql = (
141 """
142 UPDATE %s
143 SET last_seen_at = ?
144 WHERE number = ?
145 """
146 ).formatted(TABLE_CDSI);
147 try (final var statement = connection.prepareStatement(sql)) {
148 for (final var number : numbers) {
149 statement.setLong(1, lastSeen);
150 statement.setString(2, number);
151 statement.executeUpdate();
152 }
153 }
154 }
155
156 public void clearAll() {
157 final var sql = (
158 """
159 DELETE FROM %s
160 """
161 ).formatted(TABLE_CDSI);
162 try (final var connection = database.getConnection()) {
163 try (final var statement = connection.prepareStatement(sql)) {
164 statement.executeUpdate();
165 }
166 } catch (SQLException e) {
167 throw new RuntimeException("Failed update cdsi store", e);
168 }
169 }
170 }