Import bookmarks and identities from the TeamSpeak 3 client

The official client keeps what a myTeamSpeak account synchronises as protobuf
Item_Data blobs in the ProtobufItems table of settings.db. Decode them, adopt
them on first run when we have no identity yet, and offer a re-import from
Manage Bookmarks and Identities. The settings.db reader moves from contacts to
a new teamspeak package.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-23 19:02:52 +00:00
parent bab89f9b50
commit dbb425403a
20 changed files with 805 additions and 76 deletions

View File

@@ -0,0 +1,82 @@
package com.ts3client.teamspeak;
import com.ts3client.config.IdentityStore;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* The fixture is the {@code ProtobufItems} table a fresh official client wrote
* (config, hotkey profile, "Default" identity, "TeamSpeak Public" bookmark,
* checksum), plus a folder, two more bookmarks, a deleted bookmark and a
* whisper list appended in the same encoding.
*/
class SyncItemsTest {
private static final String IDENTITY_UUID = "965483cf-210f-dd22-ee1f-33591b0587a4";
private static final String FOLDER_UUID = "0f0f0f0f-0000-4000-8000-000000000001";
static File fixture() {
return new File(SyncItemsTest.class.getResource("/teamspeak/sync.db").getFile());
}
@Test
void readsBookmarksIdentitiesAndFoldersOnly() throws Exception {
List<SyncItem> items = TeamSpeakSettingsDb.readSyncItems(fixture());
assertEquals(5, items.size());
assertEquals(List.of(IDENTITY_UUID, "ea120ce1-b68b-b314-46ee-e16b3bf10342", FOLDER_UUID,
"bbbbbbbb-0000-4000-8000-000000000002", "bbbbbbbb-0000-4000-8000-000000000003"),
items.stream().map(SyncItem::uuid).toList());
}
@Test
void decodesTheClientsOwnIdentityAndBookmark() throws Exception {
List<SyncItem> items = TeamSpeakSettingsDb.readSyncItems(fixture());
SyncItem.Identity identity = (SyncItem.Identity) items.get(0);
assertEquals("Default", identity.name());
assertEquals("SyncTester", identity.nickname());
assertEquals("", identity.parentUuid());
assertTrue(identity.isDefault());
assertTrue(identity.identity().matches("\\d+V[\\w+/=]+"));
assertNotNull(IdentityStore.parseIdentityString(identity.identity()).getUid());
SyncItem.Bookmark bookmark = (SyncItem.Bookmark) items.get(1);
assertEquals("TeamSpeak Public", bookmark.name());
assertEquals("voice.teamspeak.com", bookmark.address());
assertEquals(9987, bookmark.port());
assertEquals("", bookmark.identityUuid(), "connects with the default identity");
assertEquals("", bookmark.nickname());
assertFalse(bookmark.autoconnect());
}
@Test
void decodesFoldersAndEveryBookmarkField() throws Exception {
List<SyncItem> items = TeamSpeakSettingsDb.readSyncItems(fixture());
SyncItem.Folder folder = (SyncItem.Folder) items.get(2);
assertEquals("Clan servers", folder.name());
SyncItem.Bookmark inFolder = (SyncItem.Bookmark) items.get(3);
assertEquals(FOLDER_UUID, inFolder.parentUuid());
assertEquals("Lixko", inFolder.name());
assertEquals(IDENTITY_UUID, inFolder.identityUuid());
assertEquals("server.lixko.eu", inFolder.address());
assertEquals("Bookmark Nick", inFolder.nickname());
assertEquals("hunter2", inFolder.serverPassword());
assertEquals("Lobby/Games", inFolder.defaultChannel());
assertEquals("chanpw", inFolder.defaultChannelPassword());
assertTrue(inFolder.autoconnect());
SyncItem.Bookmark byId = (SyncItem.Bookmark) items.get(4);
assertEquals(9988, byId.port());
assertEquals(42, byId.defaultChannelId());
assertEquals("", byId.identityUuid());
}
}

View File

