Skip to content

Commit

Permalink
Discord integration and push tag command
Browse files Browse the repository at this point in the history
  • Loading branch information
Matyrobbrt committed Jan 12, 2025
1 parent dc7671c commit c8bff53
Show file tree
Hide file tree
Showing 15 changed files with 551 additions and 46 deletions.
11 changes: 11 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ repositories {
maven {
url = 'https://libraries.minecraft.net'
}
maven {
url = 'https://m2.chew.pro/releases'
}
}

java.toolchain {
Expand Down Expand Up @@ -43,6 +46,14 @@ dependencies {
implementation "com.apollographql.apollo:apollo-api:$apollo" // Apollo (GraphQL)

implementation 'org.eclipse.jgit:org.eclipse.jgit:6.10.0.202406032230-r'

implementation('org.jdbi:jdbi3-core:3.32.0')
implementation('org.jdbi:jdbi3-sqlobject:3.32.0')
implementation('org.xerial:sqlite-jdbc:3.40.0.0')
implementation('org.flywaydb:flyway-core:8.5.13')

implementation 'net.dv8tion:JDA:5.2.2'
implementation 'pw.chew:jda-chewtils:2.0'
}

apollo {
Expand Down
16 changes: 15 additions & 1 deletion src/main/java/net/neoforged/automation/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import com.mojang.brigadier.CommandDispatcher;
import io.javalin.Javalin;
import net.neoforged.automation.command.Commands;
import net.neoforged.automation.db.Database;
import net.neoforged.automation.discord.DiscordBot;
import net.neoforged.automation.util.AuthUtil;
import net.neoforged.automation.util.GHAction;
import net.neoforged.automation.webhook.handler.AutomaticLabelHandler;
Expand All @@ -21,17 +23,24 @@
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.http.HttpClient;
import java.nio.file.Path;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;

public class Main {
private static final String NEOFORGE = "neoforged/NeoForge";
public static final Logger LOGGER = LoggerFactory.getLogger("Reactionable");
public static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
public static final ScheduledExecutorService EXECUTOR = Executors.newScheduledThreadPool(2, Thread.ofPlatform().name("scheduled-", 0).factory());

public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
public static void main(String[] args) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InterruptedException {
var startupConfig = StartupConfiguration.load(Path.of("config.properties"));

Database.init();

var gitHub = new GitHubBuilder()
.withAuthorizationProvider(AuthUtil.githubApp(
startupConfig.get("gitHubAppId", ""),
Expand All @@ -51,6 +60,11 @@ public static void main(String[] args) throws IOException, NoSuchAlgorithmExcept
.start(startupConfig.getInt("port", 8080));

LOGGER.warn("Started up! Logged as {} on GitHub", GitHubAccessor.getApp(gitHub).getSlug());

var discordToken = startupConfig.get("discordToken", "");
if (!discordToken.isBlank()) {
DiscordBot.create(discordToken, gitHub, startupConfig, app);
}
}

public static WebhookHandler setupWebhookHandlers(StartupConfiguration startupConfig, WebhookHandler handler, Configuration.RepoLocation location) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import net.neoforged.automation.Configuration;
import net.neoforged.automation.runner.PRActionRunner;
import net.neoforged.automation.runner.PRRunUtils;
import net.neoforged.automation.runner.GitRunner;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;
import org.kohsuke.github.GHPullRequest;
Expand Down Expand Up @@ -33,7 +33,7 @@ public static void run(GitHub gh, GHPullRequest pr, Configuration.PRActions acti
.command(baseCommand, command2)
.onFailed((gitHub, run) -> onFailure.accept(run))
.onFinished((gitHub, run, artifacts) -> {
PRRunUtils.setupPR(pr, (dir, git) -> {
GitRunner.setupPR(gh, pr, (dir, git, creds) -> {
List<String> deleted = new ArrayList<>();
try (var file = new ZipFile(artifacts.get("status").toFile())) {
var entry = file.entries().nextElement();
Expand Down Expand Up @@ -69,15 +69,8 @@ public static void run(GitHub gh, GHPullRequest pr, Configuration.PRActions acti

git.add().addFilepattern(".").call();

var botName = GitHubAccessor.getApp(gitHub).getSlug() + "[bot]";
var user = gitHub.getUser(botName);
var creds = new UsernamePasswordCredentialsProvider(
botName,
GitHubAccessor.getToken(gitHub)
);

git.commit().setCredentialsProvider(creds)
.setCommitter(botName, user.getId() + "+" + botName + "@users.noreply.github.com")
.setCommitter(creds.getPerson())
.setMessage("Update formatting")
.setSign(false)
.setNoVerify(true)
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/net/neoforged/automation/db/Database.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package net.neoforged.automation.db;

import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.configuration.FluentConfiguration;
import org.jdbi.v3.core.Jdbi;
import org.jdbi.v3.core.extension.ExtensionCallback;
import org.jdbi.v3.core.extension.ExtensionConsumer;
import org.jdbi.v3.sqlobject.SqlObjectPlugin;
import org.sqlite.SQLiteDataSource;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.function.UnaryOperator;

public class Database {

private static Jdbi database;

public static Jdbi get() {
return database;
}

public static <R, E, X extends Exception> R withExtension(Class<E> extensionType, ExtensionCallback<R, E, X> callback) throws X {
return get().withExtension(extensionType, callback);
}

public static <E, X extends Exception> void useExtension(Class<E> extensionType, ExtensionConsumer<E, X> callback) throws X {
get().useExtension(extensionType, callback);
}

public static void init() {
var path = Path.of("data.db");
database = createDatabaseConnection(path, "reactionable", c -> c.locations("classpath:db"));
}

private static Jdbi createDatabaseConnection(Path dbPath, String name, UnaryOperator<FluentConfiguration> flywayConfig) {
dbPath = dbPath.toAbsolutePath();
if (!Files.exists(dbPath)) {
try {
Files.createDirectories(dbPath.getParent());
Files.createFile(dbPath);
} catch (IOException e) {
throw new RuntimeException("Exception creating database!", e);
}
}
final String url = "jdbc:sqlite:" + dbPath;
final SQLiteDataSource dataSource = new SQLiteDataSource();
dataSource.setUrl(url);
dataSource.setEncoding("UTF-8");
dataSource.setDatabaseName(name);
dataSource.setEnforceForeignKeys(true);
dataSource.setCaseSensitiveLike(false);

final var flyway = flywayConfig.apply(Flyway.configure().dataSource(dataSource)).load();
flyway.migrate();

return Jdbi.create(dataSource)
.installPlugin(new SqlObjectPlugin());
}
}
16 changes: 16 additions & 0 deletions src/main/java/net/neoforged/automation/db/DiscordUsersDAO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package net.neoforged.automation.db;

import org.jdbi.v3.sqlobject.customizer.Bind;
import org.jdbi.v3.sqlobject.statement.SqlQuery;
import org.jdbi.v3.sqlobject.statement.SqlUpdate;
import org.jdbi.v3.sqlobject.transaction.Transactional;
import org.jetbrains.annotations.Nullable;

public interface DiscordUsersDAO extends Transactional<DiscordUsersDAO> {
@Nullable
@SqlQuery("select github from discord_users where user = :user")
String getUser(@Bind("user") long id);

@SqlUpdate("insert into discord_users (user, github) values (:user, :github)")
void linkUser(@Bind("user") long id, @Bind("github") String githubName);
}
104 changes: 104 additions & 0 deletions src/main/java/net/neoforged/automation/discord/DiscordBot.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package net.neoforged.automation.discord;

import com.jagrosh.jdautilities.command.CommandClient;
import com.jagrosh.jdautilities.command.CommandClientBuilder;
import io.javalin.Javalin;
import net.dv8tion.jda.api.JDABuilder;
import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
import net.dv8tion.jda.api.interactions.commands.Command;
import net.dv8tion.jda.api.interactions.commands.OptionMapping;
import net.neoforged.automation.Main;
import net.neoforged.automation.StartupConfiguration;
import net.neoforged.automation.discord.command.GitHubCommand;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Predicate;

public class DiscordBot {
public static final Logger LOGGER = LoggerFactory.getLogger(DiscordBot.class);

private static volatile List<String> knownRepositories = List.of();

public static void create(String token, GitHub gitHub, StartupConfiguration configuration, Javalin app) throws InterruptedException {
var cfg = new OauthConfig(configuration.get("githubOauthClientId", ""), configuration.get("githubOauthClientSecret", ""),
configuration.get("serverUrl", "") + "/oauth2/discord/github");

var jda = JDABuilder.createLight(token)
.addEventListeners(createClient(gitHub, cfg))
.build()
.awaitReady();

app.get("/oauth2/discord/github", new GitHubOauthLinkHandler(cfg));

LOGGER.info("Discord bot started. Logged in as {}", jda.getSelfUser().getEffectiveName());

Main.EXECUTOR.scheduleAtFixedRate(() -> updateRepos(gitHub), 0, 1, TimeUnit.HOURS);
}

private static CommandClient createClient(GitHub gitHub, OauthConfig config) {
var builder = new CommandClientBuilder();
builder.setOwnerId("0");
builder.addSlashCommand(new GHLinkCommand(config.clientId, config.redirectUrl));
builder.addSlashCommand(new GitHubCommand(gitHub));
return builder.build();
}

private static void updateRepos(GitHub gitHub) {
try {
synchronized (DiscordBot.class) {
knownRepositories = gitHub.getInstallation().listRepositories().toList()
.stream()
.filter(r -> !r.isArchived() && !r.isPrivate())
.map(GHRepository::getFullName)
.sorted()
.toList();
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}

public static void suggestRepositories(CommandAutoCompleteInteractionEvent event) {
Predicate<String> filter = filterContainsCurrent(event);
event.replyChoices(knownRepositories.stream().filter(filter)
.limit(25)
.map(o -> new Command.Choice(o, o))
.toList())
.queue();
}

public static Consumer<CommandAutoCompleteInteractionEvent> suggestBranches(GitHub gitHub) {
return event -> {
var repoName = event.getOption("repo", OptionMapping::getAsString);
if (repoName == null || repoName.isBlank()) return;

try {
var repo = gitHub.getRepository(repoName);

event.replyChoices(repo.getBranches().keySet()
.stream().filter(filterContainsCurrent(event))
.limit(25)
.map(o -> new Command.Choice(o, o))
.toList()).queue();
} catch (IOException e) {
throw new RuntimeException(e);
}
};
}

private static Predicate<String> filterContainsCurrent(CommandAutoCompleteInteractionEvent event) {
var current = event.getFocusedOption().getValue().toLowerCase(Locale.ROOT);
return current.isBlank() ? e -> true : e -> e.toLowerCase(Locale.ROOT).contains(current);
}

record OauthConfig(String clientId, String clientSecret, String redirectUrl) {
}
}
27 changes: 27 additions & 0 deletions src/main/java/net/neoforged/automation/discord/GHLinkCommand.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package net.neoforged.automation.discord;

import com.jagrosh.jdautilities.command.SlashCommand;
import com.jagrosh.jdautilities.command.SlashCommandEvent;
import net.dv8tion.jda.api.interactions.components.buttons.Button;

public class GHLinkCommand extends SlashCommand {
private final String clientId;
private final String redirectUrl;
public GHLinkCommand(String clientId, String redirectUrl) {
this.name = "ghlink";
this.help = "Link your Discord account to your GitHub account";

this.clientId = clientId;
this.redirectUrl = redirectUrl;
}

@Override
protected void execute(SlashCommandEvent event) {
var url = "https://github.com/login/oauth/authorize?client_id=" + clientId + "&response_type=code&scope=user:read&redirect_uri=" + redirectUrl + "&state=" + event.getUser().getId();

event.reply("Please use the button below to link this Discord account to a GitHub one.")
.setEphemeral(true)
.addActionRow(Button.link(url, "Link account"))
.queue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package net.neoforged.automation.discord;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.javalin.http.Context;
import io.javalin.http.Handler;
import io.javalin.http.HttpStatus;
import net.neoforged.automation.Main;
import net.neoforged.automation.db.Database;
import net.neoforged.automation.db.DiscordUsersDAO;
import org.jetbrains.annotations.NotNull;
import org.kohsuke.github.GitHubBuilder;

import java.net.URI;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public record GitHubOauthLinkHandler(DiscordBot.OauthConfig config) implements Handler {
@Override
public void handle(@NotNull Context ctx) throws Exception {
var code = ctx.queryParam("code");
var state = ctx.queryParam("state");
if (code == null || code.isBlank() || state == null || state.isBlank()) {
ctx.result("Wrong input provided")
.status(HttpStatus.BAD_REQUEST);
return;
}

var mapper = new ObjectMapper();
var node = mapper.createObjectNode();
node.put("client_id", config.clientId());
node.put("client_secret", config.clientSecret());
node.put("grant_type", "authorization_code");
node.put("redirect_uri", config.redirectUrl());
node.put("scope", "user:read");
node.put("code", code);

var req = Main.HTTP_CLIENT.send(HttpRequest.newBuilder()
.POST(HttpRequest.BodyPublishers.ofString(node.toString()))
.uri(URI.create("https://github.com/login/oauth/access_token"))
.header("Content-Type", "application/json")
.header("Accept", "application/json")
.build(), HttpResponse.BodyHandlers.ofString());

var asJson = mapper.readValue(req.body(), JsonNode.class);
var token = asJson.get("access_token");
if (token == null) {
ctx.result("Error: " + req.body())
.status(HttpStatus.BAD_REQUEST);
return;
}

var user = new GitHubBuilder()
.withJwtToken(token.asText())
.build()
.getMyself()
.getLogin();

Database.useExtension(DiscordUsersDAO.class, db -> db.linkUser(Long.parseLong(state), user));

ctx.result("Authenticated as " + user + ". You may close this page.").status(HttpStatus.OK);
}
}
Loading

0 comments on commit c8bff53

Please sign in to comment.