feat: TikTok temp downloads + Lavalink timeout fix + ModerationLogger null check

- TikTok: videos >8MB saved to temp files, download link via /d/{key} (24h TTL)
- TempFileManager: persistent JSON metadata, lazy cleanup
- HealthHttpServer: added /d/{key} endpoint for file downloads
- BotConfig: added externalUrl field (EXTERNAL_URL env var)
- LavalinkManager: httpTimeout 30s → 60s
- BotInitializer: stagger delays between bot Lavalink connections
- ModerationLogger: null check for log_channel_id config
- TikTokCommand: empty title fallback (Title may not be empty fix)
This commit is contained in:
loki5512344 2026-08-06 17:47:27 +02:00
parent 43735de681
commit ce1d863009
Signed by: boba
GPG key ID: 253067914055423B
11 changed files with 334 additions and 76 deletions

View file

@ -18,6 +18,7 @@ public class BotConfig {
private final int aiDossierUpdateHours; private final int aiDossierUpdateHours;
private final String aiDebateChannelId; private final String aiDebateChannelId;
private final LavalinkConfig lavalinkConfig; private final LavalinkConfig lavalinkConfig;
private final String externalUrl;
public BotConfig( public BotConfig(
String mainToken, String mainToken,
@ -36,7 +37,8 @@ public class BotConfig {
int aiDossierMsgs, int aiDossierMsgs,
int aiDossierUpdateHours, int aiDossierUpdateHours,
String aiDebateChannelId, String aiDebateChannelId,
LavalinkConfig lavalinkConfig) { LavalinkConfig lavalinkConfig,
String externalUrl) {
this.mainToken = mainToken; this.mainToken = mainToken;
this.musicToken = musicToken; this.musicToken = musicToken;
this.radioToken = radioToken; this.radioToken = radioToken;
@ -54,6 +56,7 @@ public class BotConfig {
this.aiDossierUpdateHours = aiDossierUpdateHours; this.aiDossierUpdateHours = aiDossierUpdateHours;
this.aiDebateChannelId = aiDebateChannelId; this.aiDebateChannelId = aiDebateChannelId;
this.lavalinkConfig = lavalinkConfig; this.lavalinkConfig = lavalinkConfig;
this.externalUrl = externalUrl;
} }
public String getMainToken() { public String getMainToken() {
@ -127,4 +130,8 @@ public class BotConfig {
public LavalinkConfig getLavalinkConfig() { public LavalinkConfig getLavalinkConfig() {
return lavalinkConfig; return lavalinkConfig;
} }
public String getExternalUrl() {
return externalUrl;
}
} }

View file

@ -86,6 +86,9 @@ public final class ConfigLoader {
String aiDebateChannelId = dotenv.get("AI_DEBATE_CHANNEL_ID"); String aiDebateChannelId = dotenv.get("AI_DEBATE_CHANNEL_ID");
LavalinkConfig lavalinkConfig = getLavalinkConfig(); LavalinkConfig lavalinkConfig = getLavalinkConfig();
String externalUrl = getEnv(dotenv, "EXTERNAL_URL", "http://127.0.0.1:8712");
LoggerDecorator.success(logger, "Конфигурация загружена успешно"); LoggerDecorator.success(logger, "Конфигурация загружена успешно");
return new BotConfig( return new BotConfig(
mainToken, mainToken,
@ -104,6 +107,7 @@ public final class ConfigLoader {
aiDossierMsgs, aiDossierMsgs,
aiDossierUpdateHours, aiDossierUpdateHours,
aiDebateChannelId, aiDebateChannelId,
lavalinkConfig); lavalinkConfig,
externalUrl);
} }
} }

View file

@ -77,6 +77,7 @@ public class BotInitializer {
JDA musicJda = createMusicJDA(); JDA musicJda = createMusicJDA();
JDA radioJda = null; JDA radioJda = null;
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) { if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
sleep(3000);
LavalinkManager radioLavalink = LavalinkManager radioLavalink =
LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken()); LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken());
radioJda = createRadioJDA(radioLavalink); radioJda = createRadioJDA(radioLavalink);
@ -144,4 +145,12 @@ public class BotInitializer {
} }
return JdaFactory.awaitJda(builder.build()); return JdaFactory.awaitJda(builder.build());
} }
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} }