@@ -0,0 +1,85 @@
package com.ts3client.teamspeak;
import com.ts3client.config.Bookmark;
import com.ts3client.config.Bookmarks;
import com.ts3client.config.IdentityEntry;
import com.ts3client.config.IdentityStore;
import com.ts3client.config.Settings;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import java.io.File;
import java.nio.file.Files;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
class TeamSpeakImporterTest {
/** The stores resolve {@code ~/.ts3jclient} once, so point them at scratch space before they load. */
@BeforeAll
static void isolateHome() throws Exception {
System.setProperty("user.home", Files.createTempDirectory("ts3j-import").toString());
}
@Test
void importsBookmarksWithTheIdentitiesTheyUse() throws Exception {
Settings settings = new Settings();
settings.nickname = "Fallback";
IdentityStore identities = IdentityStore.load(settings);
Bookmarks bookmarks = new Bookmarks();
bookmarks.add(new Bookmark("TeamSpeak Public", "VOICE.teamspeak.com", 9987, "me", ""));
TeamSpeakImporter importer = new TeamSpeakImporter(identities, bookmarks, settings);
TeamSpeakImporter.Result result = importer.importBookmarks(SyncItemsTest.fixture());
assertEquals(1, result.identitiesAdded());
assertEquals(2, result.bookmarksAdded());
assertEquals(1, result.bookmarksKnown());
IdentityEntry imported = identities.all().get(0);
assertEquals("Default", imported.getName());
assertEquals(imported.getId(), settings.defaultIdentityId);
assertTrue(imported.getFile().isFile());
List<Bookmark> all = bookmarks.all();
assertEquals(3, all.size());
Bookmark lixko = all.get(1);
assertEquals("Lixko", lixko.label);
assertEquals("Bookmark Nick", lixko.nickname);
assertEquals("hunter2", lixko.password);
assertEquals("Lobby/Games", lixko.channel);
assertEquals("chanpw", lixko.channelPassword);
assertEquals(imported.getId(), lixko.identityId);
assertTrue(lixko.connectOnStartup);
Bookmark byId = all.get(2);
assertEquals("/42", byId.channel);
assertEquals("SyncTester", byId.nickname, "the default identity's nickname");
assertEquals("", byId.identityId, "connects with whatever the default identity is");
assertEquals("SyncTester", settings.nickname);
// Running it again changes nothing.
result = importer.importBookmarks(SyncItemsTest.fixture());
assertEquals(0, result.identitiesAdded());
assertEquals(0, result.bookmarksAdded());
assertEquals(3, bookmarks.all().size());
assertEquals(1, identities.all().size());
}
@Test
void identityImportRecognisesAKnownKey() throws Exception {
Settings settings = new Settings();
IdentityStore identities = IdentityStore.load(settings);
int before = identities.all().size();
TeamSpeakImporter importer = new TeamSpeakImporter(identities, new Bookmarks(), settings);
TeamSpeakImporter.Result first = importer.importIdentities(SyncItemsTest.fixture());
TeamSpeakImporter.Result second = importer.importIdentities(SyncItemsTest.fixture());
assertEquals(1, first.identities());
assertEquals(1, second.identitiesKnown());
assertEquals(before + first.identitiesAdded(), identities.all().size());
assertNotNull(identities.byId(settings.defaultIdentityId));
}
}

View File

@@ -1,5 +1,8 @@
package com.ts3client.contacts;
package com.ts3client.teamspeak;
import com.ts3client.contacts.Contact;
import com.ts3client.contacts.ContactType;
import com.ts3client.contacts.NicknameDisplay;
import org.junit.jupiter.api.Test;
import java.io.File;
@@ -12,7 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
class TeamSpeakSettingsDbTest {
private static File fixture() {
return new File(TeamSpeakSettingsDbTest.class.getResource("/contacts/settings.db").getFile());
return new File(TeamSpeakSettingsDbTest.class.getResource("/teamspeak/settings.db").getFile());
}
@Test

Binary file not shown.