From 26971e75bbdf16532d0042ba82e59205f8c358c9 Mon Sep 17 00:00:00 2001
From: hex <hex@iwakura.rip>
Date: Sat, 19 Apr 2025 21:46:49 +0200
Subject: [PATCH] feat: discord -> teamchat linking, custom join/leave
 messages, and /team home command

---
 bin/main/rip/iwakura/civil/Core.class         | Bin 4030 -> 7697 bytes
 build.gradle.kts                              |   2 +
 src/main/java/rip/iwakura/civil/Core.java     |  66 +++++-
 src/main/java/rip/iwakura/civil/Database.java |  37 +++-
 src/main/java/rip/iwakura/civil/Discord.java  | 104 ++++++++++
 .../java/rip/iwakura/civil/commands/Team.java | 195 ++++++++++++++----
 .../iwakura/civil/discord/LinkCommand.java    |  56 +++++
 .../rip/iwakura/civil/events/JoinHandler.java |   9 +-
 .../iwakura/civil/events/LeaveHandler.java    |  21 ++
 .../iwakura/civil/events/TeamChatHandler.java |  39 ++++
 .../exceptions/InvalidChannelException.java   |  12 ++
 .../rip/iwakura/civil/types/CivilTeam.java    |  49 ++++-
 12 files changed, 531 insertions(+), 59 deletions(-)
 create mode 100644 src/main/java/rip/iwakura/civil/Discord.java
 create mode 100644 src/main/java/rip/iwakura/civil/discord/LinkCommand.java
 create mode 100644 src/main/java/rip/iwakura/civil/events/LeaveHandler.java
 create mode 100644 src/main/java/rip/iwakura/civil/events/TeamChatHandler.java
 create mode 100644 src/main/java/rip/iwakura/civil/exceptions/InvalidChannelException.java

diff --git a/bin/main/rip/iwakura/civil/Core.class b/bin/main/rip/iwakura/civil/Core.class
index 4c18cb3b66d51639c9896a289ac9ab5b71047265..62d9a58d95981dd832db10e3b8df8da70ac3cfd4 100644
GIT binary patch
literal 7697
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