View file

@ -1,9 +1,12 @@
package creator.loki.feature.metrics; package creator.loki.feature.metrics;
import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.BindException; import java.net.BindException;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import com.google.inject.Inject; import com.google.inject.Inject;
@ -12,26 +15,27 @@ import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpServer; import com.sun.net.httpserver.HttpServer;
import creator.loki.config.BotConfig; import creator.loki.config.BotConfig;
import creator.loki.core.Lifecycle; import creator.loki.core.Lifecycle;
import creator.loki.feature.tiktok.TempFile;
import creator.loki.feature.tiktok.TempFileManager;
import creator.loki.util.LoggerDecorator; import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/**
* Минимальный HTTP {@code GET /health} для оркестраторов (Docker/K8s). Порт из {@link
* BotConfig#getHealthHttpPort()} (0 = выкл).
*/
@Singleton @Singleton
public class HealthHttpServer implements Lifecycle { public class HealthHttpServer implements Lifecycle {
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class); private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
private final BotConfig config; private final BotConfig config;
private final MetricsCollector metrics; private final MetricsCollector metrics;
private final TempFileManager tempFileManager;
private HttpServer server; private HttpServer server;
@Inject @Inject
public HealthHttpServer(BotConfig config, MetricsCollector metrics) { public HealthHttpServer(
BotConfig config, MetricsCollector metrics, TempFileManager tempFileManager) {
this.config = config; this.config = config;
this.metrics = metrics; this.metrics = metrics;
this.tempFileManager = tempFileManager;
} }
public synchronized void start() { public synchronized void start() {
@ -46,6 +50,7 @@ public class HealthHttpServer implements Lifecycle {
try { try {
server = bindHealthServer(port); server = bindHealthServer(port);
server.createContext("/health", this::handleHealth); server.createContext("/health", this::handleHealth);
server.createContext("/d/", this::handleDownload);
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor()); server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
server.start(); server.start();
int bound = server.getAddress().getPort(); int bound = server.getAddress().getPort();
@ -67,7 +72,6 @@ public class HealthHttpServer implements Lifecycle {
} }
} }
/** Пробуем заданный порт; при {@link BindException} - системный свободный порт (0). */
private static HttpServer bindHealthServer(final int preferredPort) throws Exception { private static HttpServer bindHealthServer(final int preferredPort) throws Exception {
try { try {
return HttpServer.create(new InetSocketAddress(preferredPort), 0); return HttpServer.create(new InetSocketAddress(preferredPort), 0);
@ -107,6 +111,56 @@ public class HealthHttpServer implements Lifecycle {
} }
} }
private void handleDownload(final HttpExchange exchange) {
try {
if (!"GET".equalsIgnoreCase(exchange.getRequestMethod())) {
exchange.sendResponseHeaders(405, -1);
return;
}
String path = exchange.getRequestURI().getPath();
String key = path.substring("/d/".length());
if (key.isEmpty()) {
exchange.sendResponseHeaders(400, -1);
return;
}
TempFile entry = tempFileManager.getEntry(key);
if (entry == null) {
String body = "File not found or expired";
byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
exchange.getResponseHeaders().add("Content-Type", "text/plain; charset=utf-8");
exchange.sendResponseHeaders(404, bytes.length);
try (final OutputStream os = exchange.getResponseBody()) {
os.write(bytes);
}
return;
}
Path filePath = tempFileManager.getFilePath(entry);
if (!Files.exists(filePath)) {
tempFileManager.getEntry(key);
exchange.sendResponseHeaders(404, -1);
return;
}
long fileSize = Files.size(filePath);
exchange.getResponseHeaders().add("Content-Type", "video/mp4");
exchange
.getResponseHeaders()
.add("Content-Disposition", "attachment; filename=\"" + entry.filename() + "\"");
exchange.sendResponseHeaders(200, fileSize);
try (InputStream is = Files.newInputStream(filePath);
OutputStream os = exchange.getResponseBody()) {
is.transferTo(os);
}
} catch (Exception e) {
LoggerDecorator.error(logger, "Ошибка ответа /d/", e);
try {
exchange.sendResponseHeaders(500, -1);
} catch (Exception ignored) {
}
} finally {
exchange.close();
}
}
@Override @Override
public synchronized void shutdown() { public synchronized void shutdown() {
if (server != null) { if (server != null) {

View file

@ -35,7 +35,9 @@ public class ModerationLogger extends ListenerAdapter {
private void send(final Guild g, final EmbedBuilder eb) { private void send(final Guild g, final EmbedBuilder eb) {
if (eb == null) return; if (eb == null) return;
var ch = g.getTextChannelById(configRepo.get("moderation.log_channel_id")); String channelId = configRepo.get("moderation.log_channel_id");
if (channelId == null || channelId.isBlank()) return;
var ch = g.getTextChannelById(channelId);
if (ch == null) return; if (ch == null) return;
ch.sendMessageEmbeds(eb.setTimestamp(OffsetDateTime.now()).build()).queue(); ch.sendMessageEmbeds(eb.setTimestamp(OffsetDateTime.now()).build()).queue();
} }
@ -46,11 +48,16 @@ public class ModerationLogger extends ListenerAdapter {
public void onMessageDelete(MessageDeleteEvent e) { public void onMessageDelete(MessageDeleteEvent e) {
var content = messageCache.getContent(e.getMessageId()); var content = messageCache.getContent(e.getMessageId());
var g = e.getGuild(); var g = e.getGuild();
g.retrieveAuditLogs().type(ActionType.MESSAGE_DELETE).limit(5).queue( g.retrieveAuditLogs()
.type(ActionType.MESSAGE_DELETE)
.limit(5)
.queue(
logs -> { logs -> {
var entry = logs.stream() var entry =
logs.stream()
.filter(log -> e.getMessageId().equals(log.getTargetId())) .filter(log -> e.getMessageId().equals(log.getTargetId()))
.findFirst().orElse(null); .findFirst()
.orElse(null);
send(g, LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content)); send(g, LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content));
}); });
} }
@ -59,7 +66,10 @@ public class ModerationLogger extends ListenerAdapter {
public void onMessageBulkDelete(MessageBulkDeleteEvent e) { public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
var g = e.getGuild(); var g = e.getGuild();
var size = e.getMessageIds().size(); var size = e.getMessageIds().size();
g.retrieveAuditLogs().type(ActionType.MESSAGE_BULK_DELETE).limit(3).queue( g.retrieveAuditLogs()
.type(ActionType.MESSAGE_BULK_DELETE)
.limit(3)
.queue(
logs -> { logs -> {
var entry = logs.stream().findFirst().orElse(null); var entry = logs.stream().findFirst().orElse(null);
send(g, LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size)); send(g, LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size));
@ -77,10 +87,7 @@ public class ModerationLogger extends ListenerAdapter {
var entry = findEntry(l, ev.getUser().getId()); var entry = findEntry(l, ev.getUser().getId());
if (entry == null) return null; if (entry == null) return null;
return LogEmbeds.ban( return LogEmbeds.ban(
ev.getUser().getAsMention(), ev.getUser().getAsMention(), mod(entry), entry.getReason(), ev.getUser().getAvatarUrl());
mod(entry),
entry.getReason(),
ev.getUser().getAvatarUrl());
} }
@Override @Override
@ -91,10 +98,7 @@ public class ModerationLogger extends ListenerAdapter {
private EmbedBuilder unban(GuildUnbanEvent ev, List<AuditLogEntry> l) { private EmbedBuilder unban(GuildUnbanEvent ev, List<AuditLogEntry> l) {
var entry = findEntry(l, ev.getUser().getId()); var entry = findEntry(l, ev.getUser().getId());
if (entry == null) return null; if (entry == null) return null;
return LogEmbeds.unban( return LogEmbeds.unban(ev.getUser().getAsMention(), mod(entry), ev.getUser().getAvatarUrl());
ev.getUser().getAsMention(),
mod(entry),
ev.getUser().getAvatarUrl());
} }
// ── Kick ────────────────────────────────────────────────────────────────── // ── Kick ──────────────────────────────────────────────────────────────────
@ -108,10 +112,7 @@ public class ModerationLogger extends ListenerAdapter {
var entry = findEntry(l, ev.getUser().getId()); var entry = findEntry(l, ev.getUser().getId());
if (entry == null) return null; if (entry == null) return null;
return LogEmbeds.kick( return LogEmbeds.kick(
ev.getUser().getAsMention(), ev.getUser().getAsMention(), mod(entry), entry.getReason(), ev.getUser().getAvatarUrl());
mod(entry),
entry.getReason(),
ev.getUser().getAvatarUrl());
} }
// ── Timeout ──────────────────────────────────────────────────────────────── // ── Timeout ────────────────────────────────────────────────────────────────
@ -122,7 +123,8 @@ public class ModerationLogger extends ListenerAdapter {
handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> timeout(e, muted, l)); handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> timeout(e, muted, l));
} }
private EmbedBuilder timeout(GuildMemberUpdateTimeOutEvent e, boolean muted, List<AuditLogEntry> l) { private EmbedBuilder timeout(
GuildMemberUpdateTimeOutEvent e, boolean muted, List<AuditLogEntry> l) {
var entry = findEntry(l, e.getUser().getId()); var entry = findEntry(l, e.getUser().getId());
if (entry == null) return null; if (entry == null) return null;
return LogEmbeds.timeout( return LogEmbeds.timeout(
@ -157,7 +159,8 @@ public class ModerationLogger extends ListenerAdapter {
handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> voiceDeafen(e, deaf, l)); handle(e.getGuild(), ActionType.MEMBER_UPDATE, 5, l -> voiceDeafen(e, deaf, l));
} }
private EmbedBuilder voiceDeafen(GuildVoiceGuildDeafenEvent e, boolean deaf, List<AuditLogEntry> l) { private EmbedBuilder voiceDeafen(
GuildVoiceGuildDeafenEvent e, boolean deaf, List<AuditLogEntry> l) {
var member = e.getMember(); var member = e.getMember();
var entry = findEntry(l, member.getId(), "deaf"); var entry = findEntry(l, member.getId(), "deaf");
return LogEmbeds.voiceDeafen( return LogEmbeds.voiceDeafen(
@ -169,9 +172,9 @@ public class ModerationLogger extends ListenerAdapter {
// ── Helpers ──────────────────────────────────────────────────────────────── // ── Helpers ────────────────────────────────────────────────────────────────
private void handle(Guild g, ActionType t, int limit, Function<List<AuditLogEntry>, EmbedBuilder> fn) { private void handle(
g.retrieveAuditLogs().type(t).limit(limit).queue( Guild g, ActionType t, int limit, Function<List<AuditLogEntry>, EmbedBuilder> fn) {
logs -> send(g, fn.apply(logs))); g.retrieveAuditLogs().type(t).limit(limit).queue(logs -> send(g, fn.apply(logs)));
} }
private static AuditLogEntry findEntry(List<AuditLogEntry> l, String targetId, String... key) { private static AuditLogEntry findEntry(List<AuditLogEntry> l, String targetId, String... key) {

View file

@ -55,7 +55,7 @@ public class LavalinkManager {
.setName("main") .setName("main")
.setServerUri(lavalinkConfig.getWsUri()) .setServerUri(lavalinkConfig.getWsUri())
.setPassword(lavalinkConfig.getPassword()) .setPassword(lavalinkConfig.getPassword())
.setHttpTimeout(30000L) .setHttpTimeout(60000L)
.build()); .build());
registerListeners(); registerListeners();

View file

@ -0,0 +1,7 @@
package creator.loki.feature.tiktok;
public record TempFile(String key, String filename, long expiresAt) {
public boolean isExpired() {
return System.currentTimeMillis() > expiresAt;
}
}

View file

@ -0,0 +1,136 @@
package creator.loki.feature.tiktok;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.SecureRandom;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import creator.loki.util.LoggerDecorator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class TempFileManager {
private static final Logger logger = LoggerFactory.getLogger(TempFileManager.class);
private static final long TTL_MS = 24 * 60 * 60 * 1000L;
private static final String KEY_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789";
private static final int KEY_LENGTH = 15;
private static final SecureRandom RANDOM = new SecureRandom();
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
private final Path cacheDir;
private final ConcurrentHashMap<String, TempFile> entries = new ConcurrentHashMap<>();
public TempFileManager(String dataDir) {
this.cacheDir = Path.of(dataDir).resolve("tiktok-cache");
init();
}
private void init() {
try {
Files.createDirectories(cacheDir);
loadMeta();
cleanup();
LoggerDecorator.database(logger, "TempFileManager: " + entries.size() + " активных ссылок");
} catch (IOException e) {
LoggerDecorator.error(logger, "TempFileManager init error", e);
}
}
public synchronized String save(byte[] data, String originalName) throws IOException {
cleanup();
String key = generateKey();
String filename = key + ".mp4";
Files.write(cacheDir.resolve(filename), data);
TempFile entry = new TempFile(key, filename, System.currentTimeMillis() + TTL_MS);
entries.put(key, entry);
saveMeta();
LoggerDecorator.database(
logger,
"TempFileManager: saved " + filename + " (" + (data.length / 1024 / 1024) + "MB)");
return key;
}
public TempFile getEntry(String key) {
TempFile entry = entries.get(key);
if (entry == null) {
return null;
}
if (entry.isExpired()) {
deleteFile(entry);
entries.remove(key);
saveMeta();
return null;
}
Path filePath = cacheDir.resolve(entry.filename());
if (!Files.exists(filePath)) {
entries.remove(key);
saveMeta();
return null;
}
return entry;
}
public Path getFilePath(TempFile entry) {
return cacheDir.resolve(entry.filename());
}
public void cleanup() {
entries.entrySet().removeIf(e -> {
if (e.getValue().isExpired()) {
deleteFile(e.getValue());
return true;
}
return false;
});
}
private void deleteFile(TempFile entry) {
try {
Path file = cacheDir.resolve(entry.filename());
Files.deleteIfExists(file);
} catch (IOException e) {
LoggerDecorator.warn(logger, "TempFileManager: failed to delete " + entry.filename());
}
}
private String generateKey() {
StringBuilder sb = new StringBuilder(KEY_LENGTH);
for (int i = 0; i < KEY_LENGTH; i++) {
sb.append(KEY_CHARS.charAt(RANDOM.nextInt(KEY_CHARS.length())));
}
return sb.toString();
}
private void loadMeta() {
Path metaFile = cacheDir.resolve("cache-meta.json");
if (!Files.exists(metaFile)) {
return;
}
try {
String json = Files.readString(metaFile, StandardCharsets.UTF_8);
Map<String, TempFile> loaded =
GSON.fromJson(json, new TypeToken<Map<String, TempFile>>() {}.getType());
if (loaded != null) {
entries.putAll(loaded);
}
} catch (Exception e) {
LoggerDecorator.warn(logger, "TempFileManager: failed to load meta, starting fresh");
}
}
private synchronized void saveMeta() {
Path metaFile = cacheDir.resolve("cache-meta.json");
try {
String json = GSON.toJson(entries);
Files.writeString(metaFile, json, StandardCharsets.UTF_8);
} catch (IOException e) {
LoggerDecorator.warn(logger, "TempFileManager: failed to save meta");
}
}
}

View file

@ -25,10 +25,15 @@ public class TikTokCommand implements Command {
private final TikTokApi tikTokApi; private final TikTokApi tikTokApi;
private final OkHttpClient downloadClient; private final OkHttpClient downloadClient;
private final TempFileManager tempFileManager;
private final String externalUrl;
@Inject @Inject
public TikTokCommand(TikTokApi tikTokApi) { public TikTokCommand(
TikTokApi tikTokApi, TempFileManager tempFileManager, String externalUrl) {
this.tikTokApi = tikTokApi; this.tikTokApi = tikTokApi;
this.tempFileManager = tempFileManager;
this.externalUrl = externalUrl;
this.downloadClient = this.downloadClient =
new OkHttpClient.Builder() new OkHttpClient.Builder()
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)
@ -88,16 +93,9 @@ public class TikTokCommand implements Command {
byte[] videoBytes = downloadVideo(result.getVideoUrl()); byte[] videoBytes = downloadVideo(result.getVideoUrl());
if (videoBytes.length > MAX_FILE_SIZE) {
loadingMsg
.editMessageEmbeds(
EmbedTemplates.error("Ошибка", "Видео слишком большое (> 8MB)"))
.queue();
return;
}
String durationStr = formatDuration(result.getDuration()); String durationStr = formatDuration(result.getDuration());
if (videoBytes.length <= MAX_FILE_SIZE) {
MessageEmbed embed = MessageEmbed embed =
EmbedFactory.create() EmbedFactory.create()
.withTitle(truncate(result.getTitle(), 256)) .withTitle(truncate(result.getTitle(), 256))
@ -119,6 +117,33 @@ public class TikTokCommand implements Command {
EmbedTemplates.error( EmbedTemplates.error(
"Ошибка", "Не удалось отправить видео")) "Ошибка", "Не удалось отправить видео"))
.queue()); .queue());
} else {
String key = tempFileManager.save(videoBytes, "tiktok.mp4");
String downloadUrl = externalUrl + "/d/" + key;
double sizeMb = videoBytes.length / (1024.0 * 1024.0);
MessageEmbed embed =
EmbedFactory.create()
.withTitle(truncate(result.getTitle(), 256))
.withAuthor("@" + result.getAuthor())
.withColor(new Color(0, 0, 0))
.addField("Длительность", durationStr, true)
.addField(
"Размер",
String.format("%.1f MB", sizeMb),
true)
.addField(
"Скачать",
"[Нажми чтобы скачать](" + downloadUrl + ")",
false)
.withFooter("Ссылка действительна 24 часа")
.withThumbnail(result.getCover())
.build();
loadingMsg
.editMessageEmbeds(embed)
.queue();
}
} catch (TikTokApi.TikTokApiException e) { } catch (TikTokApi.TikTokApiException e) {
loadingMsg loadingMsg
@ -158,8 +183,8 @@ public class TikTokCommand implements Command {
} }
private String truncate(String str, final int maxLen) { private String truncate(String str, final int maxLen) {
if (str == null) { if (str == null || str.isBlank()) {
return ""; return "TikTok Video";
} }
return str.length() > maxLen ? str.substring(0, maxLen - 3) + "..." : str; return str.length() > maxLen ? str.substring(0, maxLen - 3) + "..." : str;
} }

View file

@ -1,17 +1,29 @@
package creator.loki.feature.tiktok; package creator.loki.feature.tiktok;
import java.nio.file.Path;
import com.google.inject.AbstractModule; import com.google.inject.AbstractModule;
import com.google.inject.Provides; import com.google.inject.Provides;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import creator.loki.config.BotConfig;
public class TikTokModule extends AbstractModule { public class TikTokModule extends AbstractModule {
@Override @Override
protected void configure() { protected void configure() {}
bind(TikTokApi.class).in(Singleton.class);
@Provides
@Singleton
TempFileManager provideTempFileManager(final BotConfig config) {
String dbPath = config.getDbPath();
String dataDir = Path.of(dbPath).getParent() != null
? Path.of(dbPath).getParent().toString()
: "data";
return new TempFileManager(dataDir);
} }
@Provides @Provides
TikTokCommand provideTikTokCommand(final TikTokApi tikTokApi) { TikTokCommand provideTikTokCommand(
return new TikTokCommand(tikTokApi); final TikTokApi tikTokApi, final TempFileManager tempFileManager, final BotConfig config) {
return new TikTokCommand(tikTokApi, tempFileManager, config.getExternalUrl());
} }
} }

View file

@ -27,7 +27,8 @@ class BotConfigTest {
30, 30,
24, 24,
null, null,
lavalink); lavalink,
"http://127.0.0.1:8712");
assertEquals("token123", config.getMainToken()); assertEquals("token123", config.getMainToken());
assertEquals("token123", config.getToken()); assertEquals("token123", config.getToken());
assertNull(config.getMusicToken()); assertNull(config.getMusicToken());
@ -46,7 +47,7 @@ class BotConfigTest {
BotConfig config = BotConfig config =
new BotConfig( new BotConfig(
null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null, null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null,
null); null, null);
assertNull(config.getMainToken()); assertNull(config.getMainToken());
assertNull(config.getMusicToken()); assertNull(config.getMusicToken());
assertNull(config.getRadioToken()); assertNull(config.getRadioToken());