diff --git a/pom.xml b/pom.xml
index dbf27603..8bb101a0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -35,10 +35,31 @@
8
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.2.4
+
+
+ package
+
+ shade
+
+
+
+ bungeecore
+
+
+ dv8tion
+ m2-dv8tion
+ https://m2.dv8tion.net/releases
+
+
+
steamwar
@@ -61,5 +82,23 @@
system${main.basedir}/lib/BungeeTabListPlus.jar
+
+ net.dv8tion
+ JDA
+ 4.3.0_299
+ compile
+
+
+ club.minnced
+ opus-java
+
+
+
+
+ org.projectlombok
+ lombok
+ 1.18.20
+ provided
+
\ No newline at end of file
diff --git a/src/de/steamwar/bungeecore/BungeeCore.java b/src/de/steamwar/bungeecore/BungeeCore.java
index f0a8bb79..8a457e29 100644
--- a/src/de/steamwar/bungeecore/BungeeCore.java
+++ b/src/de/steamwar/bungeecore/BungeeCore.java
@@ -19,6 +19,8 @@
package de.steamwar.bungeecore;
+import de.steamwar.bungeecore.bot.SteamwarDiscordBot;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
import de.steamwar.bungeecore.commands.*;
import de.steamwar.bungeecore.comms.SpigotReceiver;
import de.steamwar.bungeecore.listeners.*;
@@ -124,6 +126,7 @@ public class BungeeCore extends Plugin {
new ResourcereloadCommand();
new ListCommand();
new StatCommand();
+ new VerifyCommand();
if(!EVENT_MODE){
new WebregisterCommand();
@@ -141,6 +144,7 @@ public class BungeeCore extends Plugin {
new EventStarter();
new SessionManager();
new SpigotReceiver();
+ new SteamwarDiscordBot();
new TablistManager();
getProxy().getScheduler().schedule(this, () -> {
@@ -254,6 +258,7 @@ public class BungeeCore extends Plugin {
);
ArenaMode.init(config.getSection("games"));
+ SteamwarDiscordBotConfig.loadConfig(config.getSection("discord"));
final Configuration servers = config.getSection("servers");
for(final String serverName : servers.getKeys()){
diff --git a/src/de/steamwar/bungeecore/bot/AuthManager.java b/src/de/steamwar/bungeecore/bot/AuthManager.java
new file mode 100644
index 00000000..d574c460
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/AuthManager.java
@@ -0,0 +1,78 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot;
+
+import de.steamwar.bungeecore.BungeeCore;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import de.steamwar.bungeecore.sql.SteamwarUser;
+import net.dv8tion.jda.api.MessageBuilder;
+import net.dv8tion.jda.api.entities.Emoji;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.interactions.components.ActionRow;
+import net.dv8tion.jda.api.interactions.components.Button;
+import net.md_5.bungee.api.scheduler.ScheduledTask;
+
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+
+public class AuthManager {
+
+ private static final Map TOKENS = new HashMap<>();
+ private static final Random rand = new Random();
+
+ public static String createDiscordAuthToken(Member member) {
+ if(TOKENS.containsValue(member.getIdLong())) return null;
+
+ byte[] randBytes = new byte[16];
+ rand.nextBytes(randBytes);
+ randBytes[0] = 'D';
+ randBytes[1] = 'C';
+ String code = Base64.getEncoder().encodeToString(randBytes);
+
+ TOKENS.put(code, member.getIdLong());
+ BungeeCore.log("Created Discord Auth-Token: " + code + " for: " + member.getUser().getAsTag());
+ ScheduledTask[] task = new ScheduledTask[1];
+ task[0] = BungeeCore.get().getProxy().getScheduler().schedule(BungeeCore.get(), () -> {
+ TOKENS.remove(code);
+ task[0].cancel();
+ }, 10, 10, TimeUnit.MINUTES);
+ return code;
+ }
+
+ public static Member connectAuth(SteamwarUser user, String code) {
+ if (TOKENS.containsKey(code)) {
+ Member member = SteamwarDiscordBot.instance().getJda().getGuildById(SteamwarDiscordBotConfig.GUILD).retrieveMemberById(TOKENS.get(code).longValue()).complete();
+ if(member == null) return null;
+ user.setDiscordId(member.getId());
+ MessageBuilder builder = new MessageBuilder();
+ builder.setContent(":white_check_mark: Dein Discord Konto wurde mit **" + user.getUserName() + "** verknüpft");
+ builder.setActionRows(ActionRow.of(Button.success("tada", Emoji.fromUnicode("U+1F389")), Button.danger("invalid", "Ich war das nicht")));
+
+ member.getUser().openPrivateChannel().complete().sendMessage(builder.build()).complete();
+ if(member.getNickname() == null) {
+ member.getGuild().modifyNickname(member, user.getUserName()).complete();
+ }
+ TOKENS.remove(code);
+ return member;
+ } else {
+ return null;
+ }
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/SteamwarDiscordBot.java b/src/de/steamwar/bungeecore/bot/SteamwarDiscordBot.java
new file mode 100644
index 00000000..03dceb9a
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/SteamwarDiscordBot.java
@@ -0,0 +1,114 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot;
+
+import de.steamwar.bungeecore.BungeeCore;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import de.steamwar.bungeecore.bot.events.EventManager;
+import de.steamwar.bungeecore.bot.listeners.AnnouncementListener;
+import de.steamwar.bungeecore.bot.listeners.DiscordAuthListener;
+import de.steamwar.bungeecore.bot.listeners.DiscordTicketListener;
+import de.steamwar.bungeecore.bot.listeners.RolesInteractionButtonListener;
+import de.steamwar.bungeecore.bot.util.DiscordTicketMessage;
+import de.steamwar.bungeecore.bot.util.DiscordRolesMessage;
+import de.steamwar.bungeecore.bot.util.DiscordRulesMessage;
+import de.steamwar.bungeecore.sql.Event;
+import lombok.Getter;
+import net.dv8tion.jda.api.JDA;
+import net.dv8tion.jda.api.JDABuilder;
+import net.dv8tion.jda.api.OnlineStatus;
+import net.dv8tion.jda.api.entities.Activity;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
+import net.dv8tion.jda.api.utils.MemberCachePolicy;
+import net.md_5.bungee.api.ProxyServer;
+
+import javax.security.auth.login.LoginException;
+import java.util.concurrent.TimeUnit;
+
+public class SteamwarDiscordBot {
+
+ private static SteamwarDiscordBot INSTANCE;
+
+ public static SteamwarDiscordBot instance() {
+ return INSTANCE;
+ }
+
+ @Getter
+ private AnnouncementListener announcementListener;
+
+ @Getter
+ private final JDA jda;
+
+ public SteamwarDiscordBot() {
+ INSTANCE = this;
+ JDABuilder builder = JDABuilder.createDefault(SteamwarDiscordBotConfig.TOKEN);
+ builder.setStatus(OnlineStatus.ONLINE);
+ builder.setMemberCachePolicy(MemberCachePolicy.ONLINE);
+ try {
+ jda = builder.build();
+ } catch (LoginException e) {
+ throw new SecurityException("Could not Login: " + SteamwarDiscordBotConfig.TOKEN, e);
+ }
+ try {
+ jda.awaitReady();
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ activity();
+ EventManager.update();
+ ProxyServer.getInstance().getScheduler().schedule(BungeeCore.get(), () -> {
+ activity();
+ EventManager.update();
+ }, 30, 30, TimeUnit.SECONDS);
+ DiscordRolesMessage.sendMessage();
+ DiscordRulesMessage.sendMessage();
+ DiscordTicketMessage.sendMessage();
+
+ new RolesInteractionButtonListener();
+ new DiscordTicketListener();
+ new DiscordAuthListener();
+ announcementListener = new AnnouncementListener();
+ }
+
+ private int index = 0;
+
+ private void activity() {
+ switch (index) {
+ case 0:
+ Event event = Event.get();
+ if (event != null) {
+ jda.getPresence().setActivity(Activity.competing("dem Event " + event.getEventName()));
+ } else {
+ jda.getPresence().setActivity(Activity.playing("auf SteamWar.de"));
+ }
+ break;
+ case 1:
+ jda.getPresence().setActivity(Activity.playing("mit " + BungeeCore.get().getProxy().getOnlineCount() + " Spielern"));
+ index = 0;
+ return;
+ }
+ index++;
+ }
+
+ public void addListener(ListenerAdapter listenerAdapter) {
+ jda.addEventListener(listenerAdapter);
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/config/DiscordRole.java b/src/de/steamwar/bungeecore/bot/config/DiscordRole.java
new file mode 100644
index 00000000..68b57bee
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/config/DiscordRole.java
@@ -0,0 +1,39 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import net.dv8tion.jda.api.entities.Emoji;
+import net.dv8tion.jda.api.interactions.components.Button;
+import net.dv8tion.jda.api.interactions.components.ButtonStyle;
+
+@Data
+@AllArgsConstructor
+public class DiscordRole {
+
+ private String emoji;
+ private String label;
+ private String roleId;
+
+ public Button toButton() {
+ return Button.of(ButtonStyle.SECONDARY, roleId, label, Emoji.fromUnicode(emoji));
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/config/DiscordRulesLink.java b/src/de/steamwar/bungeecore/bot/config/DiscordRulesLink.java
new file mode 100644
index 00000000..4f2ebe01
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/config/DiscordRulesLink.java
@@ -0,0 +1,36 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import net.dv8tion.jda.api.interactions.components.Button;
+
+@Data
+@AllArgsConstructor
+public class DiscordRulesLink {
+
+ private String label;
+ private String link;
+
+ public Button toButton() {
+ return Button.link(link, label);
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/config/DiscordTicketType.java b/src/de/steamwar/bungeecore/bot/config/DiscordTicketType.java
new file mode 100644
index 00000000..5ba4c654
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/config/DiscordTicketType.java
@@ -0,0 +1,41 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.config;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import net.dv8tion.jda.api.entities.Emoji;
+import net.dv8tion.jda.api.interactions.components.Button;
+import net.dv8tion.jda.api.interactions.components.ButtonStyle;
+
+@Data
+@AllArgsConstructor
+public class DiscordTicketType {
+
+ private String key;
+ private String emoji;
+ private String label;
+ private String color;
+ private String preMessage;
+
+ public Button toButton() {
+ return Button.of(ButtonStyle.valueOf(color), key, Emoji.fromUnicode(emoji)).withLabel(label);
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/config/SteamwarDiscordBotConfig.java b/src/de/steamwar/bungeecore/bot/config/SteamwarDiscordBotConfig.java
new file mode 100644
index 00000000..14f1efbd
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/config/SteamwarDiscordBotConfig.java
@@ -0,0 +1,108 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.config;
+
+import de.steamwar.bungeecore.sql.UserGroup;
+import net.md_5.bungee.config.Configuration;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class SteamwarDiscordBotConfig {
+
+ public static String TOKEN;
+ public static String GUILD;
+ public static String ANNOUNCEMENTS_CHANNEL;
+ public static String EVENTS_CHANNEL;
+ public static String ROLES_CHANNEL;
+ public static String ROLES_BASE_MESSAGE;
+ public static String ROLES_ADDED;
+ public static String ROLES_REMOVED;
+ public static List ROLES;
+ public static String RULES_CHANNEL;
+ public static String RULES_TITLE;
+ public static List RULES_RULES;
+ public static List RULES_LINKS;
+ public static String TICKET_CATEGORY;
+ public static String TICKET_CHANNEL;
+ public static String TICKET_MESSAGE;
+ public static String TICKET_CREATED;
+ public static String TICKET_LOG;
+ public static Map TICKET_TYPES;
+ public static Map RANKS;
+
+ public static void loadConfig(Configuration config) {
+ TOKEN = config.getString("token");
+ GUILD = config.getString("guild");
+ ANNOUNCEMENTS_CHANNEL = config.getString("announcements-channel");
+ EVENTS_CHANNEL = config.getString("events-channel");
+ Configuration rolesSection = config.getSection("roles-claim");
+ ROLES_CHANNEL = rolesSection.getString("channel");
+ ROLES_BASE_MESSAGE = rolesSection.getString("base");
+ ROLES_ADDED = rolesSection.getString("added");
+ ROLES_REMOVED = rolesSection.getString("removed");
+ ROLES = new ArrayList<>();
+
+ for (String roles : rolesSection.getSection("roles").getKeys()) {
+ Configuration role = rolesSection.getSection("roles").getSection(roles);
+ ROLES.add(new DiscordRole(role.getString("emoji"),
+ role.getString("label"),
+ role.getString("roleId")));
+ }
+
+ Configuration rulesSection = config.getSection("rules");
+ RULES_CHANNEL = rulesSection.getString("channel");
+ RULES_TITLE = rulesSection.getString("title");
+ RULES_RULES = rulesSection.getStringList("rules");
+
+ RULES_LINKS = new ArrayList<>();
+
+ for (String links : rulesSection.getSection("links").getKeys()) {
+ Configuration link = rulesSection.getSection("links").getSection(links);
+ RULES_LINKS.add(new DiscordRulesLink(link.getString("label"),
+ link.getString("url")));
+ }
+
+ Configuration ticketSection = config.getSection("tickets");
+ TICKET_CATEGORY = ticketSection.getString("category");
+ TICKET_CHANNEL = ticketSection.getString("channel");
+ TICKET_MESSAGE = ticketSection.getString("message");
+ TICKET_CREATED = ticketSection.getString("created");
+ TICKET_LOG = ticketSection.getString("log");
+ TICKET_TYPES = new HashMap<>();
+
+ for (String types : ticketSection.getSection("types").getKeys()) {
+ Configuration type = ticketSection.getSection("types").getSection(types);
+ TICKET_TYPES.put(types, new DiscordTicketType(types,
+ type.getString("emoji"),
+ type.getString("label"),
+ type.getString("color"),
+ type.getString("pre")));
+ }
+
+ RANKS = new HashMap<>();
+ Configuration ranksSections = config.getSection("ranks");
+ for (String type : ranksSections.getKeys()) {
+ RANKS.put(UserGroup.getUsergroup(type), ranksSections.getString(type));
+ }
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/events/EventManager.java b/src/de/steamwar/bungeecore/bot/events/EventManager.java
new file mode 100644
index 00000000..53daa79e
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/events/EventManager.java
@@ -0,0 +1,128 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.events;
+
+import de.steamwar.bungeecore.bot.SteamwarDiscordBot;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import de.steamwar.bungeecore.sql.Event;
+import de.steamwar.bungeecore.sql.EventFight;
+import de.steamwar.bungeecore.sql.Team;
+import de.steamwar.bungeecore.sql.TeamTeilnahme;
+import lombok.experimental.UtilityClass;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.MessageBuilder;
+import net.dv8tion.jda.api.entities.Message;
+import net.dv8tion.jda.api.entities.TextChannel;
+
+import java.awt.*;
+import java.sql.Timestamp;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.stream.Collectors;
+
+@UtilityClass
+public class EventManager {
+
+ private Message message;
+ private TextChannel textChannel;
+
+ static {
+ textChannel = SteamwarDiscordBot.instance().getJda().getGuildById(SteamwarDiscordBotConfig.GUILD).getTextChannelById(SteamwarDiscordBotConfig.EVENTS_CHANNEL);
+ assert textChannel != null;
+ if(textChannel.hasLatestMessage()) {
+ textChannel.getIterableHistory().complete().forEach(message -> message.delete().complete());
+ }
+ }
+
+ public void update() {
+ if (Event.get() == null) {
+ updateComing();
+ } else {
+ updateCurrent();
+ }
+ }
+
+ private void updateComing() {
+ EmbedBuilder embedBuilder = new EmbedBuilder();
+ embedBuilder.setColor(Color.GRAY);
+ embedBuilder.setTitle("Zukünftige Events");
+ embedBuilder.setAuthor("SteamWar", "https://www.steamwar.de");
+
+ Timestamp now = Timestamp.from(Instant.now());
+ Event.getComing().forEach(event -> {
+ StringBuilder st = new StringBuilder();
+ if (event.getDeadline().after(now)) {
+ st.append("Deadline: \n");
+ }
+ st.append("Start: ");
+ String teilname = TeamTeilnahme.getTeams(event.getEventID()).stream().map(Team::getTeamKuerzel).collect(Collectors.joining(", "));
+ if (!teilname.isEmpty()) {
+ st.append("\nAngemeldete Teams: ").append(teilname);
+ }
+ embedBuilder.addField(event.getEventName(), st.toString(), false);
+ });
+
+ MessageBuilder messageBuilder = new MessageBuilder();
+ messageBuilder.setEmbeds(embedBuilder.build());
+ if (message == null) {
+ message = textChannel.sendMessage(messageBuilder.build()).complete();
+ } else {
+ message.editMessage(messageBuilder.build()).complete();
+ }
+ }
+
+ private void updateCurrent() {
+ Event event = Event.get();
+ if (event == null) return;
+ EmbedBuilder embedBuilder = new EmbedBuilder();
+ embedBuilder.setColor(Color.GRAY);
+ embedBuilder.setTitle("Event: " + event.getEventName());
+ embedBuilder.setAuthor("SteamWar", "https://www.steamwar.de");
+
+ Instant now = Instant.now();
+ EventFight.getEvent(event.getEventID()).forEach(eventFight -> {
+ Team teamBlue = Team.get(eventFight.getTeamBlue());
+ Team teamRed = Team.get(eventFight.getTeamRed());
+
+ StringBuilder st = new StringBuilder();
+ st.append("Fightbeginn: ");
+ if(now.isAfter(eventFight.getStartTime().toInstant().plus(35, ChronoUnit.MINUTES))) {
+ st.append("\n");
+ if (eventFight.getErgebnis() == 1) {
+ st.append("Sieg ").append(teamBlue.getTeamKuerzel());
+ } else if (eventFight.getErgebnis() == 2) {
+ st.append("Sieg ").append(teamRed.getTeamKuerzel());
+ } else {
+ st.append("Unentschieden");
+ }
+ }
+ embedBuilder.addField(teamBlue.getTeamKuerzel() + " vs. " + teamRed.getTeamKuerzel(), st.toString(), true);
+ });
+
+ MessageBuilder messageBuilder = new MessageBuilder();
+ messageBuilder.setEmbeds(embedBuilder.build());
+ if (message == null) {
+ message = textChannel.sendMessage(messageBuilder.build()).complete();
+ } else {
+ message.editMessage(messageBuilder.build()).complete();
+ }
+ }
+
+}
diff --git a/src/de/steamwar/bungeecore/bot/listeners/AnnouncementListener.java b/src/de/steamwar/bungeecore/bot/listeners/AnnouncementListener.java
new file mode 100644
index 00000000..f3803d2c
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/listeners/AnnouncementListener.java
@@ -0,0 +1,50 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.listeners;
+
+import de.steamwar.bungeecore.Message;
+import de.steamwar.bungeecore.bot.SteamwarDiscordBot;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import net.dv8tion.jda.api.MessageBuilder;
+import net.dv8tion.jda.api.entities.TextChannel;
+import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
+import org.jetbrains.annotations.NotNull;
+
+public class AnnouncementListener extends BasicDiscordListener {
+
+ @Override
+ public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) {
+ if (!event.getChannel().getId().equals(SteamwarDiscordBotConfig.ANNOUNCEMENTS_CHANNEL)) {
+ return;
+ }
+ if (event.getAuthor().isBot()) {
+ return;
+ }
+ Message.broadcast("ALERT", event.getMessage().getContentRaw());
+ }
+
+ public void announce(String message) {
+ TextChannel textChannel = SteamwarDiscordBot.instance().getJda().getGuildById(SteamwarDiscordBotConfig.GUILD).getTextChannelById(SteamwarDiscordBotConfig.ANNOUNCEMENTS_CHANNEL);
+ assert textChannel != null;
+ MessageBuilder messageBuilder = new MessageBuilder();
+ messageBuilder.append(message.replace("&", ""));
+ textChannel.sendMessage(messageBuilder.build()).complete();
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/listeners/BasicDiscordListener.java b/src/de/steamwar/bungeecore/bot/listeners/BasicDiscordListener.java
new file mode 100644
index 00000000..1c8a5ffd
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/listeners/BasicDiscordListener.java
@@ -0,0 +1,30 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.listeners;
+
+import de.steamwar.bungeecore.bot.SteamwarDiscordBot;
+import net.dv8tion.jda.api.hooks.ListenerAdapter;
+
+public abstract class BasicDiscordListener extends ListenerAdapter {
+
+ BasicDiscordListener() {
+ SteamwarDiscordBot.instance().addListener(this);
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/listeners/DiscordAuthListener.java b/src/de/steamwar/bungeecore/bot/listeners/DiscordAuthListener.java
new file mode 100644
index 00000000..514b193f
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/listeners/DiscordAuthListener.java
@@ -0,0 +1,59 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.listeners;
+
+import de.steamwar.bungeecore.bot.AuthManager;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import de.steamwar.bungeecore.sql.SteamwarUser;
+import net.dv8tion.jda.api.entities.ChannelType;
+import net.dv8tion.jda.api.events.interaction.GenericComponentInteractionCreateEvent;
+import net.dv8tion.jda.api.interactions.InteractionType;
+import org.jetbrains.annotations.NotNull;
+
+public class DiscordAuthListener extends BasicDiscordListener {
+
+ @Override
+ public void onGenericComponentInteractionCreate(@NotNull GenericComponentInteractionCreateEvent event) {
+ if(event.getType() == InteractionType.COMPONENT) {
+ if(event.getChannel().getId().equals(SteamwarDiscordBotConfig.RULES_CHANNEL) && event.getComponentId().equals("auth")) {
+ String authMessage = AuthManager.createDiscordAuthToken(event.getMember());
+ if(authMessage != null) {
+ event.reply("Gebe innerhalb der nächsten 10 Minuten ``/verify " + authMessage + "`` auf dem Minecraft Server ein").setEphemeral(true).complete();
+ } else {
+ event.reply("Du hast bereits einen Code am laufen").setEphemeral(true).complete();
+ }
+ }
+
+ if(event.getComponentId().equals("tada") && event.getChannelType() == ChannelType.PRIVATE) {
+ event.reply(":tada:").setEphemeral(false).complete();
+ }
+
+ if(event.getComponentId().equals("invalid") && event.getChannelType() == ChannelType.PRIVATE) {
+ SteamwarUser user = SteamwarUser.get(event.getUser().getIdLong());
+ if(user == null) {
+ event.reply(":question: Da ist keine verknüpfung?").setEphemeral(false).complete();
+ } else {
+ user.setDiscordId(null);
+ event.reply(":x: Die Verknüpfung wurde beendet").setEphemeral(false).complete();
+ }
+ }
+ }
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/listeners/DiscordTicketListener.java b/src/de/steamwar/bungeecore/bot/listeners/DiscordTicketListener.java
new file mode 100644
index 00000000..7c18c6fb
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/listeners/DiscordTicketListener.java
@@ -0,0 +1,145 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.listeners;
+
+import de.steamwar.bungeecore.BungeeCore;
+import de.steamwar.bungeecore.Message;
+import de.steamwar.bungeecore.bot.config.DiscordTicketType;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import de.steamwar.bungeecore.sql.SteamwarUser;
+import net.dv8tion.jda.api.EmbedBuilder;
+import net.dv8tion.jda.api.MessageBuilder;
+import net.dv8tion.jda.api.Permission;
+import net.dv8tion.jda.api.entities.*;
+import net.dv8tion.jda.api.events.interaction.GenericComponentInteractionCreateEvent;
+import net.dv8tion.jda.api.events.message.guild.GuildMessageReceivedEvent;
+import net.dv8tion.jda.api.interactions.InteractionType;
+import net.dv8tion.jda.api.interactions.components.ActionRow;
+import net.dv8tion.jda.api.interactions.components.Button;
+import net.dv8tion.jda.api.interactions.components.selections.SelectionMenu;
+import net.md_5.bungee.api.chat.ClickEvent;
+import org.jetbrains.annotations.NotNull;
+
+import java.awt.Color;
+import java.time.Instant;
+import java.util.LinkedList;
+
+public class DiscordTicketListener extends BasicDiscordListener {
+
+ @Override
+ public void onGenericComponentInteractionCreate(@NotNull GenericComponentInteractionCreateEvent event) {
+ if(event.getType() == InteractionType.COMPONENT && event.getChannelType() == ChannelType.TEXT && event.getTextChannel().getParent() != null && event.getTextChannel().getParent().getId().equals(SteamwarDiscordBotConfig.TICKET_CATEGORY)) {
+ if(event.getTextChannel().getId().equals(SteamwarDiscordBotConfig.TICKET_CHANNEL) && SteamwarDiscordBotConfig.TICKET_TYPES.containsKey(event.getComponentId())) {
+ DiscordTicketType ticketType = SteamwarDiscordBotConfig.TICKET_TYPES.get(event.getComponentId());
+ Category ct = event.getGuild().getCategoryById(SteamwarDiscordBotConfig.TICKET_CATEGORY);
+ SteamwarUser swUser = SteamwarUser.get(event.getUser().getIdLong());
+ TextChannel ticketChannel = ct.createTextChannel((swUser == null?event.getUser().getName():swUser.getUserName()) + "-" + event.getComponentId() + "-" + System.currentTimeMillis() % 1000).complete();
+ ticketChannel.createPermissionOverride(event.getMember()).setAllow(Permission.VIEW_CHANNEL,
+ Permission.MESSAGE_WRITE,
+ Permission.MESSAGE_ATTACH_FILES,
+ Permission.MESSAGE_ADD_REACTION,
+ Permission.MESSAGE_READ,
+ Permission.MESSAGE_EMBED_LINKS,
+ Permission.MESSAGE_HISTORY).complete();
+ ticketChannel.getManager().setTopic(event.getUser().getId()).complete();
+
+ MessageBuilder messageBuilder = new MessageBuilder();
+ EmbedBuilder builder = new EmbedBuilder();
+ builder.setDescription(ticketType.getPreMessage());
+ builder.setTitle("Steamwar Ticket");
+ builder.setColor(Color.GREEN);
+
+ Button closeButton = Button.danger("close-" + ticketChannel.getName(), "Schliesen").withEmoji(Emoji.fromUnicode("U+26A0"));
+ messageBuilder.setEmbeds(builder.build());
+ messageBuilder.setActionRows(ActionRow.of(closeButton));
+
+ ticketChannel.sendMessage(messageBuilder.build()).complete();
+ event.reply(SteamwarDiscordBotConfig.TICKET_CREATED.replace("%channel%", ticketChannel.getAsMention())).setEphemeral(true).complete();
+ Message.team("DISCORD_TICKET_NEW", ticketChannel.getName());
+ } else if(event.getComponentId().startsWith("close-")) {
+ TextChannel logChannel = event.getGuild().getTextChannelById(SteamwarDiscordBotConfig.TICKET_LOG);
+
+ MessageBuilder builder = new MessageBuilder();
+ StringBuilder stringBuilder = new StringBuilder();
+
+ new LinkedList<>(event.getTextChannel().getIterableHistory().complete()).descendingIterator().forEachRemaining(message -> {
+ if(message.getAuthor().isSystem() || message.getAuthor().isBot()) return;
+ stringBuilder.append(" ")
+ .append("**")
+ .append(message.getAuthor().getName())
+ .append("**: ")
+ .append(message.getContentRaw());
+ if(!message.getAttachments().isEmpty()) {
+ stringBuilder.append("\n")
+ .append("Files: ").append("\n");
+ message.getAttachments().forEach(attachment -> stringBuilder.append(attachment.getUrl()).append("\n"));
+ }
+
+ stringBuilder.append("\n");
+ });
+
+ stringBuilder.append("\n");
+ stringBuilder.append(" ")
+ .append("**").append(event.getUser().getName()).append("**: ")
+ .append("Ticket geschlossen");
+
+ EmbedBuilder embedBuilder = new EmbedBuilder();
+ embedBuilder.setColor(Color.GREEN);
+ User user = event.getJDA().retrieveUserById(event.getTextChannel().getTopic()).complete();
+ SteamwarUser swuser = SteamwarUser.get(user.getIdLong());
+
+ embedBuilder.setAuthor(user.getName(), swuser==null?"https://steamwar.de/":("https://steamwar.de/users/" + swuser.getUserName().toLowerCase() + "/"), user.getAvatarUrl());
+ embedBuilder.setTimestamp(Instant.now());
+ embedBuilder.setTitle(event.getTextChannel().getName());
+ embedBuilder.setDescription(stringBuilder);
+
+ builder.setEmbeds(embedBuilder.build());
+
+ logChannel.sendMessage(builder.build()).complete();
+
+ Message.team("DISCORD_TICKET_CLOSED", event.getTextChannel().getName());
+ event.getTextChannel().delete().reason("Closed").complete();
+ }
+ }
+ }
+
+ @Override
+ public void onGuildMessageReceived(@NotNull GuildMessageReceivedEvent event) {
+ if(event.getChannel().getParent() != null && event.getChannel().getParent().getId().equals(SteamwarDiscordBotConfig.TICKET_CATEGORY)) {
+ if(!event.getChannel().getId().equals(SteamwarDiscordBotConfig.TICKET_CHANNEL) && !event.getChannel().getId().equals(SteamwarDiscordBotConfig.TICKET_LOG)) {
+ BungeeCore.get().getProxy().getPlayers().forEach(player -> {
+ if(event.getAuthor().isBot() || event.getAuthor().isSystem()) return;
+ SteamwarUser user = SteamwarUser.get(player);
+ if(user.getDiscordId() == null) {
+ if(user.getUserGroup().isTeamGroup()) {
+ Message.sendPrefixless("DISCORD_TICKET_MESSAGE", player, "Zur nachricht", new ClickEvent(ClickEvent.Action.OPEN_URL, event.getMessage().getJumpUrl()), event.getChannel().getName(), event.getAuthor().getName(), event.getMessage().getContentRaw());
+ }
+ } else {
+ if(event.getAuthor().getId().equals(user.getDiscordId())) return;
+ Member member = event.getGuild().retrieveMemberById(user.getDiscordId()).complete();
+ if(member.hasPermission(event.getChannel().getManager().getChannel(), Permission.MESSAGE_WRITE)) {
+ Message.sendPrefixless("DISCORD_TICKET_MESSAGE", player, "Zur nachricht", new ClickEvent(ClickEvent.Action.OPEN_URL, event.getMessage().getJumpUrl()), event.getChannel().getName(), event.getAuthor().getName(), event.getMessage().getContentRaw());
+ }
+ }
+ });
+ }
+ }
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/listeners/RolesInteractionButtonListener.java b/src/de/steamwar/bungeecore/bot/listeners/RolesInteractionButtonListener.java
new file mode 100644
index 00000000..035cd77c
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/listeners/RolesInteractionButtonListener.java
@@ -0,0 +1,42 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.listeners;
+
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import net.dv8tion.jda.api.entities.ChannelType;
+import net.dv8tion.jda.api.events.interaction.GenericComponentInteractionCreateEvent;
+import net.dv8tion.jda.api.interactions.InteractionType;
+import org.jetbrains.annotations.NotNull;
+
+public class RolesInteractionButtonListener extends BasicDiscordListener {
+
+ @Override
+ public void onGenericComponentInteractionCreate(@NotNull GenericComponentInteractionCreateEvent event) {
+ if(event.getType() == InteractionType.COMPONENT && event.getChannelType() == ChannelType.TEXT && event.getTextChannel().getId().equals(SteamwarDiscordBotConfig.ROLES_CHANNEL) && SteamwarDiscordBotConfig.ROLES.stream().anyMatch(discordRole -> discordRole.getRoleId().equals(event.getComponentId()))) {
+ if (event.getMember().getRoles().stream().anyMatch(role -> role.getId().equals(event.getComponentId()))) {
+ event.getGuild().removeRoleFromMember(event.getMember(), event.getGuild().getRoleById(event.getComponentId())).complete();
+ event.reply(SteamwarDiscordBotConfig.ROLES_REMOVED.replace("%role%", event.getGuild().getRoleById(event.getComponentId()).getAsMention())).setEphemeral(true).complete();
+ } else {
+ event.getGuild().addRoleToMember(event.getMember(), event.getGuild().getRoleById(event.getComponentId())).complete();
+ event.reply(SteamwarDiscordBotConfig.ROLES_ADDED.replace("%role%", event.getGuild().getRoleById(event.getComponentId()).getAsMention())).setEphemeral(true).complete();
+ }
+ }
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/util/DiscordRanks.java b/src/de/steamwar/bungeecore/bot/util/DiscordRanks.java
new file mode 100644
index 00000000..9b976cef
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/util/DiscordRanks.java
@@ -0,0 +1,67 @@
+/*
+ * This file is a part of the SteamWar software.
+ *
+ * Copyright (C) 2020 SteamWar.de-Serverteam
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.util;
+
+import de.steamwar.bungeecore.bot.SteamwarDiscordBot;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import de.steamwar.bungeecore.sql.SteamwarUser;
+import de.steamwar.bungeecore.sql.UserGroup;
+import lombok.experimental.UtilityClass;
+import net.dv8tion.jda.api.entities.Guild;
+import net.dv8tion.jda.api.entities.Member;
+import net.dv8tion.jda.api.entities.Role;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+@UtilityClass
+public class DiscordRanks {
+
+ public void update(SteamwarUser steamwarUser) {
+ if (steamwarUser.getDiscordId() == null) {
+ return;
+ }
+ Guild guild = SteamwarDiscordBot.instance().getJda().getGuildById(SteamwarDiscordBotConfig.GUILD);
+ Member member = guild.retrieveMemberById(steamwarUser.getDiscordId()).complete();
+ if (member == null) {
+ return;
+ }
+
+ List roleList = member.getRoles();
+ Set strings = new HashSet<>(SteamwarDiscordBotConfig.RANKS.values());
+ String needed = SteamwarDiscordBotConfig.RANKS.get(steamwarUser.getUserGroup());
+ for (Role role : roleList) {
+ if (!strings.contains(role.getId())) {
+ continue;
+ }
+ if (role.getId().equals(needed)) {
+ needed = "";
+ continue;
+ }
+ guild.removeRoleFromMember(member, role).complete();
+ }
+
+ if (!needed.isEmpty()) {
+ guild.addRoleToMember(member, guild.getRoleById(needed)).complete();
+ }
+ }
+}
diff --git a/src/de/steamwar/bungeecore/bot/util/DiscordRolesMessage.java b/src/de/steamwar/bungeecore/bot/util/DiscordRolesMessage.java
new file mode 100644
index 00000000..deb65ed2
--- /dev/null
+++ b/src/de/steamwar/bungeecore/bot/util/DiscordRolesMessage.java
@@ -0,0 +1,51 @@
+/*
+ This file is a part of the SteamWar software.
+
+ Copyright (C) 2020 SteamWar.de-Serverteam
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+ */
+
+package de.steamwar.bungeecore.bot.util;
+
+import de.steamwar.bungeecore.bot.SteamwarDiscordBot;
+import de.steamwar.bungeecore.bot.config.SteamwarDiscordBotConfig;
+import net.dv8tion.jda.api.MessageBuilder;
+import net.dv8tion.jda.api.entities.TextChannel;
+import net.dv8tion.jda.api.interactions.components.ActionRow;
+import net.dv8tion.jda.api.interactions.components.Button;
+import net.dv8tion.jda.api.requests.restaction.MessageAction;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class DiscordRolesMessage {
+
+ public static void sendMessage() {
+ TextChannel channel = SteamwarDiscordBot.instance().getJda().getGuildById(SteamwarDiscordBotConfig.GUILD).getTextChannelById(SteamwarDiscordBotConfig.ROLES_CHANNEL);
+ assert channel != null;
+ if(channel.hasLatestMessage()) {
+ channel.getIterableHistory().complete().forEach(message -> message.delete().complete());
+ }
+
+ MessageBuilder builder = new MessageBuilder();
+ builder.setContent(SteamwarDiscordBotConfig.ROLES_BASE_MESSAGE);
+ List