Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,6 @@ dependency-reduced-pom.xml

#run dirs
/fabric/versions/*/run

# Compound-engineering / agent review artifacts
.context/
700 changes: 700 additions & 0 deletions UPGRADE.md

Large diffs are not rendered by default.

52 changes: 52 additions & 0 deletions api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import com.vanniktech.maven.publish.JavaLibrary
import com.vanniktech.maven.publish.JavadocJar

plugins {
`java-library`
id("com.vanniktech.maven.publish")
}

applyPlatformAndCoreConfiguration()

mavenPublishing {
publishToMavenCentral()
if (project.hasProperty("signingInMemoryKey")) {
signAllPublications()
}

configure(JavaLibrary(
javadocJar = JavadocJar.Javadoc(),
sourcesJar = true
))

pom {
name.set("BanManagerAPI")
description.set("Stable, dependency-light public API for BanManager v8+. Plugin authors integrate against this artifact.")
url.set("https://github.com/BanManagement/BanManager/")
licenses {
license {
name.set("Creative Commons Attribution-NonCommercial-ShareAlike 2.0 UK: England & Wales")
url.set("https://github.com/BanManagement/BanManager/blob/master/LICENCE")
}
}
developers {
developer {
id.set("confuser")
name.set("James Mortemore")
email.set("jamesmortemore@gmail.com")
}
}
scm {
connection.set("scm:git:git://github.com/BanManagement/BanManager.git")
developerConnection.set("scm:git:ssh://git@github.com/BanManagement/BanManager.git")
url.set("https://github.com/BanManagement/BanManager/")
}
}
}

dependencies {
// The single non-JDK dependency. Unshaded so the IPAddress type the API
// exposes carries the canonical inet.ipaddr.* package, not the BM-shaded
// me.confuser.banmanager.common.ipaddr.* prefix.
api("com.github.seancfoley:ipaddress:5.5.1")
}
79 changes: 79 additions & 0 deletions api/src/main/java/me/confuser/banmanager/api/BanManager.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package me.confuser.banmanager.api;

import java.util.concurrent.atomic.AtomicReference;

/**
* Static locator for the platform's {@link BanManagerService} instance.
*
* <p>This works on every platform (Bukkit, Bungee, Velocity, Sponge, Fabric)
* because the BanManager bootstrap calls {@link #set(BanManagerService)}
* during plugin enable, so {@code BanManager.get()} is the portable
* resolution path. On Bukkit you may alternatively use
* {@code Bukkit.getServicesManager().load(BanManagerService.class)} — the
* other platforms have no plugin-extensible service manager.</p>
*
* <h2>Resolution</h2>
* <ol>
* <li>If {@link #set(BanManagerService)} has been called, the registered
* instance is returned. BanManager publishes itself this way on every
* platform during plugin enable.</li>
* <li>Otherwise {@link IllegalStateException} is thrown — there is no
* {@link java.util.ServiceLoader} fallback. {@code META-INF/services}
* discovery would only resolve consumer-side stubs, never the running
* plugin (whose classloader is invisible to consumer plugins), so it
* was a footgun and is intentionally absent.</li>
* </ol>
*
* <h2>Tests</h2>
* Unit tests should construct a service implementation (or test double)
* directly and call {@link #set(BanManagerService)} in {@code @BeforeEach},
* then {@link #clear()} in {@code @AfterEach}.
*/
public final class BanManager {

private static final AtomicReference<BanManagerService> INSTANCE = new AtomicReference<>();

private BanManager() {}

/**
* @return the active service instance
* @throws IllegalStateException when BanManager has not finished enabling
* yet, or when running outside a BanManager
* environment
*/
public static BanManagerService get() {
BanManagerService current = INSTANCE.get();
if (current != null) return current;
throw new IllegalStateException(
"BanManagerService has not been registered. Either BanManager hasn't enabled yet"
+ " or you're running outside a server with the BanManager plugin installed.");
}

/**
* Register the active service. Called once by the platform plugin on
* enable. {@code /bmreload} mutates the existing service in place rather
* than re-publishing — consumer plugins should subscribe to
* {@link me.confuser.banmanager.api.event.player.PluginReloadedEvent} to
* re-register listeners after a reload, not poll {@link #get()} for a
* fresh reference. Calls during a normal disable/enable cycle replace the
* previous instance.
*/
public static void set(BanManagerService service) {
INSTANCE.set(service);
}

/**
* Clear the registered service. Called on plugin disable to avoid
* retaining classloaders.
*/
public static void clear() {
INSTANCE.set(null);
}

/**
* @return {@code true} when {@link #get()} would succeed
*/
public static boolean isAvailable() {
return INSTANCE.get() != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package me.confuser.banmanager.api;

import me.confuser.banmanager.api.database.DatabaseAccess;
import me.confuser.banmanager.api.database.MigrationService;
import me.confuser.banmanager.api.event.EventBus;
import me.confuser.banmanager.api.scheduler.BanManagerScheduler;
import me.confuser.banmanager.api.service.BanService;
import me.confuser.banmanager.api.service.HistoryService;
import me.confuser.banmanager.api.service.IpBanService;
import me.confuser.banmanager.api.service.IpMuteService;
import me.confuser.banmanager.api.service.IpRangeBanService;
import me.confuser.banmanager.api.service.MuteService;
import me.confuser.banmanager.api.service.NameBanService;
import me.confuser.banmanager.api.service.NoteService;
import me.confuser.banmanager.api.service.PlayerService;
import me.confuser.banmanager.api.service.ReportService;
import me.confuser.banmanager.api.service.WarnService;

/**
* Root entry point for the BanManager API. Resolve via either
* {@link BanManager#get()} (works everywhere) or, on Bukkit, the platform's
* native services manager:
*
* <pre>{@code
* // Portable across Bukkit, Bungee, Velocity, Sponge, Fabric:
* BanManagerService bm = BanManager.get();
*
* // Bukkit also publishes the service via the Bukkit ServicesManager:
* BanManagerService bm = Bukkit.getServicesManager().load(BanManagerService.class);
* }</pre>
*
* <p>Velocity, Sponge, Bungee and Fabric have no plugin-extensible service
* manager that fits this use case, so {@link BanManager#get()} is the
* recommended path on those platforms.</p>
*
* <p>Subservices are stable references — fetch them once at startup and reuse.</p>
*/
public interface BanManagerService {

PlayerService players();

BanService bans();

MuteService mutes();

WarnService warnings();

IpBanService ipBans();

IpMuteService ipMutes();

IpRangeBanService ipRangeBans();

NameBanService nameBans();

NoteService notes();

ReportService reports();

HistoryService history();

EventBus events();

DatabaseAccess database();

BanManagerScheduler scheduler();

MigrationService migrations();
}
52 changes: 52 additions & 0 deletions api/src/main/java/me/confuser/banmanager/api/Page.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package me.confuser.banmanager.api;

import java.util.List;
import java.util.Objects;

/**
* Immutable page of results. Replaces every {@code CloseableIterator<T>}
* return on the public API surface so consumers never have to remember to
* {@code close()}.
*
* @param items the items in this page (never {@code null})
* @param page the zero-indexed page number this {@code Page} represents
* (must be {@code >= 0})
* @param size the requested page size (must be {@code > 0}); always reflects
* what the caller asked for, never the number of items actually
* returned
* @param total the total number of records matching the query, or {@code -1}
* when the storage layer cannot cheaply compute it
* @param <T> element type
*/
public record Page<T>(List<T> items, int page, int size, long total) {

public Page {
Objects.requireNonNull(items, "items");
if (page < 0) throw new IllegalArgumentException("page must be >= 0");
if (size <= 0) throw new IllegalArgumentException("size must be > 0");
items = List.copyOf(items);
}

/**
* @return {@code true} if a {@link #page() page+1} call may yield more results
*/
public boolean hasMore() {
if (total < 0) {
return items.size() == size;
}
return ((long) (page + 1) * size) < total;
}

/**
* Empty page that preserves the caller's pagination request. Use when a
* lookup precondition is not satisfied (e.g. unknown player) — the
* {@code page}/{@code size} round-trip lets clients render the correct
* pager UI without re-asking.
*
* @param page zero-indexed page number that was requested
* @param size page size that was requested ({@code > 0})
*/
public static <T> Page<T> empty(int page, int size) {
return new Page<>(List.of(), page, size, 0L);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package me.confuser.banmanager.api.database;

import javax.sql.DataSource;

import java.util.Optional;

/**
* Direct access to the {@link DataSource}s that back BanManager. Useful for
* companion plugins that need to issue custom queries against BanManager's
* tables (e.g. BanManager-WebEnhancer for its forum-specific reporting
* queries) or that ship their own tables in the same database (typically
* created via {@link MigrationService}).
*
* <h2>Privilege scope</h2>
* <p>The returned pools are <strong>not sandboxed</strong>. They expose the
* same database privileges as BanManager itself — typically full
* {@code SELECT/INSERT/UPDATE/DELETE/DDL} on the configured database — so a
* misbehaving consumer can mutate {@code bm_*} tables out from under the
* service layer. Treat them like raw JDBC:</p>
*
* <ul>
* <li><b>Reads against any table</b> — safe.</li>
* <li><b>Writes against tables your plugin owns</b> (typically created via
* {@link MigrationService}) — safe.</li>
* <li><b>Writes against {@code bm_*} tables</b> — <b>do not</b>. Use the
* {@code BanManagerService} sub-services instead so the cache layer,
* event bus, and global-sync replication stay coherent.</li>
* </ul>
*
* <p><b>Do not close the returned {@link DataSource} instances</b>; they are
* owned by BanManager and shared across the JVM. Closing one shuts the pool
* for every consumer (including BanManager itself).</p>
*
* <h2>Table name lookup</h2>
* <p>BanManager allows operators to rename individual tables in
* {@code config.yml} (e.g. for WordPress-style {@code wp_} prefixes); use
* {@link #localTable(String)} / {@link #globalTable(String)} to resolve a
* logical key like {@code "playerBans"} to the configured SQL table name.
* The full list of logical keys is documented under {@code config.yml ->
* databases.local.tables}.</p>
*/
public interface DatabaseAccess {

/**
* @return the local (single-server) database that BanManager always uses
*/
DataSource localDataSource();

/**
* @return the optional global database, when the operator has configured
* cross-server sync. Empty otherwise.
*/
Optional<DataSource> globalDataSource();

/**
* Resolve a logical table key (e.g. {@code "players"}, {@code "playerBans"})
* to the configured SQL table name in the local database.
*
* @return the SQL table name, or empty when the key is unknown
*/
Optional<String> localTable(String logicalName);

/**
* Same as {@link #localTable(String)} but for the global database.
*/
Optional<String> globalTable(String logicalName);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package me.confuser.banmanager.api.database;

/**
* Logical identifier for one of BanManager's two database connection
* pools. Used wherever the API needs the caller to name a database
* without exposing the underlying {@link javax.sql.DataSource} (which
* would invite reference-equality bugs when callers pass wrapped or
* proxied instances).
*/
public enum DatabaseKind {

/**
* The local database. Always present — every BanManager install has a
* local database for player records and history.
*/
LOCAL,

/**
* The optional global database. Only present when the operator has
* configured cross-server sharing; otherwise
* {@link DatabaseAccess#globalDataSource()} returns
* {@link java.util.Optional#empty()}.
*/
GLOBAL
}
Loading
Loading