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:
parent
43735de681
commit
ce1d863009
11 changed files with 334 additions and 76 deletions
|
|
@ -18,6 +18,7 @@ public class BotConfig {
|
|||
private final int aiDossierUpdateHours;
|
||||
private final String aiDebateChannelId;
|
||||
private final LavalinkConfig lavalinkConfig;
|
||||
private final String externalUrl;
|
||||
|
||||
public BotConfig(
|
||||
String mainToken,
|
||||
|
|
@ -36,7 +37,8 @@ public class BotConfig {
|
|||
int aiDossierMsgs,
|
||||
int aiDossierUpdateHours,
|
||||
String aiDebateChannelId,
|
||||
LavalinkConfig lavalinkConfig) {
|
||||
LavalinkConfig lavalinkConfig,
|
||||
String externalUrl) {
|
||||
this.mainToken = mainToken;
|
||||
this.musicToken = musicToken;
|
||||
this.radioToken = radioToken;
|
||||
|
|
@ -54,6 +56,7 @@ public class BotConfig {
|
|||
this.aiDossierUpdateHours = aiDossierUpdateHours;
|
||||
this.aiDebateChannelId = aiDebateChannelId;
|
||||
this.lavalinkConfig = lavalinkConfig;
|
||||
this.externalUrl = externalUrl;
|
||||
}
|
||||
|
||||
public String getMainToken() {
|
||||
|
|
@ -127,4 +130,8 @@ public class BotConfig {
|
|||
public LavalinkConfig getLavalinkConfig() {
|
||||
return lavalinkConfig;
|
||||
}
|
||||
|
||||
public String getExternalUrl() {
|
||||
return externalUrl;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ public final class ConfigLoader {
|
|||
String aiDebateChannelId = dotenv.get("AI_DEBATE_CHANNEL_ID");
|
||||
|
||||
LavalinkConfig lavalinkConfig = getLavalinkConfig();
|
||||
|
||||
String externalUrl = getEnv(dotenv, "EXTERNAL_URL", "http://127.0.0.1:8712");
|
||||
|
||||
LoggerDecorator.success(logger, "Конфигурация загружена успешно");
|
||||
return new BotConfig(
|
||||
mainToken,
|
||||
|
|
@ -104,6 +107,7 @@ public final class ConfigLoader {
|
|||
aiDossierMsgs,
|
||||
aiDossierUpdateHours,
|
||||
aiDebateChannelId,
|
||||
lavalinkConfig);
|
||||
lavalinkConfig,
|
||||
externalUrl);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ public class BotInitializer {
|
|||
JDA musicJda = createMusicJDA();
|
||||
JDA radioJda = null;
|
||||
if (config.getRadioToken() != null && !config.getRadioToken().isBlank()) {
|
||||
sleep(3000);
|
||||
LavalinkManager radioLavalink =
|
||||
LavalinkManagerFactory.create(config, queueRepository, config.getRadioToken());
|
||||
radioJda = createRadioJDA(radioLavalink);
|
||||
|
|
@ -144,4 +145,12 @@ public class BotInitializer {
|
|||
}
|
||||
return JdaFactory.awaitJda(builder.build());
|
||||
}
|
||||
|
||||
private static void sleep(long ms) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
package creator.loki.feature.metrics;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.BindException;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
import com.google.inject.Inject;
|
||||
|
|
@ -12,26 +15,27 @@ import com.sun.net.httpserver.HttpExchange;
|
|||
import com.sun.net.httpserver.HttpServer;
|
||||
import creator.loki.config.BotConfig;
|
||||
import creator.loki.core.Lifecycle;
|
||||
import creator.loki.feature.tiktok.TempFile;
|
||||
import creator.loki.feature.tiktok.TempFileManager;
|
||||
import creator.loki.util.LoggerDecorator;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
/**
|
||||
* Минимальный HTTP {@code GET /health} для оркестраторов (Docker/K8s). Порт из {@link
|
||||
* BotConfig#getHealthHttpPort()} (0 = выкл).
|
||||
*/
|
||||
@Singleton
|
||||
public class HealthHttpServer implements Lifecycle {
|
||||
private static final Logger logger = LoggerFactory.getLogger(HealthHttpServer.class);
|
||||
|
||||
private final BotConfig config;
|
||||
private final MetricsCollector metrics;
|
||||
private final TempFileManager tempFileManager;
|
||||
private HttpServer server;
|
||||
|
||||
@Inject
|
||||
public HealthHttpServer(BotConfig config, MetricsCollector metrics) {
|
||||
public HealthHttpServer(
|
||||
BotConfig config, MetricsCollector metrics, TempFileManager tempFileManager) {
|
||||
this.config = config;
|
||||
this.metrics = metrics;
|
||||
this.tempFileManager = tempFileManager;
|
||||
}
|
||||
|
||||
public synchronized void start() {
|
||||
|
|
@ -46,6 +50,7 @@ public class HealthHttpServer implements Lifecycle {
|
|||
try {
|
||||
server = bindHealthServer(port);
|
||||
server.createContext("/health", this::handleHealth);
|
||||
server.createContext("/d/", this::handleDownload);
|
||||
server.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
|
||||
server.start();
|
||||
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 {
|
||||
try {
|
||||
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
|
||||
public synchronized void shutdown() {
|
||||
if (server != null) {
|
||||
|
|
|
|||
|
|
@ -35,7 +35,9 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
|
||||
private void send(final Guild g, final EmbedBuilder eb) {
|
||||
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;
|
||||
ch.sendMessageEmbeds(eb.setTimestamp(OffsetDateTime.now()).build()).queue();
|
||||
}
|
||||
|
|
@ -46,24 +48,32 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
public void onMessageDelete(MessageDeleteEvent e) {
|
||||
var content = messageCache.getContent(e.getMessageId());
|
||||
var g = e.getGuild();
|
||||
g.retrieveAuditLogs().type(ActionType.MESSAGE_DELETE).limit(5).queue(
|
||||
logs -> {
|
||||
var entry = logs.stream()
|
||||
.filter(log -> e.getMessageId().equals(log.getTargetId()))
|
||||
.findFirst().orElse(null);
|
||||
send(g, LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content));
|
||||
});
|
||||
g.retrieveAuditLogs()
|
||||
.type(ActionType.MESSAGE_DELETE)
|
||||
.limit(5)
|
||||
.queue(
|
||||
logs -> {
|
||||
var entry =
|
||||
logs.stream()
|
||||
.filter(log -> e.getMessageId().equals(log.getTargetId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
send(g, LogEmbeds.messageDelete(entry, e.getChannel().getAsMention(), content));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageBulkDelete(MessageBulkDeleteEvent e) {
|
||||
var g = e.getGuild();
|
||||
var size = e.getMessageIds().size();
|
||||
g.retrieveAuditLogs().type(ActionType.MESSAGE_BULK_DELETE).limit(3).queue(
|
||||
logs -> {
|
||||
var entry = logs.stream().findFirst().orElse(null);
|
||||
send(g, LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size));
|
||||
});
|
||||
g.retrieveAuditLogs()
|
||||
.type(ActionType.MESSAGE_BULK_DELETE)
|
||||
.limit(3)
|
||||
.queue(
|
||||
logs -> {
|
||||
var entry = logs.stream().findFirst().orElse(null);
|
||||
send(g, LogEmbeds.messageBulkDelete(entry, e.getChannel().getAsMention(), size));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Ban / Unban ────────────────────────────────────────────────────────────
|
||||
|
|
@ -77,10 +87,7 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
var entry = findEntry(l, ev.getUser().getId());
|
||||
if (entry == null) return null;
|
||||
return LogEmbeds.ban(
|
||||
ev.getUser().getAsMention(),
|
||||
mod(entry),
|
||||
entry.getReason(),
|
||||
ev.getUser().getAvatarUrl());
|
||||
ev.getUser().getAsMention(), mod(entry), entry.getReason(), ev.getUser().getAvatarUrl());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -91,10 +98,7 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
private EmbedBuilder unban(GuildUnbanEvent ev, List<AuditLogEntry> l) {
|
||||
var entry = findEntry(l, ev.getUser().getId());
|
||||
if (entry == null) return null;
|
||||
return LogEmbeds.unban(
|
||||
ev.getUser().getAsMention(),
|
||||
mod(entry),
|
||||
ev.getUser().getAvatarUrl());
|
||||
return LogEmbeds.unban(ev.getUser().getAsMention(), mod(entry), ev.getUser().getAvatarUrl());
|
||||
}
|
||||
|
||||
// ── Kick ──────────────────────────────────────────────────────────────────
|
||||
|
|
@ -108,10 +112,7 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
var entry = findEntry(l, ev.getUser().getId());
|
||||
if (entry == null) return null;
|
||||
return LogEmbeds.kick(
|
||||
ev.getUser().getAsMention(),
|
||||
mod(entry),
|
||||
entry.getReason(),
|
||||
ev.getUser().getAvatarUrl());
|
||||
ev.getUser().getAsMention(), mod(entry), entry.getReason(), ev.getUser().getAvatarUrl());
|
||||
}
|
||||
|
||||
// ── Timeout ────────────────────────────────────────────────────────────────
|
||||
|
|
@ -122,7 +123,8 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
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());
|
||||
if (entry == null) return null;
|
||||
return LogEmbeds.timeout(
|
||||
|
|
@ -157,7 +159,8 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
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 entry = findEntry(l, member.getId(), "deaf");
|
||||
return LogEmbeds.voiceDeafen(
|
||||
|
|
@ -169,9 +172,9 @@ public class ModerationLogger extends ListenerAdapter {
|
|||
|
||||
// ── Helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
private void handle(Guild g, ActionType t, int limit, Function<List<AuditLogEntry>, EmbedBuilder> fn) {
|
||||
g.retrieveAuditLogs().type(t).limit(limit).queue(
|
||||
logs -> send(g, fn.apply(logs)));
|
||||
private void handle(
|
||||
Guild g, ActionType t, int limit, Function<List<AuditLogEntry>, EmbedBuilder> fn) {
|
||||
g.retrieveAuditLogs().type(t).limit(limit).queue(logs -> send(g, fn.apply(logs)));
|
||||
}
|
||||
|
||||
private static AuditLogEntry findEntry(List<AuditLogEntry> l, String targetId, String... key) {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ public class LavalinkManager {
|
|||
.setName("main")
|
||||
.setServerUri(lavalinkConfig.getWsUri())
|
||||
.setPassword(lavalinkConfig.getPassword())
|
||||
.setHttpTimeout(30000L)
|
||||
.setHttpTimeout(60000L)
|
||||
.build());
|
||||
|
||||
registerListeners();
|
||||
|
|
|
|||
7
src/main/java/creator/loki/feature/tiktok/TempFile.java
Normal file
7
src/main/java/creator/loki/feature/tiktok/TempFile.java
Normal 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;
|
||||
}
|
||||
}
|
||||
136
src/main/java/creator/loki/feature/tiktok/TempFileManager.java
Normal file
136
src/main/java/creator/loki/feature/tiktok/TempFileManager.java
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -25,10 +25,15 @@ public class TikTokCommand implements Command {
|
|||
|
||||
private final TikTokApi tikTokApi;
|
||||
private final OkHttpClient downloadClient;
|
||||
private final TempFileManager tempFileManager;
|
||||
private final String externalUrl;
|
||||
|
||||
@Inject
|
||||
public TikTokCommand(TikTokApi tikTokApi) {
|
||||
public TikTokCommand(
|
||||
TikTokApi tikTokApi, TempFileManager tempFileManager, String externalUrl) {
|
||||
this.tikTokApi = tikTokApi;
|
||||
this.tempFileManager = tempFileManager;
|
||||
this.externalUrl = externalUrl;
|
||||
this.downloadClient =
|
||||
new OkHttpClient.Builder()
|
||||
.connectTimeout(15, TimeUnit.SECONDS)
|
||||
|
|
@ -88,37 +93,57 @@ public class TikTokCommand implements Command {
|
|||
|
||||
byte[] videoBytes = downloadVideo(result.getVideoUrl());
|
||||
|
||||
if (videoBytes.length > MAX_FILE_SIZE) {
|
||||
loadingMsg
|
||||
.editMessageEmbeds(
|
||||
EmbedTemplates.error("Ошибка", "Видео слишком большое (> 8MB)"))
|
||||
.queue();
|
||||
return;
|
||||
}
|
||||
|
||||
String durationStr = formatDuration(result.getDuration());
|
||||
|
||||
MessageEmbed embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle(truncate(result.getTitle(), 256))
|
||||
.withAuthor("@" + result.getAuthor())
|
||||
.withColor(new Color(0, 0, 0))
|
||||
.addField("Длительность", durationStr, true)
|
||||
.withThumbnail(result.getCover())
|
||||
.build();
|
||||
if (videoBytes.length <= MAX_FILE_SIZE) {
|
||||
MessageEmbed embed =
|
||||
EmbedFactory.create()
|
||||
.withTitle(truncate(result.getTitle(), 256))
|
||||
.withAuthor("@" + result.getAuthor())
|
||||
.withColor(new Color(0, 0, 0))
|
||||
.addField("Длительность", durationStr, true)
|
||||
.withThumbnail(result.getCover())
|
||||
.build();
|
||||
|
||||
context
|
||||
.getChannel()
|
||||
.sendFiles(FileUpload.fromData(videoBytes, "tiktok.mp4"))
|
||||
.setEmbeds(embed)
|
||||
.queue(
|
||||
success -> loadingMsg.delete().queue(),
|
||||
failure ->
|
||||
loadingMsg
|
||||
.editMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка", "Не удалось отправить видео"))
|
||||
.queue());
|
||||
context
|
||||
.getChannel()
|
||||
.sendFiles(FileUpload.fromData(videoBytes, "tiktok.mp4"))
|
||||
.setEmbeds(embed)
|
||||
.queue(
|
||||
success -> loadingMsg.delete().queue(),
|
||||
failure ->
|
||||
loadingMsg
|
||||
.editMessageEmbeds(
|
||||
EmbedTemplates.error(
|
||||
"Ошибка", "Не удалось отправить видео"))
|
||||
.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) {
|
||||
loadingMsg
|
||||
|
|
@ -158,8 +183,8 @@ public class TikTokCommand implements Command {
|
|||
}
|
||||
|
||||
private String truncate(String str, final int maxLen) {
|
||||
if (str == null) {
|
||||
return "";
|
||||
if (str == null || str.isBlank()) {
|
||||
return "TikTok Video";
|
||||
}
|
||||
return str.length() > maxLen ? str.substring(0, maxLen - 3) + "..." : str;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,17 +1,29 @@
|
|||
package creator.loki.feature.tiktok;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
import com.google.inject.AbstractModule;
|
||||
import com.google.inject.Provides;
|
||||
import com.google.inject.Singleton;
|
||||
import creator.loki.config.BotConfig;
|
||||
|
||||
public class TikTokModule extends AbstractModule {
|
||||
@Override
|
||||
protected void configure() {
|
||||
bind(TikTokApi.class).in(Singleton.class);
|
||||
protected void configure() {}
|
||||
|
||||
@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
|
||||
TikTokCommand provideTikTokCommand(final TikTokApi tikTokApi) {
|
||||
return new TikTokCommand(tikTokApi);
|
||||
TikTokCommand provideTikTokCommand(
|
||||
final TikTokApi tikTokApi, final TempFileManager tempFileManager, final BotConfig config) {
|
||||
return new TikTokCommand(tikTokApi, tempFileManager, config.getExternalUrl());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,8 @@ class BotConfigTest {
|
|||
30,
|
||||
24,
|
||||
null,
|
||||
lavalink);
|
||||
lavalink,
|
||||
"http://127.0.0.1:8712");
|
||||
assertEquals("token123", config.getMainToken());
|
||||
assertEquals("token123", config.getToken());
|
||||
assertNull(config.getMusicToken());
|
||||
|
|
@ -46,7 +47,7 @@ class BotConfigTest {
|
|||
BotConfig config =
|
||||
new BotConfig(
|
||||
null, null, null, null, null, 0, null, null, null, null, 10, 300, 3000, 30, 24, null,
|
||||
null);
|
||||
null, null);
|
||||
assertNull(config.getMainToken());
|
||||
assertNull(config.getMusicToken());
|
||||
assertNull(config.getRadioToken());
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue