1
0
mirror of https://github.com/google/nomulus synced 2026-05-21 23:31:51 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
gbrodman
7aec579d96 Add DB annotations to console User and related classes (#1757)
We added the DB code last week, this is the corresponding bit now that
that has been released.
2022-08-25 16:54:39 -04:00
Lai Jiang
b9f8faa165 Drop autorenew poll message history id column from the domain table (#1743)
We stopped using the column since #1732.
2022-08-25 15:52:32 -04:00
Pavlo Tkach
b0e4e86586 Add registry email to bcc for outgoing DNS failure emails (#1755) 2022-08-25 14:15:20 -04:00
18 changed files with 2032 additions and 1768 deletions

View File

@@ -1320,8 +1320,14 @@ public final class RegistryConfig {
@Provides
@Config("registrySupportEmail")
public static String provideRegistrySupportEmail(RegistryConfigSettings config) {
return config.dnsUpdate.registrySupportEmail;
public static InternetAddress provideRegistrySupportEmail(RegistryConfigSettings config) {
return parseEmailAddress(config.dnsUpdate.registrySupportEmail);
}
@Provides
@Config("registryCcEmail")
public static InternetAddress provideRegistryCcEmail(RegistryConfigSettings config) {
return parseEmailAddress(config.dnsUpdate.registryCcEmail);
}
@Provides

View File

@@ -254,5 +254,6 @@ public class RegistryConfigSettings {
public String dnsUpdateFailEmailBodyText;
public String dnsUpdateFailRegistryName;
public String registrySupportEmail;
public String registryCcEmail;
}
}

View File

@@ -481,6 +481,7 @@ contactHistory:
dnsUpdate:
dnsUpdateFailRegistryName: Example name
registrySupportEmail: email@example.com
registryCcEmail: email@example.com
# Email subject text template to notify partners after repeatedly failing DNS update
dnsUpdateFailEmailSubjectText: "[ACTION REQUIRED]: Incomplete DNS Update"
# Email body text template for failing DNS update that accepts 5 parameters:

View File

@@ -34,6 +34,7 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InternetDomainName;
import dagger.Lazy;
import google.registry.config.RegistryConfig.Config;
import google.registry.dns.DnsMetrics.ActionStatus;
import google.registry.dns.DnsMetrics.CommitStatus;
@@ -52,14 +53,16 @@ import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import google.registry.ui.server.SendEmailUtils;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import google.registry.util.DomainNameUtils;
import google.registry.util.EmailMessage;
import google.registry.util.SendEmailService;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -109,11 +112,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
private final Clock clock;
private final CloudTasksUtils cloudTasksUtils;
private final Response response;
private final SendEmailUtils sendEmailUtils;
private final SendEmailService sendEmailService;
private final String dnsUpdateFailEmailSubjectText;
private final String dnsUpdateFailEmailBodyText;
private final String dnsUpdateFailRegistryName;
private final String registrySupportEmail;
private final Lazy<InternetAddress> registrySupportEmail;
private final Lazy<InternetAddress> registryCcEmail;
private final InternetAddress gSuiteOutgoingEmailAddress;
@Inject
public PublishDnsUpdatesAction(
@@ -129,7 +134,9 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
@Config("dnsUpdateFailEmailSubjectText") String dnsUpdateFailEmailSubjectText,
@Config("dnsUpdateFailEmailBodyText") String dnsUpdateFailEmailBodyText,
@Config("dnsUpdateFailRegistryName") String dnsUpdateFailRegistryName,
@Config("registrySupportEmail") String registrySupportEmail,
@Config("registrySupportEmail") Lazy<InternetAddress> registrySupportEmail,
@Config("registryCcEmail") Lazy<InternetAddress> registryCcEmail,
@Config("gSuiteOutgoingEmailAddress") InternetAddress gSuiteOutgoingEmailAddress,
@Header(APP_ENGINE_RETRY_HEADER) Optional<Integer> appEngineRetryCount,
@Header(CLOUD_TASKS_RETRY_HEADER) Optional<Integer> cloudTasksRetryCount,
DnsQueue dnsQueue,
@@ -138,13 +145,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
LockHandler lockHandler,
Clock clock,
CloudTasksUtils cloudTasksUtils,
SendEmailUtils sendEmailUtils,
SendEmailService sendEmailService,
Response response) {
this.dnsQueue = dnsQueue;
this.dnsWriterProxy = dnsWriterProxy;
this.dnsMetrics = dnsMetrics;
this.timeout = timeout;
this.sendEmailUtils = sendEmailUtils;
this.sendEmailService = sendEmailService;
this.retryCount =
cloudTasksRetryCount.orElse(
appEngineRetryCount.orElseThrow(
@@ -165,6 +172,8 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
this.dnsUpdateFailEmailSubjectText = dnsUpdateFailEmailSubjectText;
this.dnsUpdateFailRegistryName = dnsUpdateFailRegistryName;
this.registrySupportEmail = registrySupportEmail;
this.registryCcEmail = registryCcEmail;
this.gSuiteOutgoingEmailAddress = gSuiteOutgoingEmailAddress;
}
private void recordActionResult(ActionStatus status) {
@@ -267,25 +276,48 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
return null;
}
private InternetAddress emailToInternetAddress(String email) {
try {
return new InternetAddress(email, true);
} catch (Exception e) {
logger.atWarning().withCause(e).log(
String.format(
"Could not parse email contact %s to send DNS failure notification", email));
return null;
}
}
/** Sends an email to partners regarding a failure during DNS update */
private void notifyWithEmailAboutDnsUpdateFailure(
String registrarId, String hostOrDomainName, Boolean isHost) {
Optional<Registrar> registrar = Registrar.loadByRegistrarIdCached(registrarId);
if (registrar.isPresent()) {
sendEmailUtils.sendEmail(
dnsUpdateFailEmailSubjectText,
String body =
String.format(
dnsUpdateFailEmailBodyText,
registrar.get().getRegistrarName(),
hostOrDomainName,
isHost ? "host" : "domain",
registrySupportEmail,
dnsUpdateFailRegistryName),
registrySupportEmail.get().getAddress(),
dnsUpdateFailRegistryName);
ImmutableList<InternetAddress> recipients =
registrar.get().getContacts().stream()
.filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.collect(toImmutableList()));
.map(this::emailToInternetAddress)
.collect(toImmutableList());
sendEmailService.sendEmail(
EmailMessage.newBuilder()
.setBody(body)
.setSubject(dnsUpdateFailEmailSubjectText)
.setRecipients(recipients)
.addBcc(registryCcEmail.get())
.setFrom(gSuiteOutgoingEmailAddress)
.build());
} else {
logger.atSevere().log(String.format("Could not find registrar %s", registrarId));
}

View File

@@ -24,20 +24,34 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
@Entity
@Table(
indexes = {
@Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
@Index(columnList = "emailAddress", name = "user_email_address_idx")
})
public class User extends ImmutableObject implements Buildable {
/** Autogenerated unique ID of this user. */
private long id;
@Id private long id;
/** GAIA ID associated with the user in question. */
@Column(nullable = false)
private String gaiaId;
/** Email address of the user in question. */
@Column(nullable = false)
private String emailAddress;
/** Roles (which grant permissions) associated with this user. */
@Column(nullable = false)
private UserRoles userRoles;
/**

View File

@@ -21,6 +21,10 @@ import com.google.common.collect.ImmutableMap;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
/**
* Contains the global and per-registrar roles for a given user.
@@ -28,15 +32,19 @@ import java.util.Map;
* <p>See <a href="https://go/nomulus-console-authz">go/nomulus-console-authz</a> for more
* information.
*/
@Embeddable
public class UserRoles extends ImmutableObject implements Buildable {
/** Whether the user is a global admin, who has access to everything. */
@Column(nullable = false)
private boolean isAdmin = false;
/**
* The global role (e.g. {@link GlobalRole#SUPPORT_AGENT}) that the user has across all
* registrars.
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private GlobalRole globalRole = GlobalRole.NONE;
/** Any per-registrar roles that this user may have. */

View File

@@ -0,0 +1,50 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.converter;
import google.registry.model.console.RegistrarRole;
import java.util.Map;
import javax.persistence.Converter;
/** JPA converter for storing / retrieving {@code Map<String, RegistrarRole>} objects. */
@Converter(autoApply = true)
public class RegistrarToRoleConverter
extends StringMapConverterBase<String, RegistrarRole, Map<String, RegistrarRole>> {
@Override
protected String convertKeyToString(String key) {
return key;
}
@Override
protected String convertValueToString(RegistrarRole value) {
return value.toString();
}
@Override
protected String convertStringToKey(String string) {
return string;
}
@Override
protected RegistrarRole convertStringToValue(String string) {
return RegistrarRole.valueOf(string);
}
@Override
protected Map<String, RegistrarRole> convertMapToDerivedType(Map<String, RegistrarRole> map) {
return map;
}
}

View File

@@ -43,6 +43,7 @@
<class>google.registry.model.billing.BillingEvent$Recurring</class>
<class>google.registry.model.common.Cursor</class>
<class>google.registry.model.common.DatabaseMigrationStateSchedule</class>
<class>google.registry.model.console.User</class>
<class>google.registry.model.contact.ContactHistory</class>
<class>google.registry.model.contact.ContactResource</class>
<class>google.registry.model.domain.Domain</class>
@@ -91,6 +92,7 @@
<class>google.registry.persistence.converter.LocalDateConverter</class>
<class>google.registry.persistence.converter.PostalInfoChoiceListConverter</class>
<class>google.registry.persistence.converter.RegistrarPocSetConverter</class>
<class>google.registry.persistence.converter.RegistrarToRoleConverter</class>
<class>google.registry.persistence.converter.Spec11ThreatMatchThreatTypeSetConverter</class>
<class>google.registry.persistence.converter.StatusValueSetConverter</class>
<class>google.registry.persistence.converter.StringListConverter</class>

View File

@@ -38,9 +38,9 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import dagger.Lazy;
import google.registry.dns.DnsMetrics.ActionStatus;
import google.registry.dns.DnsMetrics.CommitStatus;
import google.registry.dns.DnsMetrics.PublishStatus;
@@ -55,7 +55,7 @@ import google.registry.testing.CloudTasksHelper.TaskMatcher;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import google.registry.testing.FakeResponse;
import google.registry.ui.server.SendEmailUtils;
import google.registry.testing.Lazies;
import google.registry.util.EmailMessage;
import google.registry.util.SendEmailService;
import java.util.Optional;
@@ -83,19 +83,17 @@ public class PublishDnsUpdatesActionTest {
private final DnsQueue dnsQueue = mock(DnsQueue.class);
private final CloudTasksHelper cloudTasksHelper = new CloudTasksHelper();
private PublishDnsUpdatesAction action;
private InternetAddress outgoingRegistry;
private Lazy<InternetAddress> registrySupportEmail;
private Lazy<InternetAddress> registryCcEmail;
private final SendEmailService emailService = mock(SendEmailService.class);
private SendEmailUtils sendEmailUtils;
@BeforeEach
void beforeEach() throws Exception {
sendEmailUtils =
new SendEmailUtils(
new InternetAddress("outgoing@registry.example"),
"UnitTest Registry",
ImmutableList.of("notification@test.example", "notification2@test.example"),
emailService);
createTld("xn--q9jyb4c");
outgoingRegistry = new InternetAddress("outgoing@registry.example");
registrySupportEmail = Lazies.of(new InternetAddress("registry@test.com"));
registryCcEmail = Lazies.of(new InternetAddress("registry-cc@test.com"));
persistResource(
Registry.get("xn--q9jyb4c")
.asBuilder()
@@ -143,6 +141,7 @@ public class PublishDnsUpdatesActionTest {
int lockIndex,
int numPublishLocks,
LockHandler lockHandler) {
return new PublishDnsUpdatesAction(
dnsWriterString,
clock.nowUtc().minusHours(1),
@@ -155,8 +154,10 @@ public class PublishDnsUpdatesActionTest {
Duration.standardSeconds(10),
"Subj",
"Body %1$s %2$s %3$s %4$s %5$s",
"registry@test.com",
"awesomeRegistry",
registrySupportEmail,
registryCcEmail,
outgoingRegistry,
Optional.ofNullable(retryCount),
Optional.empty(),
dnsQueue,
@@ -165,7 +166,7 @@ public class PublishDnsUpdatesActionTest {
lockHandler,
clock,
cloudTasksHelper.getTestCloudTasksUtils(),
sendEmailUtils,
emailService,
response);
}
@@ -413,7 +414,9 @@ public class PublishDnsUpdatesActionTest {
assertThat(emailMessage.subject()).isEqualTo("Subj");
assertThat(emailMessage.body())
.isEqualTo(
"Body The Registrar example.xn--q9jyb4c domain awesomeRegistry registry@test.com");
"Body The Registrar example.xn--q9jyb4c domain registry@test.com awesomeRegistry");
assertThat(emailMessage.bccs().stream().findFirst().get().toString())
.isEqualTo("registry-cc@test.com");
}
@Test
@@ -429,7 +432,9 @@ public class PublishDnsUpdatesActionTest {
assertThat(emailMessage.subject()).isEqualTo("Subj");
assertThat(emailMessage.body())
.isEqualTo(
"Body The Registrar ns1.example.xn--q9jyb4c host awesomeRegistry registry@test.com");
"Body The Registrar ns1.example.xn--q9jyb4c host registry@test.com awesomeRegistry");
assertThat(emailMessage.bccs().stream().findFirst().get().toString())
.isEqualTo("registry-cc@test.com");
}
@Test

View File

@@ -15,12 +15,71 @@
package google.registry.model.console;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.junit.jupiter.api.Assertions.assertThrows;
import google.registry.model.EntityTestCase;
import org.junit.jupiter.api.Test;
/** Tests for {@link User}. */
public class UserTest {
public class UserTest extends EntityTestCase {
UserTest() {
super(JpaEntityCoverageCheck.ENABLED);
}
@Test
void testPersistence_lookupByEmail() {
User user =
new User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build();
persistResource(user);
jpaTm()
.transact(
() -> {
assertThat(
jpaTm()
.query("FROM User WHERE emailAddress = 'email@email.com'", User.class)
.getSingleResult())
.isEqualTo(user);
assertThat(
jpaTm()
.query("FROM User WHERE emailAddress = 'bad@fake.com'", User.class)
.getResultList())
.isEmpty();
});
}
@Test
void testPersistence_lookupByGaiaId() {
User user =
new User.Builder()
.setGaiaId("gaiaId")
.setEmailAddress("email@email.com")
.setUserRoles(
new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).setIsAdmin(true).build())
.build();
persistResource(user);
jpaTm()
.transact(
() -> {
assertThat(
jpaTm()
.query("FROM User WHERE gaiaId = 'gaiaId'", User.class)
.getSingleResult())
.isEqualTo(user);
assertThat(
jpaTm()
.query("FROM User WHERE gaiaId = 'badGaiaId'", User.class)
.getResultList())
.isEmpty();
});
}
@Test
void testFailure_badInputs() {

View File

@@ -0,0 +1,68 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.converter;
import static com.google.common.truth.Truth.assertThat;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import google.registry.model.ImmutableObject;
import google.registry.model.console.RegistrarRole;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaUnitTestExtension;
import google.registry.testing.DatabaseHelper;
import java.util.Map;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link RegistrarToRoleConverter}. */
public class RegistrarToRoleConverterTest {
@RegisterExtension
public final JpaUnitTestExtension jpa =
new JpaTestExtensions.Builder().withEntityClass(TestEntity.class).buildUnitTestExtension();
@Test
void testRoundTripConversion() {
Map<String, RegistrarRole> map =
ImmutableMap.of(
"TheRegistrar",
RegistrarRole.ACCOUNT_MANAGER,
"NewRegistrar",
RegistrarRole.PRIMARY_CONTACT,
"FooRegistrar",
RegistrarRole.TECH_CONTACT);
TestEntity entity = new TestEntity(map);
DatabaseHelper.insertInDb(entity);
TestEntity persisted = Iterables.getOnlyElement(DatabaseHelper.loadAllOf(TestEntity.class));
assertThat(persisted.map).isEqualTo(map);
}
@Entity(name = "TestEntity")
private static class TestEntity extends ImmutableObject {
@Id String name = "id";
Map<String, RegistrarRole> map;
private TestEntity() {}
private TestEntity(Map<String, RegistrarRole> map) {
this.map = map;
}
}
}

View File

@@ -18,6 +18,7 @@ import static com.google.common.truth.Truth.assert_;
import google.registry.model.billing.BillingEventTest;
import google.registry.model.common.CursorTest;
import google.registry.model.console.UserTest;
import google.registry.model.contact.ContactResourceTest;
import google.registry.model.domain.DomainSqlTest;
import google.registry.model.domain.token.AllocationTokenTest;
@@ -101,6 +102,7 @@ import org.junit.runner.RunWith;
SignedMarkRevocationListDaoTest.class,
Spec11ThreatMatchTest.class,
TmchCrlTest.class,
UserTest.class,
// AfterSuiteTest must be the last entry. See class javadoc for details.
AfterSuiteTest.class
})

View File

@@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2022-08-18 20:43:24.136129</td>
<td class="property_value">2022-08-20 00:16:00.917251</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V125__create_package_promotion.sql</td>
<td id="lastFlywayFile" class="property_value">V126__drop_autorenew_poll_message_history_id_column_in_domain_table.sql</td>
</tr>
</tbody>
</table>
@@ -284,7 +284,7 @@ td.section {
generated on
</text>
<text text-anchor="start" x="3835.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2022-08-18 20:43:24.136129
2022-08-20 00:16:00.917251
</text>
<polygon fill="none" stroke="#888888" points="3748,-4 3748,-44 4013,-44 4013,-4 3748,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">

File diff suppressed because it is too large Load Diff

View File

@@ -123,3 +123,4 @@ V122__add_current_package_token_to_domain.sql
V123__drop_unused_columns_in_billing_cancellation_table.sql
V124__add_console_user.sql
V125__create_package_promotion.sql
V126__drop_autorenew_poll_message_history_id_column_in_domain_table.sql

View File

@@ -0,0 +1,15 @@
-- Copyright 2022 The Nomulus Authors. All Rights Reserved.
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
ALTER TABLE "Domain" DROP COLUMN autorenew_poll_message_history_id;

View File

@@ -740,6 +740,18 @@
url text not null,
primary key (id)
);
create table "User" (
id int8 not null,
email_address text not null,
gaia_id text not null,
registry_lock_password_hash text,
registry_lock_password_salt text,
global_role text not null,
is_admin boolean not null,
registrar_roles hstore,
primary key (id)
);
create index allocation_token_domain_name_idx on "AllocationToken" (domain_name);
create index IDX9g3s7mjv1yn4t06nqid39whss on "AllocationToken" (token_type);
create index IDXtmlqd31dpvvd2g1h9i7erw6aj on "AllocationToken" (redemption_domain_repo_id);
@@ -825,6 +837,8 @@ create index reservedlist_name_idx on "ReservedList" (name);
create index spec11threatmatch_registrar_id_idx on "Spec11ThreatMatch" (registrar_id);
create index spec11threatmatch_tld_idx on "Spec11ThreatMatch" (tld);
create index spec11threatmatch_check_date_idx on "Spec11ThreatMatch" (check_date);
create index user_gaia_id_idx on "User" (gaia_id);
create index user_email_address_idx on "User" (email_address);
alter table if exists "DelegationSignerData"
add constraint FKtr24j9v14ph2mfuw2gsmt12kq

View File

@@ -391,7 +391,6 @@ CREATE TABLE public."Domain" (
autorenew_poll_message_id bigint,
deletion_poll_message_id bigint,
autorenew_end_time timestamp with time zone,
autorenew_poll_message_history_id bigint,
transfer_autorenew_poll_message_history_id bigint,
transfer_history_entry_id bigint,
transfer_repo_id text,