Skip to content

Commit

Permalink
Post launcher changelog (#16)
Browse files Browse the repository at this point in the history
* Remove Reddit fetcher

* Add launcher news posting feature

* Some fixes
  • Loading branch information
apple502j authored Apr 26, 2024
1 parent 1b61e04 commit 28bd34a
Show file tree
Hide file tree
Showing 3 changed files with 250 additions and 327 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
/*
* Copyright (c) 2024 FabricMC
*
* 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 net.fabricmc.discord.bot.module.mcversion;

import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeParseException;
import java.util.Objects;

import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.javacord.api.entity.message.embed.EmbedBuilder;
import org.jetbrains.annotations.Nullable;

import net.fabricmc.discord.bot.util.HttpUtil;

final class LauncherNewsFetcher {
private static final String HOST = "launchercontent.mojang.com";
private static final String PATH = "/v2/javaPatchNotes.json";
private static final Logger LOGGER = LogManager.getLogger("mcversion/launcher");
private final McVersionModule mcVersionModule;
private Version latestRelease;
private Version latestSnapshot;
private long lastUpdateTimeMs = System.currentTimeMillis();
private ZonedDateTime lastAnnounceTime = ZonedDateTime.now(ZoneId.of("UTC"));

LauncherNewsFetcher(McVersionModule module) {
this.mcVersionModule = module;
}

Version getLatestRelease() {
return latestRelease;
}

Version getLatestSnapshot() {
return latestSnapshot;
}

long getLastUpdateTimeMs() {
return lastUpdateTimeMs;
}

void update() throws IOException, URISyntaxException, InterruptedException, DateTimeParseException {
fetchLatest();
Version latest = latestRelease.date.isAfter(latestSnapshot.date) ? latestRelease : latestSnapshot;

if (latest.date.isAfter(lastAnnounceTime) && !McVersionModule.isOldVersion(latest.name) && announce(latest)) {
lastAnnounceTime = latest.date;
}
}

void fetchLatest() throws IOException, URISyntaxException, InterruptedException, DateTimeParseException {
HttpResponse<InputStream> response = HttpUtil.makeRequest(HttpUtil.toUri(HOST, PATH));

if (response.statusCode() != 200) {
LOGGER.warn("Request failed: {}", response.statusCode());
response.body().close();
return;
}

try (JsonReader reader = new JsonReader(new InputStreamReader(response.body(), StandardCharsets.UTF_8))) {
reader.beginObject();

while (reader.hasNext()) {
if (reader.nextName().equals("entries")) {
reader.beginArray();

// Note: the first item is often not the latest!
while (reader.peek() == JsonToken.BEGIN_OBJECT) {
reader.beginObject();

String type = null;
String title = null;
String version = null;
String date = null;
String imagePath = null;
String shortText = null;
boolean readSnapshot = false;
boolean readRelease = false;

ZonedDateTime releaseDateTime = null;
boolean skipTheRest = false;

while (reader.peek() == JsonToken.NAME) {
if (skipTheRest) {
reader.nextName();
reader.skipValue();
continue;
}

switch (reader.nextName()) {
case "title" -> {
title = reader.nextString();
}
case "version" -> {
version = reader.nextString();
}
case "date" -> {
date = reader.nextString();
releaseDateTime = ZonedDateTime.parse(date);

if (
(readSnapshot && latestSnapshot != null && releaseDateTime.isBefore(latestSnapshot.date)) ||
(readRelease && latestRelease != null && releaseDateTime.isBefore(latestRelease.date))) {
skipTheRest = true;
}
}
case "image" -> {
reader.beginObject();
while (reader.peek() == JsonToken.NAME) {
if (reader.nextName().equals("url")) {
imagePath = reader.nextString();
} else {
reader.skipValue();
}
}
reader.endObject();
}
case "shortText" -> {
shortText = reader.nextString();
}
case "type" -> {
type = reader.nextString();
readSnapshot = type.equals("snapshot");
readRelease = type.equals("release");

if (
releaseDateTime != null &&
(
(readSnapshot && latestSnapshot != null && releaseDateTime.isBefore(latestSnapshot.date)) ||
(readRelease && latestRelease != null && releaseDateTime.isBefore(latestRelease.date)))
) {
skipTheRest = true;
}
}
default -> reader.skipValue();
}
}

reader.endObject();

if (skipTheRest || version == null || date == null || !(readRelease || readSnapshot)) {
continue;
}

URI imageUri = null;

if (imagePath != null) {
try {
imageUri = HttpUtil.toUri(HOST, imagePath);
} catch (URISyntaxException ignored) {
}
}

Version latest = new Version(
type,
version,
Objects.requireNonNull(title, "Minecraft %s".formatted(version)),
imageUri,
Objects.requireNonNull(shortText, "A new update was released!"),
releaseDateTime
);

if (readRelease) {
latestRelease = latest;
} else if (readSnapshot) {
latestSnapshot = latest;
}

lastUpdateTimeMs = System.currentTimeMillis();
}

reader.endArray();
} else {
reader.skipValue();
}
}
}
}

private boolean announce(Version version) {
// Note: at this point URL is not available (yet).
// However, this is *very* quick from my observation.
LOGGER.info("Announcing MCLauncher-News {} version: {}", version.type, version.name);
return this.mcVersionModule.sendAnnouncement(mcVersionModule.getUpdateChannel(), version.toEmbed());
}

record Version(String type, String name, String title, @Nullable URI image, String shortText, ZonedDateTime date) {
EmbedBuilder toEmbed() {
EmbedBuilder builder = new EmbedBuilder();
builder.setTitle(title);
builder.setDescription(shortText + "...");
if (image != null) builder.setThumbnail(image.toString());
builder.setTimestamp(date.toInstant());
return builder;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
import org.javacord.api.DiscordApi;
import org.javacord.api.entity.channel.TextChannel;
import org.javacord.api.entity.message.Message;
import org.javacord.api.entity.message.embed.EmbedBuilder;
import org.javacord.api.entity.server.Server;

import net.fabricmc.discord.bot.CachedMessage;
Expand Down Expand Up @@ -62,6 +63,7 @@
public final class McVersionModule implements Module {
private static final int UPDATE_DELAY = 30; // in s
private static final int NEWS_UPDATE_CYCLES = 4; // check news every n updates
private static final int LAUNCHER_NEWS_UPDATE_CYCLES = 2; // check launcher news every n updates

private static final String MESSAGE = "A new Minecraft %s is out: %s"; // args: kind, version
static final String KIND_RELEASE = "release";
Expand Down Expand Up @@ -95,7 +97,7 @@ public final class McVersionModule implements Module {

final MetaFetcher metaFetcher = new MetaFetcher(this);
final NewsFetcher newsFetcher = new NewsFetcher(this);
private final RedditFetcher redditFetcher = new RedditFetcher(this);
final LauncherNewsFetcher launcherNewsFetcher = new LauncherNewsFetcher(this);

@Override
public String getName() {
Expand All @@ -111,7 +113,6 @@ public void registerConfigEntries(DiscordBot bot) {
bot.registerConfigEntry(ANNOUNCED_PENDING_VERSION, () -> "0");

newsFetcher.register(bot);
redditFetcher.register(bot);
}

@Override
Expand Down Expand Up @@ -194,15 +195,15 @@ private void update() {
updateSpecific(KIND_RELEASE, metaFetcher.getLatestRelease(), ANNOUNCED_RELEASE_VERSION);
updateSpecific(KIND_SNAPSHOT, metaFetcher.getLatestSnapshot(), ANNOUNCED_SNAPSHOT_VERSION, ANNOUNCED_RELEASE_VERSION);

redditFetcher.update();
updateSpecific(KIND_RELEASE, redditFetcher.getLatestRelease(), ANNOUNCED_RELEASE_VERSION);
updateSpecific(KIND_SNAPSHOT, redditFetcher.getLatestSnapshot(), ANNOUNCED_SNAPSHOT_VERSION);
updateSpecific(KIND_PENDING, redditFetcher.getLatestPending(), ANNOUNCED_PENDING_VERSION);

if (updateChannel != null
&& newsCycleCouter++ % NEWS_UPDATE_CYCLES == 0) {
newsFetcher.update();
}

if (updateChannel != null
&& newsCycleCouter % LAUNCHER_NEWS_UPDATE_CYCLES == 0) {
launcherNewsFetcher.update();
}
} catch (Throwable t) {
HttpUtil.logError("mc version check failed", t, LOGGER);
}
Expand Down Expand Up @@ -287,4 +288,25 @@ boolean sendAnnouncement(TextChannel channel, String msg) {

return true;
}

boolean sendAnnouncement(TextChannel channel, EmbedBuilder embed) {
if (channel == null) return false;

Message message;

try {
message = channel.sendMessage(embed).join();
} catch (Throwable t) {
LOGGER.warn("Announcement failed", t);
return false;
}

message.crossPost()
.exceptionally(exc -> {
LOGGER.warn("Message crossposting failed: "+exc); // fails with MissingPermissionsException for non-news channel
return null;
});

return true;
}
}
Loading

0 comments on commit 28bd34a

Please sign in to comment.