delta 1225
zcmY*ZTT@$A6#jNn&Ph&oLhdAmG?Ysk0xi(071OE|qGAikMOw6459NTBnA9XtwQ5?c
zR%_AX@>VbPR;^MWN>z|D)6qA_7iWA|XY}0}|A6D#fHV4X_B!AC*2=fm{!Zq0U5|Tz
z{QKH<0G;?!hfN?%10p<^7$PAenoB~5gW_Uuz>&{oa^_gpG9ciW2p~vTTw6t$esv|Z
z2sY}7k|AUEYSd6l*2gwU#NiO|70nr|+nO+^v!$+lZsN$Kj?Hx388cL;dWi-!(s|ik
zy+uHdn<aD1ELz*93t0nAXx5PuXr;@twL+o=TdBppGggaMC9KBD4ca9tpgFMvH|n@S
zpo)HSSNSAv!cGBgC^M27%m|kDy({RpW~^-5KnKz~ZlPH{!n|GBEnrMqrCzI8G$-ko
z4G|U=!EF+^qmvw-4y_9r9o>{S!uC6GC-r#JS`YT>xSP&+8tnVfOADShO-H{#oXkMn
zZJ-YWO5vbF+Vvovb2{AVrV%yqu*AI>q4COt+5z0BW0d~%Hq$X*P%BE@kAvj$j#WM=
zaR?6y*bCD+fk)PQcn@e}7}s%_EB4qeOwbPBy!Nof5oY}1Yt)fdN*|?4f5?VBCHzSb
zau{%-2L*{DN_5KKYr_ov>`zKe<8h_&34v<WU+>BnEY?WvfkBUZsAL|V>NSrJt1G9I
zfwF;T@tlrRg2(>Lo1`nDI=+$T@dAwo`)zoU=7VjHKAe$w8E5JDV5bwO@ruN&IQLac
zXiDffk9T#vLl?q{4R{al(+}a_G`ua4PSQf4iK0GFgn!<P57d@EmMG&yrG}3L`W2xs
z<i$mHNKY<j6}qx!v1k=_d`^cVO%<3I*hKABRdg{DEekGZOR%Ty9I=1->EyRoJ_Vl@
zsU;W-9M~ZFsjY$@xH*<<1b9%%Zvx(bi-mHTjj`F(B7FYPBC0rurB+Zo%0Zn!v54dn
zwta)P@+H*lQ>?4l-n@vL7jf&i*pphuT{bMNpl>wwHQJVNkKznn#s21NcmP<2X$K>d
z%a{^aL2jLPlnWpGUuJrg^&eY<7g(bkoB0#!;9=D?+fi0WfF}-dD#FPq8nKbTk81XM
z47*XwcBo^gCD;L5a2Cnc(QCN@kGDR0BUe6&<J`Un-FOO5bMy@7`7eSE*b(s>>UtPd
ziyh|8sNDpcmvK@Q@f}X{Brh#v)`tCR(QDF-RZ=}`YxvKygP6l>+=!UBwtj`Y&WIgv
dFlNJ>cxyd(fw2$q2|mSVj3}F2!WX!V=s(v?=h6TG

diff --git a/build.gradle.kts b/build.gradle.kts
index e1607bc..3a1fbe7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -33,6 +33,8 @@ tasks {
 
 dependencies {
     compileOnly("io.papermc.paper:paper-api:1.21.4-R0.1-SNAPSHOT")
+    implementation("club.minnced:discord-webhooks:0.8.4")
+    implementation("net.dv8tion:JDA:5.3.2")
     implementation("com.j256.ormlite", "ormlite-jdbc", "6.1")
     implementation("org.postgresql", "postgresql", "42.7.5")
     library("com.google.code.gson", "gson", "2.10.1") // All platform plugins
diff --git a/src/main/java/rip/iwakura/civil/Core.java b/src/main/java/rip/iwakura/civil/Core.java
index c04addb..1f21b89 100644
--- a/src/main/java/rip/iwakura/civil/Core.java
+++ b/src/main/java/rip/iwakura/civil/Core.java
@@ -1,18 +1,72 @@
 package rip.iwakura.civil;
 
 import java.sql.SQLException;
+import java.util.ArrayList;
 import java.util.logging.Level;
 
 import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
 import org.bukkit.plugin.java.JavaPlugin;
 
 import io.papermc.paper.plugin.lifecycle.event.types.LifecycleEvents;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.JDABuilder;
+import net.dv8tion.jda.api.entities.User;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import net.dv8tion.jda.api.utils.messages.MessageCreateData;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
 import rip.iwakura.civil.events.ChatHandler;
+import rip.iwakura.civil.events.TeamChatHandler;
 import rip.iwakura.civil.commands.Team;
 import rip.iwakura.civil.events.JoinHandler;
+import rip.iwakura.civil.types.CivilPlayer;
 
 public class Core extends JavaPlugin {
     public Database database;
+    public Discord discord;
+
+    public java.util.List<Player> teamChatToggled;
+
+    public Component renderTeamChat(CivilPlayer p, Component message) {
+        return Component.text("TEAM")
+                .color(NamedTextColor.BLUE)
+                .decoration(TextDecoration.BOLD, true)
+                .appendSpace()
+                .append(Component.text(p.getName())
+                        .append(Component.text(": "))
+                        .append(message)
+                        .color(NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false));
+    }
+
+    public Component renderTeamChat(User author, String message) {
+        return Component.text("TEAM")
+                .color(NamedTextColor.BLUE)
+                .decoration(TextDecoration.BOLD, true)
+                .append(Component.text(" Discord", NamedTextColor.BLUE).decoration(TextDecoration.BOLD, false)
+                .appendSpace()
+                .append(Component.text(author.getName())
+                        .append(Component.text(": "))
+                        .append(Component.text(message))
+                        .color(NamedTextColor.WHITE).decoration(TextDecoration.BOLD, false)));
+    }
+
+
+    public void sendTeamMessage(CivilPlayer author, Component message) throws SQLException {
+        Component deserializedMessage = renderTeamChat(author, message);
+        discord.sendMessage(author, PlainTextComponentSerializer.plainText().serialize(message));
+        for (CivilPlayer member : database.getAllPlayers(author.getTeam())) {
+            Player player = Bukkit.getPlayer(member.getName());
+
+            if (!player.isOnline())
+                continue;
+
+            player.sendMessage(deserializedMessage);
+        }
+    }
 
     @Override
     public void onEnable() {
@@ -20,16 +74,19 @@ public class Core extends JavaPlugin {
 
         saveDefaultConfig();
 
+        teamChatToggled = new ArrayList<>();
+
         try {
-            this.database = new Database(getConfig().getString("database.url"));
+            this.database = new Database(getConfig().getString("database.url"), this);
         } catch (SQLException e) {
             getLogger().log(Level.SEVERE, e.getMessage());
             Bukkit.getPluginManager().disablePlugin(this);
-
-            return;
         }
 
-        Team teamCommand = new Team(database);
+        this.discord = new Discord(this);
+        discord.connect(getConfig().getString("discord.token"));
+
+        Team teamCommand = new Team(this);
 
         this.getLifecycleManager().registerEventHandler(LifecycleEvents.COMMANDS, commands -> {
             commands.registrar().register(teamCommand.getCommand());
@@ -37,5 +94,6 @@ public class Core extends JavaPlugin {
 
         getServer().getPluginManager().registerEvents(new JoinHandler(database), this);
         getServer().getPluginManager().registerEvents(new ChatHandler(this), this);
+        getServer().getPluginManager().registerEvents(new TeamChatHandler(this), this);
     }
 }
diff --git a/src/main/java/rip/iwakura/civil/Database.java b/src/main/java/rip/iwakura/civil/Database.java
index 6cabcb0..1669d2f 100644
--- a/src/main/java/rip/iwakura/civil/Database.java
+++ b/src/main/java/rip/iwakura/civil/Database.java
@@ -4,17 +4,17 @@ import java.sql.SQLException;
 import java.util.List;
 import java.util.UUID;
 
-import org.apache.logging.log4j.LogManager;
+import org.bukkit.Location;
 import org.bukkit.entity.Player;
 
 import com.j256.ormlite.dao.Dao;
 import com.j256.ormlite.dao.DaoManager;
 import com.j256.ormlite.jdbc.JdbcConnectionSource;
-import com.j256.ormlite.logger.LoggerFactory;
 import com.j256.ormlite.support.ConnectionSource;
 import com.j256.ormlite.table.TableUtils;
 
-import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import rip.iwakura.civil.exceptions.InvalidChannelException;
 import rip.iwakura.civil.types.CivilPlayer;
 import rip.iwakura.civil.types.CivilTeam;
 
@@ -22,28 +22,27 @@ public class Database {
     private final Dao<CivilPlayer, UUID> playerDao;
     private final Dao<CivilTeam, String> teamDao;
     private ConnectionSource connectionSource;
+    private Core core;
 
-    public Database(String uri) throws SQLException {
+    public Database(String uri, Core core) throws SQLException {
         connectionSource = new JdbcConnectionSource(uri);
 
-
         TableUtils.createTableIfNotExists(connectionSource, CivilPlayer.class);
         TableUtils.createTableIfNotExists(connectionSource, CivilTeam.class);
 
         playerDao = DaoManager.createDao(connectionSource, CivilPlayer.class);
         teamDao = DaoManager.createDao(connectionSource, CivilTeam.class);
+        this.core = core;
     }
 
     public void createPlayer(Player p) throws SQLException {
         playerDao.create(
-                new CivilPlayer(p.getUniqueId(), p.getName())
-        );
+                new CivilPlayer(p.getUniqueId(), p.getName()));
     }
 
     public void createTeam(String name, String prefix) throws SQLException {
         teamDao.create(
-                new CivilTeam(name, prefix)
-        );
+                new CivilTeam(name, prefix));
     }
 
     public void joinTeam(Player p, CivilTeam team) throws SQLException {
@@ -65,6 +64,10 @@ public class Database {
         return teamDao.queryForAll();
     }
 
+    public List<CivilTeam> getAllTeams(Long channel) throws SQLException {
+        return teamDao.queryBuilder().where().eq("channel", channel).query();
+    }
+
     public CivilPlayer getPlayer(String name) throws SQLException {
         return playerDao.queryBuilder().where().eq("name", name).queryForFirst();
     }
@@ -80,4 +83,20 @@ public class Database {
     public List<CivilPlayer> getAllPlayers() throws SQLException {
         return playerDao.queryForAll();
     }
+
+    public List<CivilPlayer> getAllPlayers(CivilTeam team) throws SQLException {
+        return playerDao.queryBuilder().where().eq("team_id", team.getName()).query();
+    }
+
+    public void setHome(CivilTeam team, Location location) throws SQLException {
+        team.setHome(location);
+        teamDao.update(team);
+    }
+
+    public void linkChannel(CivilTeam team, TextChannel channel) throws SQLException {
+        team.setChannel(channel.getIdLong());
+
+        team.setWebhook(channel.createWebhook("CivilCore").complete().getUrl());
+        teamDao.update(team);
+    }
 }
diff --git a/src/main/java/rip/iwakura/civil/Discord.java b/src/main/java/rip/iwakura/civil/Discord.java
new file mode 100644
index 0000000..a344563
--- /dev/null
+++ b/src/main/java/rip/iwakura/civil/Discord.java
@@ -0,0 +1,104 @@
+package rip.iwakura.civil;
+
+import java.sql.SQLException;
+import java.util.List;
+
+import org.bukkit.Bukkit;
+import org.bukkit.entity.Player;
+
+import com.mojang.brigadier.arguments.StringArgumentType;
+
+import club.minnced.discord.webhook.WebhookClient;
+import club.minnced.discord.webhook.WebhookClientBuilder;
+import club.minnced.discord.webhook.send.WebhookMessageBuilder;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.JDABuilder;
+import net.dv8tion.jda.api.Permission;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Webhook;
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+import net.dv8tion.jda.api.events.message.MessageReceivedEvent;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
+import net.dv8tion.jda.api.interactions.commands.DefaultMemberPermissions;
+import net.dv8tion.jda.api.interactions.commands.OptionType;
+import net.dv8tion.jda.api.interactions.commands.build.Commands;
+import net.dv8tion.jda.api.requests.GatewayIntent;
+import net.dv8tion.jda.api.utils.messages.MessageCreateBuilder;
+import rip.iwakura.civil.discord.LinkCommand;
+import rip.iwakura.civil.types.CivilPlayer;
+import rip.iwakura.civil.types.CivilTeam;
+
+/**
+ * Discord
+ */
+public class Discord extends ListenerAdapter {
+    private Core core;
+    public JDA jda;
+
+    public Discord(Core core) {
+        this.core = core;
+    }
+
+    public void sendMessage(CivilPlayer p, String message) {
+        String webhook = p.getTeam().getWebhook();
+        if (webhook != null) {
+            WebhookClient client = WebhookClient.withUrl(webhook);
+            client.send(new WebhookMessageBuilder().setUsername(p.getName())
+                    .setAvatarUrl("https://www.mc-heads.net/avatar/" + p.getName()).setContent(message).build());
+        }
+    }
+
+    public void connect(String token) {
+        jda = JDABuilder.createDefault(token)
+                .enableIntents(GatewayIntent.MESSAGE_CONTENT, GatewayIntent.GUILD_MESSAGES, GatewayIntent.GUILD_MEMBERS)
+                .addEventListeners(this)
+                .addEventListeners(new LinkCommand(core))
+                .build();
+
+        try {
+            jda.awaitReady();
+        } catch (InterruptedException e) {
+            e.printStackTrace();
+        }
+
+        registerCommands(core.getConfig().getLong("discord.guild"));
+    }
+
+    public void registerCommands(Long guildId) {
+        Guild guild = jda.getGuildById(guildId);
+
+        if (guild == null) {
+            core.getLogger().info("Guild ID not found.");
+            return;
+        }
+
+        guild.updateCommands().addCommands(Commands.slash("link", "Link in-game team chat to a discord channel.")
+                .setDefaultPermissions(DefaultMemberPermissions.enabledFor(Permission.MANAGE_CHANNEL))
+                .addOption(OptionType.STRING, "team", "Name of the team you want to link to the current channel.", true,
+                        true))
+                .queue();
+    }
+
+    @Override
+    public void onMessageReceived(MessageReceivedEvent event) {
+        if (event.getAuthor().isBot()) return;
+
+        try {
+            List<CivilTeam> linked_teams = core.database.getAllTeams(event.getChannel().getIdLong());
+
+            for (CivilTeam team : linked_teams) {
+                List<CivilPlayer> members = core.database.getAllPlayers(team);
+
+                for (CivilPlayer member : members) {
+                    Player player = Bukkit.getPlayerExact(member.getName());
+
+                    if (player == null || !player.isOnline()) continue;
+
+                    player.sendMessage(core.renderTeamChat(event.getAuthor(),event.getMessage().getContentDisplay()));
+                }
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/main/java/rip/iwakura/civil/commands/Team.java b/src/main/java/rip/iwakura/civil/commands/Team.java
index 3bfb7c8..8db4450 100644
--- a/src/main/java/rip/iwakura/civil/commands/Team.java
+++ b/src/main/java/rip/iwakura/civil/commands/Team.java
@@ -1,79 +1,200 @@
 package rip.iwakura.civil.commands;
 
 import java.sql.SQLException;
+import java.text.Format;
 
-import javax.xml.crypto.Data;
-
+import org.bukkit.Location;
+import org.bukkit.command.CommandSender;
 import org.bukkit.entity.Player;
 
 import com.mojang.brigadier.Command;
+import com.mojang.brigadier.arguments.LongArgumentType;
 import com.mojang.brigadier.arguments.StringArgumentType;
 import com.mojang.brigadier.builder.LiteralArgumentBuilder;
-import com.mojang.brigadier.context.CommandContext;
 import com.mojang.brigadier.tree.LiteralCommandNode;
 
 import io.papermc.paper.command.brigadier.CommandSourceStack;
 import io.papermc.paper.command.brigadier.Commands;
 import net.kyori.adventure.text.Component;
 import net.kyori.adventure.text.format.NamedTextColor;
+import net.kyori.adventure.text.format.TextDecoration;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import rip.iwakura.civil.Core;
 import rip.iwakura.civil.Database;
+import rip.iwakura.civil.exceptions.InvalidChannelException;
 import rip.iwakura.civil.types.*;
 
 public class Team {
     private Database database;
+    private Core core;
 
-    public Team(Database database) {
-        this.database = database;
+    public Team(Core core) {
+        this.core = core;
+        this.database = core.database;
     }
 
-    private LiteralArgumentBuilder<CommandSourceStack> createTeam() { 
+    private LiteralArgumentBuilder<CommandSourceStack> createTeam() {
         return Commands.literal("create")
-            .then(
-                    Commands.argument("name", StringArgumentType.string())
-                    .then(
-                        Commands.argument("prefix", StringArgumentType.string())
+                .requires(source -> source.getSender().hasPermission("civil.team.create"))
+                .then(
+                        Commands.argument("name", StringArgumentType.string())
+                                .then(
+                                        Commands.argument("prefix", StringArgumentType.string())
+                                                .executes(ctx -> {
+                                                    String name = StringArgumentType.getString(ctx, "name");
+                                                    String prefix = StringArgumentType.getString(ctx, "prefix");
+                                                    CommandSender sender = ctx.getSource().getSender();
+
+                                                    try {
+                                                        database.createTeam(name, prefix);
+                                                        sender.sendMessage(
+                                                                Component
+                                                                        .text("Successfully created team ",
+                                                                                NamedTextColor.GREEN)
+                                                                        .append(Component
+                                                                                .text(name, NamedTextColor.GOLD))
+                                                                        .append(Component.text(" with prefix: \""))
+                                                                        .append(MiniMessage.miniMessage()
+                                                                                .deserialize(prefix))
+                                                                        .append(Component.text("\"")));
+
+                                                        return Command.SINGLE_SUCCESS;
+                                                    } catch (SQLException e) {
+                                                        ctx.getSource().getSender().sendMessage(
+                                                                Component.text(e.getMessage(), NamedTextColor.RED));
+                                                        e.printStackTrace();
+                                                        return 0;
+                                                    }
+                                                })));
+    }
+
+    // Send team chat message
+    private LiteralArgumentBuilder<CommandSourceStack> homeCommand() {
+        return Commands.literal("home")
+                .requires(sender -> sender.getSender() instanceof Player)
+                .executes(ctx -> {
+                    Player p = (Player) ctx.getSource().getSender();
+
+                    try {
+                        CivilPlayer cPlayer = core.database.getPlayer(p);
+                        p.teleport(cPlayer.getTeam().getHome(core));
+                        p.sendRichMessage("<green>You have been teleported to your team's home.");
+                        return Command.SINGLE_SUCCESS;
+                    } catch (SQLException e) {
+                        e.printStackTrace();
+                    }
+
+                    return 0;
+                }).then(Commands.literal("set")
+                        .requires(sender -> sender.getSender() instanceof Player)
                         .executes(ctx -> {
-                            String name = StringArgumentType.getString(ctx, "name");
-                            String prefix = StringArgumentType.getString(ctx, "prefix");
+                            Player p = (Player) ctx.getSource().getSender();
 
                             try {
-                                database.createTeam(name, prefix);
+                                CivilPlayer cPlayer = database.getPlayer(p);
+
+                                Location location = p.getLocation();
+
+                                database.setHome(cPlayer.getTeam(), location);
+
+                                p.sendRichMessage("<green>Your team's home location has been set to: "
+                                        + String.format("<red>%d %d %d", Math.round(location.x()),
+                                                Math.round(location.y()), Math.round(location.z())));
                                 return Command.SINGLE_SUCCESS;
+
                             } catch (SQLException e) {
-                                ctx.getSource().getSender().sendMessage(
-                                        Component.text(e.getMessage(), NamedTextColor.RED));
-                                return 0;
+                                e.printStackTrace();
                             }
-                        })));
+
+                            return 1;
+                        }));
+
+    }
+
+    // Send team chat message
+    private LiteralArgumentBuilder<CommandSourceStack> teamChat() {
+        return Commands.literal("chat")
+                .requires(sender -> sender.getSender() instanceof Player)
+                .executes(ctx -> {
+                    Player p = (Player) ctx.getSource().getSender();
+
+                    if (core.teamChatToggled.contains(p)) {
+                        core.teamChatToggled.remove(p);
+                        p.sendRichMessage("<red>Team chat disabled.");
+                    } else {
+                        core.teamChatToggled.add(p);
+                        p.sendRichMessage("<green>Team chat enabled.");
+                    }
+
+                    return Command.SINGLE_SUCCESS;
+                }).then(Commands.argument("message", StringArgumentType.greedyString())
+                        .requires(sender -> sender.getSender() instanceof Player)
+                        .executes(ctx -> {
+                            CommandSender sender = ctx.getSource().getSender();
+                            String message = StringArgumentType.getString(ctx, "message");
+
+                            try {
+                                CivilPlayer p = database.getPlayer((Player) sender);
+
+                                core.sendTeamMessage(p, message);
+
+                                return Command.SINGLE_SUCCESS;
+
+                            } catch (SQLException e) {
+                                sender.sendRichMessage("<red>Uh oh, something went wrong!");
+                                e.printStackTrace();
+                            }
+
+                            return 1;
+                        }));
+
     }
 
     // Add a player to a team.
     private LiteralArgumentBuilder<CommandSourceStack> joinTeam() {
         return Commands.literal("add")
-            .then(
-                    Commands.argument("player", new PlayerArgument(database))
-                    .then(
-                        Commands.argument("team", new TeamArgument(database))
-                        .executes(ctx -> {
-                            CivilTeam civilTeam = ctx.getArgument("team", CivilTeam.class);
-                            CivilPlayer civilPlayer = ctx.getArgument("player",
-                                    CivilPlayer.class);
+                .requires(source -> source.getSender().hasPermission("civil.team.add"))
+                .then(
+                        Commands.argument("player", new PlayerArgument(database))
+                                .then(
+                                        Commands.argument("team", new TeamArgument(database))
+                                                .executes(ctx -> {
+                                                    CivilTeam civilTeam = ctx.getArgument("team", CivilTeam.class);
+                                                    CivilPlayer civilPlayer = ctx.getArgument("player",
+                                                            CivilPlayer.class);
+                                                    CommandSender sender = ctx.getSource().getSender();
 
-                            try {
-                                database.joinTeam(civilPlayer, civilTeam);
-                                return Command.SINGLE_SUCCESS;
-                            } catch (SQLException e) {
-                                ctx.getSource().getSender().sendMessage(
-                                        Component.text(e.getMessage(), NamedTextColor.RED));
-                                return 0;
-                            }
-                        })));
+                                                    try {
+                                                        database.joinTeam(civilPlayer, civilTeam);
+
+                                                        sender.sendMessage(
+                                                                Component
+                                                                        .text("Successfully added ",
+                                                                                NamedTextColor.GREEN)
+                                                                        .append(Component
+                                                                                .text(civilPlayer.getName(),
+                                                                                        NamedTextColor.GOLD))
+                                                                        .append(Component.text(" to team "))
+                                                                        .append(Component
+                                                                                .text(civilTeam.getName(),
+                                                                                        NamedTextColor.AQUA)));
+                                                        return Command.SINGLE_SUCCESS;
+                                                    } catch (SQLException e) {
+                                                        ctx.getSource().getSender().sendMessage(
+                                                                Component.text(e.getMessage(), NamedTextColor.RED));
+                                                        e.printStackTrace();
+                                                        return 0;
+                                                    }
+                                                })));
     };
 
-    public LiteralCommandNode<CommandSourceStack> getCommand() { 
+    public LiteralCommandNode<CommandSourceStack> getCommand() {
         return Commands.literal("team")
-            .then(createTeam())
-            .then(joinTeam()).build();
+                .then(createTeam())
+                .then(joinTeam())
+                .then(teamChat())
+                .then(homeCommand())
+                .build();
     }
 
 }
diff --git a/src/main/java/rip/iwakura/civil/discord/LinkCommand.java b/src/main/java/rip/iwakura/civil/discord/LinkCommand.java
new file mode 100644
index 0000000..c5db068
--- /dev/null
+++ b/src/main/java/rip/iwakura/civil/discord/LinkCommand.java
@@ -0,0 +1,56 @@
+package rip.iwakura.civil.discord;
+
+import java.sql.SQLException;
+import java.util.List;
+import java.util.logging.StreamHandler;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import net.dv8tion.jda.api.events.interaction.command.CommandAutoCompleteInteractionEvent;
+import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
+import net.dv8tion.jda.api.interactions.commands.Command;
+import net.dv8tion.jda.api.interactions.commands.SlashCommandInteraction;
+import rip.iwakura.civil.Core;
+import rip.iwakura.civil.types.CivilTeam;
+
+/**
+ * LinkCommand
+ */
+public class LinkCommand extends ListenerAdapter {
+    private Core core;
+
+    public LinkCommand(Core core) {
+        this.core = core;
+    }
+
+    @Override
+    public void onSlashCommandInteraction(SlashCommandInteractionEvent event) {
+        try {
+            if (event.getName().equals("link")) {
+                core.database.linkChannel(core.database.getTeam(event.getOption("team").getAsString()),event.getChannel().asTextChannel());
+                event.reply("Successfully linked channel to team chat.").queue();
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Override
+    public void onCommandAutoCompleteInteraction(CommandAutoCompleteInteractionEvent event) {
+        try {
+            if (event.getName().equals("link") && event.getFocusedOption().getName().equals("team")) {
+                List<String> teamNames = core.database.getAllTeams().stream().map(CivilTeam::getName).collect(
+                        Collectors.toList());
+
+                List<Command.Choice> options = teamNames.stream()
+                    .filter(name -> name.startsWith(event.getFocusedOption().getValue()))
+                    .map(name -> new Command.Choice(name, name)).collect(Collectors.toList());
+
+                event.replyChoices(options).queue();
+            }
+        } catch (SQLException e) {
+            e.printStackTrace();
+        }
+    }
+}
diff --git a/src/main/java/rip/iwakura/civil/events/JoinHandler.java b/src/main/java/rip/iwakura/civil/events/JoinHandler.java
index 77f8082..770a8d4 100644
--- a/src/main/java/rip/iwakura/civil/events/JoinHandler.java
+++ b/src/main/java/rip/iwakura/civil/events/JoinHandler.java
@@ -7,6 +7,7 @@ import org.bukkit.event.EventHandler;
 import org.bukkit.event.Listener;
 import org.bukkit.event.player.PlayerJoinEvent;
 
+import net.kyori.adventure.text.minimessage.MiniMessage;
 import rip.iwakura.civil.Database;
 
 public class JoinHandler implements Listener {
@@ -17,9 +18,13 @@ public class JoinHandler implements Listener {
     }
 
     @EventHandler
-    public void chatHandler(PlayerJoinEvent event) throws SQLException {
+    public void joinHandler(PlayerJoinEvent event) throws SQLException {
         Player p = event.getPlayer();
 
-        if (database.getPlayer(p) == null) database.createPlayer(p); 
+        event.joinMessage(MiniMessage.miniMessage()
+                .deserialize("<gray>[<green>+<gray>] <green>" + p.getName() + "<white>"));
+
+        if (database.getPlayer(p) == null)
+            database.createPlayer(p);
     }
 }
diff --git a/src/main/java/rip/iwakura/civil/events/LeaveHandler.java b/src/main/java/rip/iwakura/civil/events/LeaveHandler.java
new file mode 100644
index 0000000..18ce883
--- /dev/null
+++ b/src/main/java/rip/iwakura/civil/events/LeaveHandler.java
@@ -0,0 +1,21 @@
+package rip.iwakura.civil.events;
+
+import java.sql.SQLException;
+
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.Listener;
+import org.bukkit.event.player.PlayerQuitEvent;
+
+import net.kyori.adventure.text.minimessage.MiniMessage;
+
+public class LeaveHandler implements Listener {
+
+    @EventHandler
+    public void leaveHandler(PlayerQuitEvent event) throws SQLException {
+        Player p = event.getPlayer();
+
+        event.quitMessage(MiniMessage.miniMessage()
+                .deserialize("<gray>[<red>-<gray>] <red>" + p.getName() + "<white>"));
+    }
+}
diff --git a/src/main/java/rip/iwakura/civil/events/TeamChatHandler.java b/src/main/java/rip/iwakura/civil/events/TeamChatHandler.java
new file mode 100644
index 0000000..d097852
--- /dev/null
+++ b/src/main/java/rip/iwakura/civil/events/TeamChatHandler.java
@@ -0,0 +1,39 @@
+package rip.iwakura.civil.events;
+
+import java.sql.SQLException;
+
+import org.bukkit.entity.Player;
+import org.bukkit.event.EventHandler;
+import org.bukkit.event.EventPriority;
+import org.bukkit.event.Listener;
+
+import io.papermc.paper.chat.ChatRenderer;
+import io.papermc.paper.event.player.AsyncChatEvent;
+import net.kyori.adventure.audience.Audience;
+import net.kyori.adventure.text.Component;
+import net.kyori.adventure.text.minimessage.MiniMessage;
+import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
+import rip.iwakura.civil.Core;
+import rip.iwakura.civil.Database;
+import rip.iwakura.civil.types.CivilPlayer;
+
+public class TeamChatHandler implements Listener {
+    private Core core;
+    private Database database;
+
+    public TeamChatHandler(Core core) {
+        this.core = core;
+        this.database = core.database;
+    }
+
+    @EventHandler(priority = EventPriority.HIGHEST)
+    public void chatHandler(AsyncChatEvent event) throws SQLException {
+        if (core.teamChatToggled.contains(event.getPlayer())) {
+            CivilPlayer p = database.getPlayer(event.getPlayer());
+
+            core.sendTeamMessage(p, MiniMessage.miniMessage()
+                    .deserialize(PlainTextComponentSerializer.plainText().serialize(event.message())));
+            event.setCancelled(true);
+        }
+    }
+}
diff --git a/src/main/java/rip/iwakura/civil/exceptions/InvalidChannelException.java b/src/main/java/rip/iwakura/civil/exceptions/InvalidChannelException.java
new file mode 100644
index 0000000..14d56ec
--- /dev/null
+++ b/src/main/java/rip/iwakura/civil/exceptions/InvalidChannelException.java
@@ -0,0 +1,12 @@
+package rip.iwakura.civil.exceptions;
+
+import net.dv8tion.jda.api.entities.channel.concrete.TextChannel;
+
+/**
+ * InvalidChannelException
+ */
+public class InvalidChannelException extends Exception {
+    public InvalidChannelException(Long channelId) {
+        super("Channel with ID: " + channelId.toString() + " could not be found.");
+    }
+}
diff --git a/src/main/java/rip/iwakura/civil/types/CivilTeam.java b/src/main/java/rip/iwakura/civil/types/CivilTeam.java
index c6fb73e..b6ebf97 100644
--- a/src/main/java/rip/iwakura/civil/types/CivilTeam.java
+++ b/src/main/java/rip/iwakura/civil/types/CivilTeam.java
@@ -5,6 +5,8 @@ import org.bukkit.Location;
 import com.j256.ormlite.field.DatabaseField;
 import com.j256.ormlite.table.DatabaseTable;
 
+import rip.iwakura.civil.Core;
+
 @DatabaseTable(tableName = "teams")
 public class CivilTeam {
     @DatabaseField(id = true)
@@ -13,8 +15,23 @@ public class CivilTeam {
     @DatabaseField(canBeNull = false)
     private String prefix;
 
-    /*@DatabaseField(canBeNull = true)
-    private Location home;*/
+    @DatabaseField(canBeNull = true)
+    private Long channel;
+
+    @DatabaseField(canBeNull = true)
+    private String webhook;
+
+    @DatabaseField(canBeNull = true)
+    private double home_x;
+
+    @DatabaseField(canBeNull = true)
+    private double home_y;
+
+    @DatabaseField(canBeNull = true)
+    private double home_z;
+
+    @DatabaseField(canBeNull = true)
+    private String home_world;
 
     public CivilTeam() {
     }
@@ -31,13 +48,31 @@ public class CivilTeam {
     public String getPrefix() {
         return prefix;
     }
-/*
+
     public void setHome(Location home) {
-        this.home = home;
+        this.home_x = home.x();
+        this.home_y = home.y();
+        this.home_z = home.z();
+        this.home_world = home.getWorld().getName();
     }
 
-    public Location getHome() {
-        return home;
+    public Location getHome(Core core) {
+        return new Location(core.getServer().getWorld(home_world), home_x, home_y, home_z);
+    }
+
+    public void setChannel(Long channel) {
+        this.channel = channel;
+    }
+
+    public Long getChannel() {
+        return channel;
+    }
+
+    public void setWebhook(String webhook) {
+        this.webhook = webhook;
+    }
+
+    public String getWebhook() {
+        return webhook;
     }
-    */
 }