Remove reaction boss bar; fix death coords; F-tribute head
- Reactions: drop the purple boss bar that sat on screen for the whole reaction window (curiosidades, adivinha, luto [F], /ia answers). Live counts still ride on the reactor's action bar and the closing tally line. - mortes: death coordinates were sent during PlayerDeathEvent and the Java death screen swallowed them. Capture at death, deliver on PlayerRespawnEvent (1 tick later) so the player actually receives them. Respects keepInventory. - luto: pressing F to pay respects now drops the dead player's head into the mourner's inventory — once per mourner per death, never to the dead player. First gameplay-touching feature; toggle with luto.cabeca (default on). - Fix the /f typed twin: it fell through to help because "f" is not a configured reaction (the mourning set is hardcoded). Route it to reactLatest so Bedrock players can pay respects too. - README + config updated. 117 tests green. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -2,9 +2,13 @@
|
||||
|
||||
Chat-only social features for the Canalhandia Minecraft server (Paper 26.2).
|
||||
|
||||
**Nothing here touches gameplay.** No items, no world edits, no attributes, no
|
||||
economy. Everything is chat messages, boss bars and clickable buttons, and every
|
||||
module can be switched off independently.
|
||||
**Almost nothing here touches gameplay** — no world edits, no attributes, no
|
||||
economy, and no items anywhere except one: the `luto` tribute. Pressing F to pay
|
||||
respects drops the dead player's head into the mourner's inventory as a symbolic
|
||||
memento (toggle: `luto.cabeca`). Everything else is chat messages and clickable
|
||||
buttons, and every module can be switched off independently. (The reaction-count
|
||||
boss bar was removed; live counts ride on the reactor's action bar and a closing
|
||||
tally line.)
|
||||
|
||||
All player-facing text is Portuguese (pt-BR).
|
||||
|
||||
@@ -16,11 +20,11 @@ All player-facing text is Portuguese (pt-BR).
|
||||
|---|---|
|
||||
| `curiosidades` | *"Sabia que o Fulano já minerou 5.966 blocos de Pedra?"* — a fact about a player, with reaction buttons. Fires on join by default. |
|
||||
| `adivinha` | The same fact with the name hidden, plus clickable player names. Reveals after 45s and names who guessed right. |
|
||||
| `luto` | A clickable `[F]` under each death message, with a count when the window closes. |
|
||||
| `luto` | A clickable `[F]` under each death message, with a count when the window closes. Pressing F drops the **dead player's head** into the mourner's inventory (once per mourner per death, never to the dead player themselves) — the one gameplay-touching feature; toggle with `luto.cabeca`. Bedrock types `/f`. |
|
||||
| `enquete` | `/enquete Pergunta \| A \| B` — clickable voting with a live tally on the boss bar. |
|
||||
| `ranking` | `/ranking mineracao` and friends. Covers **offline players too**. |
|
||||
| `marcos` | Announces round milestones — 100 km walked, 24 hours played — the first time someone crosses one. |
|
||||
| `mortes` | Replaces each death message with a comic pt-BR line (cause-based flavor + a death counter) and sends the death coordinates **privately** to the dead player, so they can run back to their dropped items. No storage, no command. |
|
||||
| `mortes` | Replaces each death message with a comic pt-BR line (cause-based flavor + a death counter) and sends the death coordinates **privately** to the dead player on respawn (the death screen swallows chat sent during the event), so they can run back to their dropped items. Respects `keepInventory`. No storage, no command. |
|
||||
| `ia` | `/ia <pergunta>` asks an OpenAI-compatible model in chat. Optional wiki grounding, per-player memory, operator corrections. |
|
||||
|
||||
Toggle any of them: `/canalhandia modulo <nome> <on|off>`
|
||||
@@ -52,14 +56,14 @@ These shaped the design, and anyone changing the code should know them before
|
||||
|
||||
There is no vanilla way to update a message that is already in the chat log. So
|
||||
the counts baked into the reaction buttons are **frozen at send time** and never
|
||||
change. The live numbers appear on three other surfaces instead:
|
||||
change. The live numbers appear on two other surfaces instead:
|
||||
|
||||
- a **boss bar** while the window is open (`janela-reacao-segundos`, default 90)
|
||||
- an **action bar** shown to whoever just reacted
|
||||
- a **final tally line** broadcast when the window closes
|
||||
|
||||
An earlier version only had the boss bar, and it read as broken — the buttons
|
||||
showed no number at all.
|
||||
(A boss bar spanning the whole reaction window was tried and removed — it sat
|
||||
on screen for `janela-reacao-segundos` and read as clutter, and the two
|
||||
surfaces above already carry the counts.)
|
||||
|
||||
### 2. Who reacted, without filling the screen
|
||||
|
||||
|
||||
@@ -6,7 +6,9 @@ import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import net.kyori.adventure.text.format.TextDecoration;
|
||||
import net.kyori.adventure.text.serializer.plain.PlainTextComponentSerializer;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameRule;
|
||||
import org.bukkit.Location;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.NamespacedKey;
|
||||
import org.bukkit.Statistic;
|
||||
import org.bukkit.entity.Entity;
|
||||
@@ -17,6 +19,9 @@ import org.bukkit.event.entity.EntityDamageByEntityEvent;
|
||||
import org.bukkit.event.entity.EntityDamageEvent;
|
||||
import org.bukkit.event.entity.PlayerDeathEvent;
|
||||
import org.bukkit.event.player.PlayerJoinEvent;
|
||||
import org.bukkit.event.player.PlayerRespawnEvent;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.SkullMeta;
|
||||
import org.bukkit.persistence.PersistentDataType;
|
||||
import org.bukkit.plugin.java.JavaPlugin;
|
||||
import org.bukkit.scheduler.BukkitTask;
|
||||
@@ -28,6 +33,8 @@ import java.util.Deque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
@@ -35,8 +42,11 @@ import java.util.UUID;
|
||||
* Chat-only social features for the Canalhandia server: curiosities, a guess
|
||||
* game, polls, mourning reactions, milestones and rankings.
|
||||
*
|
||||
* <p>Nothing here touches gameplay — no items, no world edits, no attributes.
|
||||
* Every module can be switched off independently.
|
||||
* <p>Almost nothing here touches gameplay — no items, no world edits, no
|
||||
* attributes. The one exception is the {@code luto} tribute: pressing F to pay
|
||||
* respects drops the dead player's head into the mourner's inventory, a symbolic
|
||||
* memento. Toggle it with {@code luto.cabeca} in config. Every module can be
|
||||
* switched off independently.
|
||||
*/
|
||||
public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
|
||||
@@ -45,6 +55,10 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
private final Deque<String> recentFacts = new ArrayDeque<>();
|
||||
/** Recent reaction sets, newest last, so late clicks still land. */
|
||||
private final Deque<Reactions> reactionHistory = new ArrayDeque<>();
|
||||
/** Mourning tribute per reaction id: who died, and who already got the head. */
|
||||
private final Map<Integer, Tribute> tributes = new ConcurrentHashMap<>();
|
||||
/** Death coords awaiting delivery on the player's next respawn (see onDeathComic). */
|
||||
private final Map<UUID, DeathCoords> pendingDeathCoords = new ConcurrentHashMap<>();
|
||||
|
||||
private Settings settings;
|
||||
private OfflineStats offlineStats;
|
||||
@@ -110,9 +124,6 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
if (liveReactions != null) {
|
||||
liveReactions.hide();
|
||||
}
|
||||
if (poll != null) {
|
||||
poll.hide();
|
||||
}
|
||||
@@ -293,10 +304,8 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
Reactions reactions = new Reactions(nextId++, settings.reactions());
|
||||
liveReactions = reactions;
|
||||
remember(reactions);
|
||||
reactions.show();
|
||||
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
reactions.hide();
|
||||
if (liveReactions == reactions) {
|
||||
liveReactions = null;
|
||||
}
|
||||
@@ -327,9 +336,7 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
new ReactionDef("errado", "[❌]", "[ERRADO]", "errado")));
|
||||
liveReactions = reactions;
|
||||
remember(reactions);
|
||||
reactions.show();
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
reactions.hide();
|
||||
if (liveReactions == reactions) {
|
||||
liveReactions = null;
|
||||
}
|
||||
@@ -360,14 +367,15 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
private void remember(Reactions reactions) {
|
||||
reactionHistory.addLast(reactions);
|
||||
while (reactionHistory.size() > 8) {
|
||||
reactionHistory.removeFirst();
|
||||
Reactions oldest = reactionHistory.removeFirst();
|
||||
tributes.remove(oldest.id());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a reaction set that is still accepting clicks. The boss bar only
|
||||
* lasts {@code janela-reacao-segundos}, but people scroll back and click
|
||||
* minutes later, so clicks stay valid for {@code reacao-validade-minutos}.
|
||||
* Finds a reaction set that is still accepting clicks. The reaction window
|
||||
* closes after {@code janela-reacao-segundos}, but people scroll back and
|
||||
* click minutes later, so clicks stay valid for {@code reacao-validade-minutos}.
|
||||
*/
|
||||
Reactions findReactions(int id) {
|
||||
long limit = settings.reactionValidityMinutes() * 60_000L;
|
||||
@@ -454,9 +462,6 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
@EventHandler
|
||||
public void onJoin(PlayerJoinEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
if (liveReactions != null) {
|
||||
liveReactions.showTo(player);
|
||||
}
|
||||
if (poll != null) {
|
||||
poll.showTo(player);
|
||||
}
|
||||
@@ -483,8 +488,10 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
List.of(new ReactionDef("f", "[F]", "[F]", "f")));
|
||||
liveReactions = mourning;
|
||||
remember(mourning);
|
||||
mourning.show();
|
||||
String name = event.getEntity().getName();
|
||||
if (settings.lutoHeadReward()) {
|
||||
tributes.put(mourning.id(), new Tribute(event.getEntity().getUniqueId(), name));
|
||||
}
|
||||
|
||||
// One tick later so it prints under the vanilla death message.
|
||||
getServer().getScheduler().runTaskLater(this, () -> broadcastPerPlatform(bedrock -> {
|
||||
@@ -501,7 +508,6 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
}), 2L);
|
||||
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
mourning.hide();
|
||||
if (mourning.hasAnyVote()) {
|
||||
// One line, names truncated, so a busy death does not fill the screen.
|
||||
List<String> who = mourning.names("f");
|
||||
@@ -555,21 +561,90 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
+ DeathFlavor.ordinal(shown) + " morte)", NamedTextColor.YELLOW));
|
||||
|
||||
// Private coords to the dead player only — never broadcast, so others
|
||||
// don't learn where to loot. Java: clickable copy; Bedrock: plain text.
|
||||
// don't learn where to loot. Delivered on respawn (not at death): the
|
||||
// Java death screen swallows chat sent during PlayerDeathEvent, so
|
||||
// sending it then quietly failed. Java: clickable copy; Bedrock: plain.
|
||||
Location loc = player.getLocation();
|
||||
String coords = loc.getBlockX() + " " + loc.getBlockY() + " " + loc.getBlockZ()
|
||||
+ " (" + loc.getWorld().getName() + ")";
|
||||
Component coordsMsg;
|
||||
if (Platform.isBedrock(player)) {
|
||||
coordsMsg = Component.text("Você morreu em " + coords + ". Corre buscar seus itens!",
|
||||
NamedTextColor.AQUA);
|
||||
} else {
|
||||
coordsMsg = Component.text("Você morreu em ", NamedTextColor.AQUA)
|
||||
.append(Component.text(coords, NamedTextColor.WHITE)
|
||||
.clickEvent(ClickEvent.copyToClipboard(coords)))
|
||||
.append(Component.text(". Corre buscar seus itens!", NamedTextColor.AQUA));
|
||||
boolean keepInventory = Boolean.TRUE.equals(
|
||||
loc.getWorld().getGameRuleValue(GameRule.KEEP_INVENTORY));
|
||||
pendingDeathCoords.put(player.getUniqueId(), new DeathCoords(coords, keepInventory));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the death coordinates once the player has actually respawned and
|
||||
* can act on them. The death screen ate the message when it was sent
|
||||
* synchronously during {@link PlayerDeathEvent}.
|
||||
*/
|
||||
@EventHandler
|
||||
public void onRespawn(PlayerRespawnEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
DeathCoords dc = pendingDeathCoords.remove(player.getUniqueId());
|
||||
if (dc == null) {
|
||||
return;
|
||||
}
|
||||
player.sendMessage(coordsMsg);
|
||||
String tail = dc.keepInventory() ? "" : ". Corre buscar seus itens!";
|
||||
getServer().getScheduler().runTaskLater(this, () -> {
|
||||
Component msg;
|
||||
if (Platform.isBedrock(player)) {
|
||||
msg = Component.text("Você morreu em " + dc.coords() + tail, NamedTextColor.AQUA);
|
||||
} else {
|
||||
msg = Component.text("Você morreu em ", NamedTextColor.AQUA)
|
||||
.append(Component.text(dc.coords(), NamedTextColor.WHITE)
|
||||
.clickEvent(ClickEvent.copyToClipboard(dc.coords())))
|
||||
.append(Component.text(tail, NamedTextColor.AQUA));
|
||||
}
|
||||
player.sendMessage(msg);
|
||||
}, 1L);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called after any successful reaction. For the mourning {@code f} reaction
|
||||
* this drops the dead player's head into the mourner's inventory — once per
|
||||
* mourner per death, and never to the dead player themselves.
|
||||
*/
|
||||
void afterReact(Player mourner, int reactionId, String key) {
|
||||
if (!"f".equals(key)) {
|
||||
return;
|
||||
}
|
||||
Tribute tribute = tributes.get(reactionId);
|
||||
if (tribute == null) {
|
||||
return;
|
||||
}
|
||||
if (mourner.getUniqueId().equals(tribute.deadId)) {
|
||||
return;
|
||||
}
|
||||
if (!tribute.rewarded.add(mourner.getUniqueId())) {
|
||||
return; // already got the head for this death
|
||||
}
|
||||
ItemStack head = new ItemStack(Material.PLAYER_HEAD);
|
||||
head.editMeta(SkullMeta.class, m -> {
|
||||
m.setPlayerProfile(Bukkit.createProfile(tribute.deadId, tribute.deadName));
|
||||
m.displayName(Component.text("Cabeça de " + tribute.deadName, NamedTextColor.GOLD));
|
||||
});
|
||||
for (ItemStack overflow : mourner.getInventory().addItem(head).values()) {
|
||||
mourner.getWorld().dropItemNaturally(mourner.getLocation(), overflow);
|
||||
}
|
||||
mourner.sendMessage(Component.text(
|
||||
"Você prestou luto e levou a cabeça de " + tribute.deadName + ".",
|
||||
NamedTextColor.GOLD));
|
||||
}
|
||||
|
||||
/** Who died for a mourning reaction set, and who has already been rewarded. */
|
||||
private static final class Tribute {
|
||||
final UUID deadId;
|
||||
final String deadName;
|
||||
final Set<UUID> rewarded = ConcurrentHashMap.newKeySet();
|
||||
|
||||
Tribute(UUID deadId, String deadName) {
|
||||
this.deadId = deadId;
|
||||
this.deadName = deadName;
|
||||
}
|
||||
}
|
||||
|
||||
/** Death location captured at death, delivered at respawn. */
|
||||
private record DeathCoords(String coords, boolean keepInventory) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -581,6 +656,9 @@ public final class Canalhandia extends JavaPlugin implements Listener {
|
||||
if (ai != null) {
|
||||
ai.conversations().forget(event.getPlayer().getUniqueId());
|
||||
}
|
||||
// Quitting on the death screen means no respawn fires for this death;
|
||||
// drop the pending coords so they never deliver stale next session.
|
||||
pendingDeathCoords.remove(event.getPlayer().getUniqueId());
|
||||
}
|
||||
|
||||
// --- per-player opt out -------------------------------------------------
|
||||
|
||||
@@ -49,6 +49,11 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
case "reacoes" -> whoReacted(sender);
|
||||
case "ia" -> ia(sender, args, false);
|
||||
case "iap" -> ia(sender, args, true);
|
||||
// Typed twin of the [F] mourning button — "f" is not a configured
|
||||
// reaction (the mourning set is hardcoded), so it can't be found via
|
||||
// reactionForCommand; route it directly. Acts on the latest message,
|
||||
// so it pays respects only if a mourning window is the most recent.
|
||||
case "f" -> reactLatest(sender, "f");
|
||||
default -> {
|
||||
String reaction = plugin.settings().reactionForCommand(command.getName());
|
||||
if (reaction != null) {
|
||||
@@ -397,6 +402,8 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
if (!reactions.react(player, key)) {
|
||||
Msg.error(sender, "Essa reação não vale para a última mensagem.");
|
||||
} else {
|
||||
plugin.afterReact(player, reactions.id(), key);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -489,6 +496,8 @@ final class CanalhandiaCommand implements CommandExecutor, TabCompleter {
|
||||
}
|
||||
if (!reactions.react(player, args[1])) {
|
||||
player.sendActionBar(Component.text("Reação desconhecida.", NamedTextColor.RED));
|
||||
} else {
|
||||
plugin.afterReact(player, reactions.id(), args[1]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package dev.marcospaulo.canalhandia;
|
||||
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.event.ClickEvent;
|
||||
import net.kyori.adventure.text.event.HoverEvent;
|
||||
import net.kyori.adventure.text.format.NamedTextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.entity.Player;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -18,8 +16,9 @@ import java.util.UUID;
|
||||
* Reaction state for one announced message.
|
||||
*
|
||||
* <p>Chat cannot be edited after sending, so the counts inside the buttons are
|
||||
* frozen at send time. Live numbers appear on the boss bar, on the reactor's
|
||||
* action bar, and in a one-line summary when the window closes.
|
||||
* frozen at send time. Live numbers appear on the reactor's action bar, and in
|
||||
* a one-line summary when the window closes. (A boss bar was removed on
|
||||
* request — it sat on screen for the whole reaction window and read as clutter.)
|
||||
*
|
||||
* <p>Who reacted is deliberately <em>not</em> given its own chat line — with
|
||||
* several reactions and several players that would fill the screen. Instead the
|
||||
@@ -36,15 +35,12 @@ final class Reactions {
|
||||
private final List<ReactionDef> defs;
|
||||
/** Reaction key to reactors, preserving both order and display name. */
|
||||
private final Map<String, LinkedHashMap<UUID, String>> votes = new LinkedHashMap<>();
|
||||
private final BossBar bar;
|
||||
private final long createdAt = System.currentTimeMillis();
|
||||
private boolean barVisible;
|
||||
|
||||
Reactions(int id, List<ReactionDef> defs) {
|
||||
this.id = id;
|
||||
this.defs = defs;
|
||||
defs.forEach(def -> votes.put(def.key(), new LinkedHashMap<>()));
|
||||
this.bar = BossBar.bossBar(tally(false), 1.0f, BossBar.Color.PURPLE, BossBar.Overlay.PROGRESS);
|
||||
}
|
||||
|
||||
int id() {
|
||||
@@ -72,9 +68,6 @@ final class Reactions {
|
||||
votes.values().forEach(map -> map.remove(player.getUniqueId()));
|
||||
// Names are captured now so the summary still works if someone logs off.
|
||||
votes.get(key).put(player.getUniqueId(), player.getName());
|
||||
if (barVisible) {
|
||||
bar.name(tally(false));
|
||||
}
|
||||
player.sendActionBar(tally(Platform.isBedrock(player)));
|
||||
return true;
|
||||
}
|
||||
@@ -135,7 +128,7 @@ final class Reactions {
|
||||
return text.toString();
|
||||
}
|
||||
|
||||
/** Compact live counts, for the boss bar and action bar. */
|
||||
/** Compact live counts, for the reactor's action bar. */
|
||||
Component tally(boolean bedrock) {
|
||||
Component text = Component.text("Reações: ", NamedTextColor.WHITE);
|
||||
for (ReactionDef def : defs) {
|
||||
@@ -200,21 +193,4 @@ final class Reactions {
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
void show() {
|
||||
barVisible = true;
|
||||
Bukkit.getOnlinePlayers().forEach(p -> p.showBossBar(bar));
|
||||
}
|
||||
|
||||
void hide() {
|
||||
barVisible = false;
|
||||
Bukkit.getOnlinePlayers().forEach(p -> p.hideBossBar(bar));
|
||||
}
|
||||
|
||||
/** Shows the bar to someone who joined while the window was still open. */
|
||||
void showTo(Player player) {
|
||||
if (barVisible) {
|
||||
player.showBossBar(bar);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +85,15 @@ final class Settings {
|
||||
set("reacoes-ativas", enabled);
|
||||
}
|
||||
|
||||
/** Whether pressing F to pay respects drops the dead player's head. */
|
||||
boolean lutoHeadReward() {
|
||||
return plugin.getConfig().getBoolean("luto.cabeca", true);
|
||||
}
|
||||
|
||||
void lutoHeadReward(boolean enabled) {
|
||||
set("luto.cabeca", enabled);
|
||||
}
|
||||
|
||||
/** How long the boss bar stays up. */
|
||||
int reactionWindowSeconds() {
|
||||
return Math.max(5, plugin.getConfig().getInt("janela-reacao-segundos", 90));
|
||||
|
||||
@@ -63,6 +63,12 @@ reacoes-ativas: true
|
||||
# Por quanto tempo a barra de reações fica visível, em segundos.
|
||||
janela-reacao-segundos: 90
|
||||
|
||||
# Luto: ao prestar F para alguém que morreu, o jogador recebe a cabeça daquele
|
||||
# jogador (uma lembrança simbólica). É o único recurso do plugin que mexe no
|
||||
# inventário; desligue aqui se não quiser.
|
||||
luto:
|
||||
cabeca: true
|
||||
|
||||
# Quantos nomes cabem no resumo de reações antes do resto virar "+N".
|
||||
# O resumo é sempre UMA linha, por mais gente que reaja; para ver a lista
|
||||
# completa use /reacoes (privado, não polui o chat).
|
||||
|
||||
Reference in New Issue
Block a user