init
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
# Build output
|
||||
target/
|
||||
out/
|
||||
build/
|
||||
*.class
|
||||
*.jar
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
.vscode/
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local config / secrets
|
||||
config.json.local
|
||||
*.env
|
||||
.env
|
||||
@@ -0,0 +1,2 @@
|
||||
# Social
|
||||
too much in this codebase and i'm not done with the docs or readme
|
||||
@@ -0,0 +1,94 @@
|
||||
# Overview
|
||||
|
||||
## What this plugin owns
|
||||
|
||||
Four player-facing features that talk to each other but stay logically independent:
|
||||
|
||||
- **Friends** - send/accept/reject friend requests, remove friends, see incoming/outgoing pending.
|
||||
- **Guilds** - create/disband, invite/accept/kick, leave, MOTD, fully configurable per-guild rank ladder with bitmask permissions, guild chat across the network.
|
||||
- **Direct messages** - cross-server `/msg` routing with `/r` reply-to-last-sender.
|
||||
- **Privacy** - per-player policy (`EVERYONE` / `FRIENDS` / `NOBODY`) for who can DM you, send you a friend request, or invite you to a guild.
|
||||
|
||||
DM history is **not** persisted in v1. Messages route through the MessageBus and surface in the recipient's chat; nothing is stored.
|
||||
|
||||
## Architecture in one diagram
|
||||
|
||||
```
|
||||
SocialPlugin (entry)
|
||||
+-- FriendService DB + bus + privacy enforcement
|
||||
| +-- FriendRepository
|
||||
+-- GuildService create/disband/invite/accept/leave/kick/promote/demote/MOTD/ranks
|
||||
| +-- GuildRepository
|
||||
| +-- GuildRankRepository per-guild rank ladder
|
||||
| +-- GuildMemberRepository
|
||||
| +-- GuildInviteRepository
|
||||
+-- GuildChatService cross-server guild chat, scoped per server to local members
|
||||
+-- DmRoutingService cross-server DMs + /r tracking
|
||||
+-- PrivacyService EVERYONE/FRIENDS/NOBODY policy lookup
|
||||
| +-- PrivacyRepository
|
||||
+-- ChatTargetService per-player chat-mode state (NORMAL / GUILD / DM)
|
||||
+-- NotificationDispatcher subscribes to friend/guild events, surfaces in chat to local players
|
||||
+-- SocialChatRouter intercepts PlayerChatEvent, routes per ChatTargetService
|
||||
+-- SocialDisconnectListener clears toggles + /r state on disconnect
|
||||
```
|
||||
|
||||
External dependencies (all from NetworkCore):
|
||||
- **`NetworkCore.getDatabase()`** - MySQL pool (HikariCP). **Required.**
|
||||
- **`NetworkCore.getMessageBus()`** - Redis pub/sub. **Required.**
|
||||
- **`NetworkCore.getPlayerPresence()`** - network-wide who-is-where lookup. **Required** for name->UUID resolution and DM routing.
|
||||
- **`NetworkCore.getServerId()`** - identifies the server an action was taken on.
|
||||
|
||||
If any of these is missing at `start()`, Social logs SEVERE and returns. The server keeps running; this plugin just does nothing.
|
||||
|
||||
## What lives where
|
||||
|
||||
| File | Responsibility |
|
||||
|---|---|
|
||||
| [SocialPlugin.java](../src/main/java/net/kewwbec/social/SocialPlugin.java) | Plugin entry. Reads NetworkCore services, bootstraps schema, registers everything. |
|
||||
| [SocialPermissionsNodes.java](../src/main/java/net/kewwbec/social/SocialPermissionsNodes.java) | Constants for every `networksocial.*` node. |
|
||||
| [config/SocialConfig.java](../src/main/java/net/kewwbec/social/config/SocialConfig.java) | Default config shape. |
|
||||
| [model/](../src/main/java/net/kewwbec/social/model/) | Records and enums: `Friendship`, `Guild`, `GuildRank`, `GuildMember`, `GuildInvite`, `GuildPermission` (bitmask), `PrivacyKey`, `PrivacyValue`, `PrivacySettings`. |
|
||||
| [db/SchemaBootstrap.java](../src/main/java/net/kewwbec/social/db/SchemaBootstrap.java) | `CREATE TABLE IF NOT EXISTS` for the six Social tables. |
|
||||
| [db/FriendRepository.java](../src/main/java/net/kewwbec/social/db/FriendRepository.java) | SQL for friend requests + accepted friendships. |
|
||||
| [db/GuildRepository.java](../src/main/java/net/kewwbec/social/db/GuildRepository.java) | SQL for guilds themselves. |
|
||||
| [db/GuildRankRepository.java](../src/main/java/net/kewwbec/social/db/GuildRankRepository.java) | SQL for the per-guild rank ladder. |
|
||||
| [db/GuildMemberRepository.java](../src/main/java/net/kewwbec/social/db/GuildMemberRepository.java) | SQL for guild membership (one row per player). |
|
||||
| [db/GuildInviteRepository.java](../src/main/java/net/kewwbec/social/db/GuildInviteRepository.java) | SQL for pending guild invites. |
|
||||
| [db/PrivacyRepository.java](../src/main/java/net/kewwbec/social/db/PrivacyRepository.java) | SQL for per-player privacy settings. |
|
||||
| [service/FriendService.java](../src/main/java/net/kewwbec/social/service/FriendService.java) | All friend operations + cross-server announcements. Returns a `Result` enum. |
|
||||
| [service/GuildService.java](../src/main/java/net/kewwbec/social/service/GuildService.java) | All guild operations + per-guild rank seeding + bus publishes. |
|
||||
| [service/GuildChatService.java](../src/main/java/net/kewwbec/social/service/GuildChatService.java) | Subscribes to guild chat channel; delivers locally to members of the originating guild. |
|
||||
| [service/DmRoutingService.java](../src/main/java/net/kewwbec/social/service/DmRoutingService.java) | Publishes DM payloads + per-server delivery + last-sender tracking for `/r`. |
|
||||
| [service/PrivacyService.java](../src/main/java/net/kewwbec/social/service/PrivacyService.java) | Reads policies and applies them in `canContact(from, to, key)`. |
|
||||
| [service/ChatTargetService.java](../src/main/java/net/kewwbec/social/service/ChatTargetService.java) | In-memory per-player chat mode (NORMAL / GUILD / DM). |
|
||||
| [service/NotificationDispatcher.java](../src/main/java/net/kewwbec/social/service/NotificationDispatcher.java) | Subscribes to friend + guild event channels; surfaces messages to local players. |
|
||||
| [service/payload/](../src/main/java/net/kewwbec/social/service/payload/) | Wire payloads: `FriendEventPayload`, `GuildEventPayload`, `GuildChatPayload`, `DmPayload`. |
|
||||
| [listener/SocialChatRouter.java](../src/main/java/net/kewwbec/social/listener/SocialChatRouter.java) | Hooks `PlayerChatEvent`; when sender's mode is GUILD or DM, cancels and reroutes. |
|
||||
| [listener/SocialDisconnectListener.java](../src/main/java/net/kewwbec/social/listener/SocialDisconnectListener.java) | Clears chat-target + last-sender state on disconnect. |
|
||||
| [command/SocialCommand.java](../src/main/java/net/kewwbec/social/command/SocialCommand.java) | `/social` opens the HyUI hub. |
|
||||
| [command/friend/FriendCommand.java](../src/main/java/net/kewwbec/social/command/friend/FriendCommand.java) | `/friend add\|accept\|reject\|remove\|list\|pending`. |
|
||||
| [command/guild/GuildCommand.java](../src/main/java/net/kewwbec/social/command/guild/GuildCommand.java) | `/guild create\|disband\|invite\|accept\|leave\|kick\|promote\|demote\|motd\|info\|roster\|ranks\|chat`. |
|
||||
| [command/dm/](../src/main/java/net/kewwbec/social/command/dm/) | `/msg`, `/r`, `/chat`. |
|
||||
| [command/privacy/PrivacyCommand.java](../src/main/java/net/kewwbec/social/command/privacy/PrivacyCommand.java) | `/privacy show\|set`. |
|
||||
| [ui/](../src/main/java/net/kewwbec/social/ui/) | HyUI: `SocialHubPage`, `FriendsListPage`, `GuildRosterPage`, `PrivacySettingsPage`. |
|
||||
| [util/PlayerResolver.java](../src/main/java/net/kewwbec/social/util/PlayerResolver.java) | Name -> `PlayerLocation` via `PlayerPresence`. Online players only. |
|
||||
|
||||
## Why the design is this shape
|
||||
|
||||
- **One service per concern.** Friends, guilds, DMs, and privacy are independent services that don't know about each other except through their public types. Each owns its own bus subscription. You can change DM routing without touching guild code.
|
||||
- **Repositories return models, services orchestrate.** Repos never call services. Services own consistency (cascade on disband, privacy gating, etc).
|
||||
- **Privacy is checked in the sender's process before publishing.** A NOBODY setting blocks the publish, so we don't ship rejection logic to every receiver.
|
||||
- **Per-guild rank ladder.** Each guild gets its own `social_guild_ranks` rows seeded on creation (Member / Officer / Owner). Owners with `MANAGE_RANKS` add or edit. Permissions are a bitmask of `GuildPermission` enum values - cheap to evaluate, easy to extend.
|
||||
- **The owner rank is special.** It's flagged `is_owner_rank=1`, can't be deleted, and members can't be promoted into or out of it via `setRank`. Ownership transfer is a future operation that updates both `guilds.owner_uuid` and the member's rank atomically.
|
||||
- **DM routing is "publish + filter locally".** Every server subscribes to `social.dm`; on receive, if the recipient's UUID is on the local Universe, deliver. This sidesteps having to ask "which server is X on?" - we let the bus fan out and each server self-selects.
|
||||
- **Chat-target toggle, not multi-arg commands.** Hytale args are single-token, so `/msg <name> <body>` isn't possible. `/msg <name>` enters one-shot DM mode; the next chat goes to them and routing snaps back. `/guild chat` is sticky until `/chat` (or disconnect) flips it off.
|
||||
|
||||
## Lifecycle
|
||||
|
||||
Standard Hytale plugin lifecycle:
|
||||
|
||||
- **`setup()`** - load config, build `ChatTargetService`.
|
||||
- **`start()`** - require NetworkCore's database + bus + presence, bootstrap schema, build services, subscribe to bus channels, register events and commands.
|
||||
- **`shutdown()`** - stop service subscriptions. The database pool is owned by NetworkCore and closes on its own shutdown.
|
||||
|
||||
If NetworkCore's MySQL is unhealthy at `start()`, Social logs SEVERE and returns. Restart once MySQL is back.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Configuration
|
||||
|
||||
## Location
|
||||
|
||||
`<plugin data dir>/config.json`. Written with defaults on first boot.
|
||||
|
||||
## Full config
|
||||
|
||||
```json
|
||||
{
|
||||
"tables": {
|
||||
"friends": "social_friends",
|
||||
"guilds": "social_guilds",
|
||||
"guild_ranks": "social_guild_ranks",
|
||||
"guild_members": "social_guild_members",
|
||||
"guild_invites": "social_guild_invites",
|
||||
"privacy": "social_privacy"
|
||||
},
|
||||
"channels": {
|
||||
"friend_event": "social.friend",
|
||||
"guild_event": "social.guild",
|
||||
"guild_chat": "social.guild.chat",
|
||||
"dm": "social.dm"
|
||||
},
|
||||
"guilds": {
|
||||
"name_min_length": 3,
|
||||
"name_max_length": 24,
|
||||
"tag_max_length": 6,
|
||||
"max_members": 50,
|
||||
"chat_prefix": "[G]"
|
||||
},
|
||||
"friends": {
|
||||
"max_friends": 100,
|
||||
"max_pending_requests": 25
|
||||
},
|
||||
"dms": {
|
||||
"prefix_to": "[to {name}]",
|
||||
"prefix_from": "[from {name}]"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Permission gating doesn't live here. Each command checks a permission node (`networksocial.<command>`) via Hytale's `PermissionsModule`. Configure those in the Hytale server's own permission config - see [03_Commands.md](03_Commands.md#permissions).
|
||||
|
||||
## Fields
|
||||
|
||||
### tables
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `friends` | `social_friends` | Friend requests + accepted friendships. |
|
||||
| `guilds` | `social_guilds` | One row per guild. |
|
||||
| `guild_ranks` | `social_guild_ranks` | Per-guild rank ladder (each guild has its own ranks). |
|
||||
| `guild_members` | `social_guild_members` | One row per player, points to their guild and rank. |
|
||||
| `guild_invites` | `social_guild_invites` | Pending invites. |
|
||||
| `privacy` | `social_privacy` | Per-player privacy settings (DM, friend-request, guild-invite policies). |
|
||||
|
||||
Change a name only if you really need to (e.g. table collision with another plugin) and you're prepared to migrate data.
|
||||
|
||||
### channels
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `friend_event` | `social.friend` | Bus channel for friend-request / accept / remove notifications. |
|
||||
| `guild_event` | `social.guild` | Bus channel for guild lifecycle events (invite, member changes, ranks, MOTD, disband). |
|
||||
| `guild_chat` | `social.guild.chat` | Bus channel for guild chat messages. |
|
||||
| `dm` | `social.dm` | Bus channel for DMs. |
|
||||
|
||||
Channels are plain Redis pub/sub. Don't share them with other plugins. If you run multiple Social plugin clusters against the same Redis (different networks), use different channel names so they don't cross-talk.
|
||||
|
||||
### guilds
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `name_min_length` | `3` | Minimum characters in a guild name. |
|
||||
| `name_max_length` | `24` | Maximum characters in a guild name. Stored as `VARCHAR(32)` so don't push above 32. |
|
||||
| `tag_max_length` | `6` | Maximum characters in the optional guild tag (currently informational; not surfaced in chat). |
|
||||
| `max_members` | `50` | Hard cap on members per guild. `acceptInvite` and `invite` enforce this. |
|
||||
| `chat_prefix` | `[G]` | Visual prefix for every guild chat line. |
|
||||
|
||||
Names must be alphanumeric plus `_` and `-`. Spaces and punctuation are rejected. This matches the single-token nature of Hytale commands - if you allow spaces, `/guild invite My Cool Guild` is unparseable.
|
||||
|
||||
### friends
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `max_friends` | `100` | Cap on accepted friendships per player. Send fails with `LIMIT_REACHED` past this. |
|
||||
| `max_pending_requests` | `25` | Cap on outgoing-pending requests per player. Stops one player from spamming the system. |
|
||||
|
||||
### dms
|
||||
|
||||
| Field | Default | Meaning |
|
||||
|---|---|---|
|
||||
| `prefix_to` | `[to {name}]` | Format for the sender's local echo. `{name}` substitutes the recipient's username. |
|
||||
| `prefix_from` | `[from {name}]` | Format for what the recipient sees. `{name}` substitutes the sender's username. |
|
||||
|
||||
## Environment variables
|
||||
|
||||
None specific to Social. Database + bus secrets live in NetworkCore's env (`MYSQL_PASSWORD`, `NETWORK_AUTH_TOKEN`).
|
||||
|
||||
## Setting up permissions
|
||||
|
||||
1. In your Hytale server's permission config, grant the relevant `networksocial.*` nodes to whatever groups you want.
|
||||
2. Default groups (e.g. `Adventurer`) should typically get `networksocial.friend.use`, `networksocial.dm.use`, `networksocial.privacy.use`, `networksocial.guild.use`.
|
||||
3. `networksocial.guild.create` is the gate for *founding* a guild - you might restrict that to a paid rank or to specific roles.
|
||||
4. `networksocial.admin` is reserved for future server-side moderation tools (force-disband, force-leave, etc); not consumed in v1.
|
||||
5. Players need to reconnect for newly-granted permissions to apply.
|
||||
@@ -0,0 +1,105 @@
|
||||
# Commands
|
||||
|
||||
Every command is single-token-only (Hytale arg restriction). Multi-word inputs (DM bodies, guild chat lines, MOTDs longer than one word) use the **chat-target toggle** pattern: a command sets your chat mode, then your next chat message carries the body.
|
||||
|
||||
## Friends - `/friend`
|
||||
|
||||
| Subcommand | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/friend add <name>` | name | `networksocial.friend.use` | Send a friend request to an online player. Fails if recipient has `FRIEND_REQUEST` privacy set to `NOBODY` (or `FRIENDS` and you're not one already). |
|
||||
| `/friend accept <name>` | sender name | `networksocial.friend.use` | Accept an incoming pending request. |
|
||||
| `/friend reject <name>` | sender name | `networksocial.friend.use` | Reject an incoming pending request (no notification sent to sender). |
|
||||
| `/friend remove <name>` | friend name | `networksocial.friend.use` | Remove someone from your friends list. Bidirectional. |
|
||||
| `/friend list` | (none) | `networksocial.friend.use` | Show all accepted friends. |
|
||||
| `/friend pending` | (none) | `networksocial.friend.use` | Show incoming + outgoing pending requests. |
|
||||
|
||||
Notes:
|
||||
- `add`/`accept`/`reject`/`remove` need a username. If the target is online network-wide, `PlayerPresence` resolves them. If not, `accept`/`reject`/`remove` fall back to scanning your own friendship rows. `add` requires the target to be online.
|
||||
|
||||
## Guilds - `/guild`
|
||||
|
||||
| Subcommand | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/guild create <name>` | name | `networksocial.guild.create` | Create a new guild with you as owner. Seeds default ranks: Member / Officer / Owner. |
|
||||
| `/guild disband` | (none) | `networksocial.guild.use` | Owner only. Deletes guild, ranks, members, and pending invites in one shot. |
|
||||
| `/guild invite <name>` | invitee name | `networksocial.guild.use` | Requires `INVITE` permission on your rank. Sends an invite to an online player. Subject to their `GUILD_INVITE` privacy. |
|
||||
| `/guild accept <guild>` | guild name | `networksocial.guild.use` | Accept a pending invite. You join at the guild's lowest-priority non-owner rank. |
|
||||
| `/guild leave` | (none) | `networksocial.guild.use` | Leave your guild. Owners can't leave - use `/guild disband` instead. |
|
||||
| `/guild kick <name>` | member name | `networksocial.guild.use` | Requires `KICK` permission, must outrank target by `priority`. Can't kick the owner. |
|
||||
| `/guild promote <name> <rankId>` | name + rank | `networksocial.guild.use` | Requires `PROMOTE` permission, both source and destination ranks must be below yours. |
|
||||
| `/guild demote <name> <rankId>` | name + rank | `networksocial.guild.use` | Same constraints, but uses `DEMOTE` permission. |
|
||||
| `/guild motd <text>` | one-token text | `networksocial.guild.use` | Requires `MOTD` permission. Use `/guild chat` for multi-word MOTDs? No - MOTD is one token. For long MOTDs, edit via SQL or extend the plugin. |
|
||||
| `/guild info` | (none) | `networksocial.guild.use` | Print your guild's name, owner, MOTD, and your rank. |
|
||||
| `/guild roster` | (none) | `networksocial.guild.use` | List every member of your guild and their rank id. |
|
||||
| `/guild ranks` | (none) | `networksocial.guild.use` | List every rank in your guild with id, name, priority, owner-flag, and bitmask permissions decoded. |
|
||||
| `/guild chat` | (none) | `networksocial.guild.use` | Toggle guild-chat mode. While ON, your chat goes to your guild instead of public. Requires `CHAT` permission on your rank. |
|
||||
|
||||
### Guild permission bits
|
||||
|
||||
A rank's `permissions` is a bitmask of [`GuildPermission`](../src/main/java/net/kewwbec/social/model/GuildPermission.java):
|
||||
|
||||
| Bit | Name | What it allows |
|
||||
|---|---|---|
|
||||
| 1 | `INVITE` | `/guild invite` |
|
||||
| 2 | `KICK` | `/guild kick` |
|
||||
| 4 | `PROMOTE` | `/guild promote` |
|
||||
| 8 | `DEMOTE` | `/guild demote` |
|
||||
| 16 | `MOTD` | `/guild motd` |
|
||||
| 32 | `MANAGE_RANKS` | Edit the rank ladder (no CLI in v1; service-only API) |
|
||||
| 64 | `DISBAND` | (reserved; today only the owner can disband) |
|
||||
| 128 | `CHAT` | `/guild chat` + guild chat send |
|
||||
|
||||
Default rank seeding on `/guild create`:
|
||||
- **Member** (priority 10, perms = `CHAT`)
|
||||
- **Officer** (priority 50, perms = `CHAT | INVITE | KICK | MOTD`)
|
||||
- **Owner** (priority 100, all perms, `is_owner_rank=1`)
|
||||
|
||||
## Direct messages
|
||||
|
||||
| Command | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/msg <name>` | recipient name | `networksocial.dm.use` | Enter one-shot DM mode for the next chat message. Your next chat is delivered as a DM and your mode reverts to NORMAL. |
|
||||
| `/r` | (none) | `networksocial.dm.use` | Enter DM mode targeting whoever DMed you last. Same one-shot behavior as `/msg`. |
|
||||
| `/chat` | (none) | (none) | Clear any DM-mode / guild-chat-mode state so your chat is public again. |
|
||||
|
||||
Why one-shot DMs: Hytale args are single-token, so `/msg name body of message` is impossible. The toggle pattern is the same one Staff uses for `/sc`.
|
||||
|
||||
## Privacy - `/privacy`
|
||||
|
||||
| Subcommand | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/privacy show` | (none) | `networksocial.privacy.use` | Print your current DM / friend-request / guild-invite policies. |
|
||||
| `/privacy set <key> <value>` | key + value | `networksocial.privacy.use` | Update one policy. `key` is `DM`, `FRIEND_REQUEST`, or `GUILD_INVITE`. `value` is `EVERYONE`, `FRIENDS`, or `NOBODY`. |
|
||||
|
||||
Privacy is enforced **at the sender's server**: when you `/friend add X`, your server calls `PrivacyService.canContact(you, X, FRIEND_REQUEST)` before publishing on the bus. If X has set their friend-request policy to NOBODY, the request never travels. Same for DM and guild-invite.
|
||||
|
||||
## Hub - `/social`
|
||||
|
||||
| Command | Args | Permission | Behavior |
|
||||
|---|---|---|---|
|
||||
| `/social` | (none) | (none) | Open the HyUI hub. Buttons go to Friends, Guild Roster, and Privacy Settings panels. |
|
||||
|
||||
See [07_UI.md](07_UI.md).
|
||||
|
||||
## Permissions index
|
||||
|
||||
```
|
||||
networksocial.friend.use
|
||||
networksocial.guild.use
|
||||
networksocial.guild.create
|
||||
networksocial.dm.use
|
||||
networksocial.privacy.use
|
||||
networksocial.admin # reserved for future server-side moderation tools
|
||||
```
|
||||
|
||||
Each command checks one specific node via `requirePermission(...)` + `canGeneratePermission() = false`. We don't rely on Hytale's auto-generated permission names - those proved unreliable in practice. See [Ranks docs / Provider_Integration](../../Ranks/docs/05_Provider_Integration.md) for the broader rationale.
|
||||
|
||||
## Error/result responses
|
||||
|
||||
Every operation returns a `Result` enum from its service. Commands map these to user-facing chat text. The full set per service:
|
||||
|
||||
**FriendService.Result**: `OK`, `SELF`, `ALREADY_PENDING`, `ALREADY_FRIENDS`, `BLOCKED_BY_PRIVACY`, `LIMIT_REACHED`, `NO_REQUEST`, `DB_ERROR`.
|
||||
|
||||
**GuildService.Result**: `OK`, `NAME_INVALID`, `NAME_TAKEN`, `ALREADY_IN_GUILD`, `NOT_IN_GUILD`, `NOT_FOUND`, `NO_PERMISSION`, `SELF`, `BLOCKED_BY_PRIVACY`, `AT_CAPACITY`, `NO_INVITE`, `RANK_INVALID`, `OWNER_RANK_LOCKED`, `OWNER_ONLY`, `DB_ERROR`.
|
||||
|
||||
**DmRoutingService.Result**: `OK`, `SELF`, `TARGET_OFFLINE`, `BLOCKED_BY_PRIVACY`, `DB_ERROR`.
|
||||
@@ -0,0 +1,9 @@
|
||||
# Social Docs
|
||||
|
||||
1. [01_Overview.md](01_Overview.md) - architecture and pieces
|
||||
2. [02_Configuration.md](02_Configuration.md) - config.json
|
||||
3. [03_Commands.md](03_Commands.md) - every command, syntax, behavior
|
||||
4. [04_Schema.md](04_Schema.md) - MySQL tables
|
||||
5. [05_Cross_Server_Propagation.md](05_Cross_Server_Propagation.md) - friends, guilds, DMs over the bus
|
||||
6. [06_Operations.md](06_Operations.md) - ops scenarios, debugging
|
||||
7. [07_UI.md](07_UI.md) - the /social panel
|
||||
@@ -0,0 +1,101 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>net.kewwbec</groupId>
|
||||
<artifactId>social</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>Social</name>
|
||||
<description>KweebecNet social: friends, guilds, direct messages, privacy</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<hytale.server.jar>${user.home}/AppData/Roaming/Hytale/install/release/package/game/latest/Server/HytaleServer.jar</hytale.server.jar>
|
||||
<networkcore.jar>${project.basedir}/../Core/target/NetworkCore-0.1.0.jar</networkcore.jar>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
<repository>
|
||||
<id>cursemaven</id>
|
||||
<url>https://www.cursemaven.com</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.hypixel.hytale</groupId>
|
||||
<artifactId>hytale-server</artifactId>
|
||||
<version>local</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${hytale.server.jar}</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.kewwbec</groupId>
|
||||
<artifactId>networkcore</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${networkcore.jar}</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>curse.maven</groupId>
|
||||
<artifactId>hyui-1431415</artifactId>
|
||||
<version>7511121</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<minimizeJar>false</minimizeJar>
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>com.hypixel.hytale:*</exclude>
|
||||
<exclude>net.kewwbec:networkcore</exclude>
|
||||
<exclude>com.google.code.findbugs:*</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
<exclude>module-info.class</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.kewwbec.social;
|
||||
|
||||
public final class SocialPermissionsNodes {
|
||||
|
||||
public static final String ROOT = "networksocial";
|
||||
|
||||
public static final String FRIEND_USE = ROOT + ".friend.use";
|
||||
public static final String GUILD_USE = ROOT + ".guild.use";
|
||||
public static final String GUILD_CREATE = ROOT + ".guild.create";
|
||||
public static final String DM_USE = ROOT + ".dm.use";
|
||||
public static final String PRIVACY_USE = ROOT + ".privacy.use";
|
||||
public static final String ADMIN = ROOT + ".admin";
|
||||
|
||||
private SocialPermissionsNodes() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
package net.kewwbec.social;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||
import net.kewwbec.networkcore.NetworkCore;
|
||||
import net.kewwbec.networkcore.api.DatabaseService;
|
||||
import net.kewwbec.networkcore.api.MessageBus;
|
||||
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||
import net.kewwbec.social.command.SocialCommand;
|
||||
import net.kewwbec.social.command.dm.ChatCommand;
|
||||
import net.kewwbec.social.command.dm.MsgCommand;
|
||||
import net.kewwbec.social.command.dm.ReplyCommand;
|
||||
import net.kewwbec.social.command.friend.FriendCommand;
|
||||
import net.kewwbec.social.command.guild.GuildCommand;
|
||||
import net.kewwbec.social.command.privacy.PrivacyCommand;
|
||||
import net.kewwbec.social.config.SocialConfig;
|
||||
import net.kewwbec.social.config.SocialConfigLoader;
|
||||
import net.kewwbec.social.db.FriendRepository;
|
||||
import net.kewwbec.social.db.GuildInviteRepository;
|
||||
import net.kewwbec.social.db.GuildMemberRepository;
|
||||
import net.kewwbec.social.db.GuildRankRepository;
|
||||
import net.kewwbec.social.db.GuildRepository;
|
||||
import net.kewwbec.social.db.PrivacyRepository;
|
||||
import net.kewwbec.social.db.SchemaBootstrap;
|
||||
import net.kewwbec.social.listener.SocialChatRouter;
|
||||
import net.kewwbec.social.listener.SocialDisconnectListener;
|
||||
import net.kewwbec.social.service.ChatTargetService;
|
||||
import net.kewwbec.social.service.DmRoutingService;
|
||||
import net.kewwbec.social.service.FriendService;
|
||||
import net.kewwbec.social.service.GuildChatService;
|
||||
import net.kewwbec.social.service.GuildService;
|
||||
import net.kewwbec.social.service.NotificationDispatcher;
|
||||
import net.kewwbec.social.service.PrivacyService;
|
||||
import net.kewwbec.social.ui.SocialUIContext;
|
||||
import net.kewwbec.social.util.PlayerResolver;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.sql.SQLException;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class SocialPlugin extends JavaPlugin {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private SocialConfig config;
|
||||
private ChatTargetService chatTargets;
|
||||
|
||||
private GuildChatService guildChat;
|
||||
private DmRoutingService dms;
|
||||
private NotificationDispatcher notifications;
|
||||
|
||||
public SocialPlugin(@Nonnull JavaPluginInit init) {
|
||||
super(init);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setup() {
|
||||
try {
|
||||
this.config = SocialConfigLoader.loadOrCreate(getDataDirectory());
|
||||
} catch (IOException e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.SEVERE).withCause(e)).log("Failed to load Social config");
|
||||
this.config = new SocialConfig();
|
||||
}
|
||||
this.chatTargets = new ChatTargetService();
|
||||
LOGGER.at(Level.INFO).log("Social setup complete");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void start() {
|
||||
NetworkCore core = NetworkCore.getInstance();
|
||||
DatabaseService db = core.getDatabase();
|
||||
if (db == null || !db.isHealthy()) {
|
||||
LOGGER.at(Level.SEVERE).log("Social requires NetworkCore's MySQL DatabaseService to be enabled and healthy.");
|
||||
return;
|
||||
}
|
||||
MessageBus bus = core.getMessageBus();
|
||||
if (bus == null) {
|
||||
LOGGER.at(Level.SEVERE).log("Social requires NetworkCore's MessageBus.");
|
||||
return;
|
||||
}
|
||||
PlayerPresence presence = core.getPlayerPresence();
|
||||
if (presence == null) {
|
||||
LOGGER.at(Level.SEVERE).log("Social requires NetworkCore's PlayerPresence.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
SchemaBootstrap.apply(db.getDataSource(), config.tables);
|
||||
} catch (SQLException e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.SEVERE).withCause(e)).log("Failed to bootstrap Social schema");
|
||||
return;
|
||||
}
|
||||
|
||||
FriendRepository friendRepo = new FriendRepository(db.getDataSource(), config.tables.friends);
|
||||
GuildRepository guildRepo = new GuildRepository(db.getDataSource(), config.tables.guilds);
|
||||
GuildRankRepository rankRepo = new GuildRankRepository(db.getDataSource(), config.tables.guild_ranks);
|
||||
GuildMemberRepository memberRepo = new GuildMemberRepository(db.getDataSource(), config.tables.guild_members);
|
||||
GuildInviteRepository inviteRepo = new GuildInviteRepository(db.getDataSource(), config.tables.guild_invites);
|
||||
PrivacyRepository privacyRepo = new PrivacyRepository(db.getDataSource(), config.tables.privacy);
|
||||
|
||||
PrivacyService privacy = new PrivacyService(privacyRepo, friendRepo);
|
||||
FriendService friends = new FriendService(friendRepo, privacy, bus, config, core.getServerId());
|
||||
GuildService guilds = new GuildService(guildRepo, rankRepo, memberRepo, inviteRepo, privacy, bus, config, core.getServerId());
|
||||
|
||||
this.guildChat = new GuildChatService(bus, memberRepo, config, core.getServerId());
|
||||
guildChat.start();
|
||||
|
||||
this.dms = new DmRoutingService(bus, presence, privacy, config, core.getServerId());
|
||||
dms.start();
|
||||
|
||||
this.notifications = new NotificationDispatcher(bus, config, memberRepo);
|
||||
notifications.start();
|
||||
|
||||
PlayerResolver resolver = new PlayerResolver(presence);
|
||||
SocialChatRouter chatRouter = new SocialChatRouter(chatTargets, dms, guildChat, guilds, presence);
|
||||
SocialDisconnectListener disconnect = new SocialDisconnectListener(chatTargets, dms);
|
||||
|
||||
getEventRegistry().registerGlobal(PlayerChatEvent.class, chatRouter::onChat);
|
||||
getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, disconnect::onDisconnect);
|
||||
|
||||
getCommandRegistry().registerCommand(new FriendCommand(friends, resolver));
|
||||
getCommandRegistry().registerCommand(new GuildCommand(guilds, resolver, chatTargets));
|
||||
getCommandRegistry().registerCommand(new MsgCommand(chatTargets, resolver));
|
||||
getCommandRegistry().registerCommand(new ReplyCommand(chatTargets, dms, presence));
|
||||
getCommandRegistry().registerCommand(new ChatCommand(chatTargets));
|
||||
getCommandRegistry().registerCommand(new PrivacyCommand(privacy));
|
||||
getCommandRegistry().registerCommand(new SocialCommand(new SocialUIContext(friends, guilds, privacy)));
|
||||
|
||||
LOGGER.at(Level.INFO).log("Social started (serverId=%s)", core.getServerId());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutdown() {
|
||||
if (notifications != null) { try { notifications.stop(); } catch (RuntimeException ignored) {} notifications = null; }
|
||||
if (dms != null) { try { dms.stop(); } catch (RuntimeException ignored) {} dms = null; }
|
||||
if (guildChat != null) { try { guildChat.stop(); } catch (RuntimeException ignored) {} guildChat = null; }
|
||||
LOGGER.at(Level.INFO).log("Social shutdown complete");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package net.kewwbec.social.command;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.social.ui.SocialHubPage;
|
||||
import net.kewwbec.social.ui.SocialUIContext;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public final class SocialCommand extends AbstractPlayerCommand {
|
||||
|
||||
private final SocialUIContext ctx;
|
||||
|
||||
public SocialCommand(@Nonnull SocialUIContext ctx) {
|
||||
super("social", "Open the social panel (friends, guild, privacy)");
|
||||
this.ctx = ctx;
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ignored,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
new SocialHubPage(sender, ctx).open(store);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package net.kewwbec.social.command.dm;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.social.service.ChatTargetService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
|
||||
/**
|
||||
* /chat clears any DM or guild-chat target so the next chat is public again.
|
||||
*/
|
||||
public final class ChatCommand extends AbstractPlayerCommand {
|
||||
|
||||
private final ChatTargetService targets;
|
||||
|
||||
public ChatCommand(@Nonnull ChatTargetService targets) {
|
||||
super("chat", "Switch back to normal public chat");
|
||||
this.targets = targets;
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
targets.clear(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("Chat target cleared. Your chat is public again.").color(Color.LIGHT_GRAY));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package net.kewwbec.social.command.dm;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.social.SocialPermissionsNodes;
|
||||
import net.kewwbec.social.service.ChatTargetService;
|
||||
import net.kewwbec.social.util.PlayerResolver;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
|
||||
/**
|
||||
* /msg <name> enters one-shot DM mode for the caller. The very next chat message they send is
|
||||
* routed to the named player via SocialChatRouter, then their chat falls back to NORMAL.
|
||||
*
|
||||
* Hytale args are single-token so we can't take the body as part of the command.
|
||||
*/
|
||||
public final class MsgCommand extends AbstractPlayerCommand {
|
||||
|
||||
private final ChatTargetService targets;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Recipient", ArgTypes.STRING);
|
||||
|
||||
public MsgCommand(@Nonnull ChatTargetService targets, @Nonnull PlayerResolver resolver) {
|
||||
super("msg", "Start a DM (your next chat message goes to that player)");
|
||||
this.targets = targets;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.DM_USE);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) {
|
||||
ctx.sendMessage(Message.raw("Usage: /msg <name>").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
PlayerLocation target = resolver.resolve(name);
|
||||
if (target == null) {
|
||||
ctx.sendMessage(Message.raw(name + " is not online.").color(Color.RED));
|
||||
return;
|
||||
}
|
||||
targets.setDm(sender.getUuid(), target.uuid(), target.username());
|
||||
ctx.sendMessage(Message.raw("DM mode -> " + target.username() + ". Type your next chat to send.").color(Color.LIGHT_GRAY));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package net.kewwbec.social.command.dm;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||
import net.kewwbec.social.SocialPermissionsNodes;
|
||||
import net.kewwbec.social.service.ChatTargetService;
|
||||
import net.kewwbec.social.service.DmRoutingService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* /r enters DM mode targeting whoever DMed you last. Then your next chat goes to them.
|
||||
*/
|
||||
public final class ReplyCommand extends AbstractPlayerCommand {
|
||||
|
||||
private final ChatTargetService targets;
|
||||
private final DmRoutingService dms;
|
||||
private final PlayerPresence presence;
|
||||
|
||||
public ReplyCommand(@Nonnull ChatTargetService targets,
|
||||
@Nonnull DmRoutingService dms,
|
||||
@Nonnull PlayerPresence presence) {
|
||||
super("r", "Reply to your last DM sender");
|
||||
this.targets = targets;
|
||||
this.dms = dms;
|
||||
this.presence = presence;
|
||||
requirePermission(SocialPermissionsNodes.DM_USE);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
UUID last = dms.getLastSender(sender.getUuid());
|
||||
if (last == null) {
|
||||
ctx.sendMessage(Message.raw("No one has DMed you recently.").color(Color.RED));
|
||||
return;
|
||||
}
|
||||
PlayerLocation target = presence.getLocation(last);
|
||||
if (target == null) {
|
||||
ctx.sendMessage(Message.raw("That person is offline.").color(Color.RED));
|
||||
return;
|
||||
}
|
||||
targets.setDm(sender.getUuid(), target.uuid(), target.username());
|
||||
ctx.sendMessage(Message.raw("Reply -> " + target.username() + ". Type your next chat to send.").color(Color.LIGHT_GRAY));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package net.kewwbec.social.command.friend;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.social.SocialPermissionsNodes;
|
||||
import net.kewwbec.social.model.Friendship;
|
||||
import net.kewwbec.social.service.FriendService;
|
||||
import net.kewwbec.social.util.PlayerResolver;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class FriendCommand extends AbstractCommandCollection {
|
||||
|
||||
public FriendCommand(@Nonnull FriendService service, @Nonnull PlayerResolver resolver) {
|
||||
super("friend", "Manage friends (add, accept, reject, remove, list)");
|
||||
addSubCommand(new Add(service, resolver));
|
||||
addSubCommand(new Accept(service, resolver));
|
||||
addSubCommand(new Reject(service, resolver));
|
||||
addSubCommand(new Remove(service, resolver));
|
||||
addSubCommand(new ListAll(service));
|
||||
addSubCommand(new Pending(service));
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
public static final class Add extends AbstractPlayerCommand {
|
||||
private final FriendService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Player name", ArgTypes.STRING);
|
||||
|
||||
public Add(@Nonnull FriendService service, @Nonnull PlayerResolver resolver) {
|
||||
super("add", "Send a friend request");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.FRIEND_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /friend add <name>").color(Color.YELLOW)); return; }
|
||||
PlayerLocation target = resolver.resolve(name);
|
||||
if (target == null) { ctx.sendMessage(Message.raw(name + " is not online anywhere on the network.").color(Color.RED)); return; }
|
||||
FriendService.Result r = service.sendRequest(sender.getUuid(), sender.getUsername(), target.uuid(), target.username());
|
||||
ctx.sendMessage(Message.raw(text(r, target.username())).color(color(r)));
|
||||
}
|
||||
|
||||
private static String text(FriendService.Result r, String name) {
|
||||
return switch (r) {
|
||||
case OK -> "Friend request sent to " + name + ".";
|
||||
case SELF -> "You can't friend yourself.";
|
||||
case ALREADY_PENDING -> "There's already a pending request between you and " + name + ".";
|
||||
case ALREADY_FRIENDS -> name + " is already your friend.";
|
||||
case BLOCKED_BY_PRIVACY -> name + " is not accepting friend requests right now.";
|
||||
case LIMIT_REACHED -> "You've hit your friend limit.";
|
||||
case DB_ERROR -> "DB error.";
|
||||
default -> r.name();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Accept extends AbstractPlayerCommand {
|
||||
private final FriendService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Player who sent the request", ArgTypes.STRING);
|
||||
|
||||
public Accept(@Nonnull FriendService service, @Nonnull PlayerResolver resolver) {
|
||||
super("accept", "Accept an incoming friend request");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.FRIEND_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /friend accept <name>").color(Color.YELLOW)); return; }
|
||||
UUID fromUuid = lookupIncomingSender(service, sender.getUuid(), name);
|
||||
String fromName = name;
|
||||
if (fromUuid == null) {
|
||||
PlayerLocation onl = resolver.resolve(name);
|
||||
if (onl == null) { ctx.sendMessage(Message.raw("No pending request from " + name + ".").color(Color.RED)); return; }
|
||||
fromUuid = onl.uuid();
|
||||
fromName = onl.username();
|
||||
}
|
||||
FriendService.Result r = service.accept(fromUuid, fromName, sender.getUuid(), sender.getUsername());
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "You and " + fromName + " are now friends.";
|
||||
case NO_REQUEST -> "No pending request from " + fromName + ".";
|
||||
case DB_ERROR -> "DB error.";
|
||||
default -> r.name();
|
||||
}).color(color(r)));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Reject extends AbstractPlayerCommand {
|
||||
private final FriendService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Player whose request to reject", ArgTypes.STRING);
|
||||
|
||||
public Reject(@Nonnull FriendService service, @Nonnull PlayerResolver resolver) {
|
||||
super("reject", "Reject an incoming friend request");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.FRIEND_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /friend reject <name>").color(Color.YELLOW)); return; }
|
||||
UUID fromUuid = lookupIncomingSender(service, sender.getUuid(), name);
|
||||
if (fromUuid == null) {
|
||||
PlayerLocation onl = resolver.resolve(name);
|
||||
if (onl == null) { ctx.sendMessage(Message.raw("No pending request from " + name + ".").color(Color.RED)); return; }
|
||||
fromUuid = onl.uuid();
|
||||
}
|
||||
FriendService.Result r = service.reject(fromUuid, sender.getUuid());
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Rejected friend request from " + name + ".";
|
||||
case NO_REQUEST -> "No pending request from " + name + ".";
|
||||
case DB_ERROR -> "DB error.";
|
||||
default -> r.name();
|
||||
}).color(color(r)));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Remove extends AbstractPlayerCommand {
|
||||
private final FriendService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Friend to remove", ArgTypes.STRING);
|
||||
|
||||
public Remove(@Nonnull FriendService service, @Nonnull PlayerResolver resolver) {
|
||||
super("remove", "Remove a friend");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.FRIEND_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /friend remove <name>").color(Color.YELLOW)); return; }
|
||||
UUID otherUuid;
|
||||
String otherName = name;
|
||||
PlayerLocation onl = resolver.resolve(name);
|
||||
if (onl != null) {
|
||||
otherUuid = onl.uuid();
|
||||
otherName = onl.username();
|
||||
} else {
|
||||
otherUuid = lookupFriend(service, sender.getUuid(), name);
|
||||
if (otherUuid == null) { ctx.sendMessage(Message.raw("No friend named " + name + ".").color(Color.RED)); return; }
|
||||
}
|
||||
FriendService.Result r = service.remove(sender.getUuid(), sender.getUsername(), otherUuid, otherName);
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Removed " + otherName + " from friends.";
|
||||
case NO_REQUEST -> "You're not friends with " + otherName + ".";
|
||||
case DB_ERROR -> "DB error.";
|
||||
default -> r.name();
|
||||
}).color(color(r)));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ListAll extends AbstractPlayerCommand {
|
||||
private final FriendService service;
|
||||
|
||||
public ListAll(@Nonnull FriendService service) {
|
||||
super("list", "Show your friends");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.FRIEND_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
try {
|
||||
List<Friendship> all = service.list(sender.getUuid());
|
||||
if (all.isEmpty()) { ctx.sendMessage(Message.raw("You have no friends yet.").color(Color.GRAY)); return; }
|
||||
ctx.sendMessage(Message.raw("=== Friends (" + all.size() + ") ===").color(Color.YELLOW));
|
||||
for (Friendship f : all) {
|
||||
String other = f.fromUuid().equals(sender.getUuid()) ? f.toName() : f.fromName();
|
||||
ctx.sendMessage(Message.raw("- " + other).color(Color.WHITE));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Pending extends AbstractPlayerCommand {
|
||||
private final FriendService service;
|
||||
|
||||
public Pending(@Nonnull FriendService service) {
|
||||
super("pending", "Show pending friend requests (incoming + outgoing)");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.FRIEND_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
try {
|
||||
List<Friendship> incoming = service.incomingPending(sender.getUuid());
|
||||
List<Friendship> outgoing = service.outgoingPending(sender.getUuid());
|
||||
if (incoming.isEmpty() && outgoing.isEmpty()) { ctx.sendMessage(Message.raw("No pending requests.").color(Color.GRAY)); return; }
|
||||
ctx.sendMessage(Message.raw("Incoming (" + incoming.size() + "):").color(Color.YELLOW));
|
||||
for (Friendship f : incoming) ctx.sendMessage(Message.raw(" from " + f.fromName()).color(Color.WHITE));
|
||||
ctx.sendMessage(Message.raw("Outgoing (" + outgoing.size() + "):").color(Color.YELLOW));
|
||||
for (Friendship f : outgoing) ctx.sendMessage(Message.raw(" to " + f.toName()).color(Color.WHITE));
|
||||
} catch (SQLException e) {
|
||||
ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Color color(FriendService.Result r) {
|
||||
return r == FriendService.Result.OK ? Color.GREEN : Color.RED;
|
||||
}
|
||||
|
||||
private static UUID lookupIncomingSender(FriendService service, UUID me, String name) {
|
||||
try {
|
||||
for (Friendship f : service.incomingPending(me)) {
|
||||
if (f.fromName().equalsIgnoreCase(name)) return f.fromUuid();
|
||||
}
|
||||
} catch (SQLException ignored) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static UUID lookupFriend(FriendService service, UUID me, String name) {
|
||||
try {
|
||||
for (Friendship f : service.list(me)) {
|
||||
if (f.fromUuid().equals(me) && f.toName().equalsIgnoreCase(name)) return f.toUuid();
|
||||
if (f.toUuid().equals(me) && f.fromName().equalsIgnoreCase(name)) return f.fromUuid();
|
||||
}
|
||||
} catch (SQLException ignored) {}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,420 @@
|
||||
package net.kewwbec.social.command.guild;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.social.SocialPermissionsNodes;
|
||||
import net.kewwbec.social.model.Guild;
|
||||
import net.kewwbec.social.model.GuildMember;
|
||||
import net.kewwbec.social.model.GuildPermission;
|
||||
import net.kewwbec.social.model.GuildRank;
|
||||
import net.kewwbec.social.service.ChatTargetService;
|
||||
import net.kewwbec.social.service.GuildService;
|
||||
import net.kewwbec.social.util.PlayerResolver;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
|
||||
public final class GuildCommand extends AbstractCommandCollection {
|
||||
|
||||
public GuildCommand(@Nonnull GuildService service,
|
||||
@Nonnull PlayerResolver resolver,
|
||||
@Nonnull ChatTargetService chatTargets) {
|
||||
super("guild", "Guild commands: create, invite, leave, etc.");
|
||||
addSubCommand(new Create(service));
|
||||
addSubCommand(new Disband(service));
|
||||
addSubCommand(new Invite(service, resolver));
|
||||
addSubCommand(new Accept(service));
|
||||
addSubCommand(new Leave(service));
|
||||
addSubCommand(new Kick(service, resolver));
|
||||
addSubCommand(new Promote(service, resolver));
|
||||
addSubCommand(new Demote(service, resolver));
|
||||
addSubCommand(new Motd(service));
|
||||
addSubCommand(new Info(service));
|
||||
addSubCommand(new RosterCmd(service));
|
||||
addSubCommand(new RanksCmd(service));
|
||||
addSubCommand(new ChatToggle(service, chatTargets));
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
public static final class Create extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Guild name (3-24 chars)", ArgTypes.STRING);
|
||||
|
||||
public Create(@Nonnull GuildService service) {
|
||||
super("create", "Create a new guild");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_CREATE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /guild create <name>").color(Color.YELLOW)); return; }
|
||||
GuildService.Result r = service.create(name, null, sender.getUuid(), sender.getUsername());
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Created guild '" + name + "'. Use /guild invite <name> to add members.";
|
||||
case NAME_INVALID -> "Invalid guild name (3-24 chars, alphanumeric).";
|
||||
case NAME_TAKEN -> "That guild name is taken.";
|
||||
case ALREADY_IN_GUILD -> "You're already in a guild. Leave first.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Disband extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
public Disband(@Nonnull GuildService service) {
|
||||
super("disband", "Disband your guild (owner only)");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
GuildService.Result r = service.disband(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Guild disbanded.";
|
||||
case NOT_IN_GUILD -> "You're not in a guild.";
|
||||
case OWNER_ONLY -> "Only the owner can disband.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Invite extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Player name", ArgTypes.STRING);
|
||||
|
||||
public Invite(@Nonnull GuildService service, @Nonnull PlayerResolver resolver) {
|
||||
super("invite", "Invite a player to your guild");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /guild invite <name>").color(Color.YELLOW)); return; }
|
||||
PlayerLocation target = resolver.resolve(name);
|
||||
if (target == null) { ctx.sendMessage(Message.raw(name + " is not online.").color(Color.RED)); return; }
|
||||
GuildService.Result r = service.invite(sender.getUuid(), target.uuid(), target.username());
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Invited " + target.username() + ".";
|
||||
case SELF -> "You can't invite yourself.";
|
||||
case NOT_IN_GUILD -> "You're not in a guild.";
|
||||
case NO_PERMISSION -> "You don't have permission to invite.";
|
||||
case ALREADY_IN_GUILD -> target.username() + " is already in a guild.";
|
||||
case AT_CAPACITY -> "Your guild is at capacity.";
|
||||
case BLOCKED_BY_PRIVACY -> target.username() + " is not accepting guild invites right now.";
|
||||
case NO_INVITE -> target.username() + " already has a pending invite.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Accept extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("guild", "Guild name to accept invite from", ArgTypes.STRING);
|
||||
|
||||
public Accept(@Nonnull GuildService service) {
|
||||
super("accept", "Accept a pending guild invite");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /guild accept <guild-name>").color(Color.YELLOW)); return; }
|
||||
Guild g = service.findByName(name);
|
||||
if (g == null) { ctx.sendMessage(Message.raw("No such guild.").color(Color.RED)); return; }
|
||||
GuildService.Result r = service.acceptInvite(sender.getUuid(), sender.getUsername(), g.id());
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Joined guild '" + g.name() + "'.";
|
||||
case NO_INVITE -> "No pending invite from that guild.";
|
||||
case ALREADY_IN_GUILD -> "You're already in a guild.";
|
||||
case AT_CAPACITY -> "That guild is full.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Leave extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
public Leave(@Nonnull GuildService service) {
|
||||
super("leave", "Leave your current guild");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
GuildService.Result r = service.leave(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "You left your guild.";
|
||||
case NOT_IN_GUILD -> "You're not in a guild.";
|
||||
case OWNER_RANK_LOCKED -> "Owners can't leave - use /guild disband instead.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Kick extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Member to kick", ArgTypes.STRING);
|
||||
|
||||
public Kick(@Nonnull GuildService service, @Nonnull PlayerResolver resolver) {
|
||||
super("kick", "Kick a guild member");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
if (name == null) { ctx.sendMessage(Message.raw("Usage: /guild kick <name>").color(Color.YELLOW)); return; }
|
||||
java.util.UUID targetUuid = lookupRosterMember(service, sender.getUuid(), name);
|
||||
if (targetUuid == null) {
|
||||
PlayerLocation onl = resolver.resolve(name);
|
||||
if (onl == null) { ctx.sendMessage(Message.raw("No such member.").color(Color.RED)); return; }
|
||||
targetUuid = onl.uuid();
|
||||
}
|
||||
GuildService.Result r = service.kick(sender.getUuid(), targetUuid);
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Kicked " + name + ".";
|
||||
case NOT_IN_GUILD -> "You're not in a guild (or target isn't in yours).";
|
||||
case NO_PERMISSION -> "You can't kick that member.";
|
||||
case OWNER_RANK_LOCKED -> "Can't kick the owner.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Promote extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Member", ArgTypes.STRING);
|
||||
private final RequiredArg<Integer> rankArg = withRequiredArg("rank", "Rank id (see /guild ranks)", ArgTypes.INTEGER);
|
||||
|
||||
public Promote(@Nonnull GuildService service, @Nonnull PlayerResolver resolver) {
|
||||
super("promote", "Promote a member to a higher rank");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
Integer rid = rankArg.get(ctx);
|
||||
if (name == null || rid == null) { ctx.sendMessage(Message.raw("Usage: /guild promote <name> <rankId>").color(Color.YELLOW)); return; }
|
||||
java.util.UUID t = lookupRosterMember(service, sender.getUuid(), name);
|
||||
if (t == null) {
|
||||
PlayerLocation onl = resolver.resolve(name);
|
||||
if (onl == null) { ctx.sendMessage(Message.raw("No such member.").color(Color.RED)); return; }
|
||||
t = onl.uuid();
|
||||
}
|
||||
GuildService.Result r = service.setRank(sender.getUuid(), t, rid, true);
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Promoted " + name + ".";
|
||||
case NO_PERMISSION -> "You can't promote that high.";
|
||||
case RANK_INVALID -> "Invalid rank id.";
|
||||
case OWNER_RANK_LOCKED -> "Can't move to/from the owner rank.";
|
||||
case NOT_IN_GUILD -> "Not in same guild.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Demote extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final PlayerResolver resolver;
|
||||
private final RequiredArg<String> nameArg = withRequiredArg("name", "Member", ArgTypes.STRING);
|
||||
private final RequiredArg<Integer> rankArg = withRequiredArg("rank", "Rank id", ArgTypes.INTEGER);
|
||||
|
||||
public Demote(@Nonnull GuildService service, @Nonnull PlayerResolver resolver) {
|
||||
super("demote", "Demote a member to a lower rank");
|
||||
this.service = service;
|
||||
this.resolver = resolver;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String name = nameArg.get(ctx);
|
||||
Integer rid = rankArg.get(ctx);
|
||||
if (name == null || rid == null) { ctx.sendMessage(Message.raw("Usage: /guild demote <name> <rankId>").color(Color.YELLOW)); return; }
|
||||
java.util.UUID t = lookupRosterMember(service, sender.getUuid(), name);
|
||||
if (t == null) {
|
||||
PlayerLocation onl = resolver.resolve(name);
|
||||
if (onl == null) { ctx.sendMessage(Message.raw("No such member.").color(Color.RED)); return; }
|
||||
t = onl.uuid();
|
||||
}
|
||||
GuildService.Result r = service.setRank(sender.getUuid(), t, rid, false);
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Demoted " + name + ".";
|
||||
case NO_PERMISSION -> "You can't demote.";
|
||||
case RANK_INVALID -> "Invalid rank id.";
|
||||
case OWNER_RANK_LOCKED -> "Can't move to/from the owner rank.";
|
||||
case NOT_IN_GUILD -> "Not in same guild.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Motd extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final RequiredArg<String> motdArg = withRequiredArg("text", "New MOTD (single token)", ArgTypes.STRING);
|
||||
|
||||
public Motd(@Nonnull GuildService service) {
|
||||
super("motd", "Set the guild MOTD");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String text = motdArg.get(ctx);
|
||||
if (text == null) { ctx.sendMessage(Message.raw("Usage: /guild motd <text>").color(Color.YELLOW)); return; }
|
||||
GuildService.Result r = service.setMotd(sender.getUuid(), text);
|
||||
ctx.sendMessage(Message.raw(switch (r) {
|
||||
case OK -> "Updated MOTD.";
|
||||
case NOT_IN_GUILD -> "You're not in a guild.";
|
||||
case NO_PERMISSION -> "You can't set the MOTD.";
|
||||
default -> r.name();
|
||||
}).color(r == GuildService.Result.OK ? Color.GREEN : Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Info extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
public Info(@Nonnull GuildService service) {
|
||||
super("info", "Show your guild");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
GuildService.GuildContext c = service.contextFor(sender.getUuid());
|
||||
if (c == null) { ctx.sendMessage(Message.raw("You are not in a guild.").color(Color.GRAY)); return; }
|
||||
ctx.sendMessage(Message.raw("=== " + c.guild().name() + " ===").color(Color.YELLOW));
|
||||
ctx.sendMessage(Message.raw("Owner: " + c.guild().ownerName()).color(Color.WHITE));
|
||||
ctx.sendMessage(Message.raw("MOTD: " + c.guild().motd()).color(Color.WHITE));
|
||||
ctx.sendMessage(Message.raw("Your rank: " + c.rank().name() + " (id=" + c.rank().rankId() + ")").color(Color.WHITE));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RosterCmd extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
public RosterCmd(@Nonnull GuildService service) {
|
||||
super("roster", "Show all guild members");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
GuildService.GuildContext c = service.contextFor(sender.getUuid());
|
||||
if (c == null) { ctx.sendMessage(Message.raw("You are not in a guild.").color(Color.GRAY)); return; }
|
||||
try {
|
||||
List<GuildMember> roster = service.roster(c.guild().id());
|
||||
ctx.sendMessage(Message.raw("=== Roster (" + roster.size() + ") ===").color(Color.YELLOW));
|
||||
for (GuildMember m : roster) {
|
||||
ctx.sendMessage(Message.raw("- " + m.name() + " [rank " + m.rankId() + "]").color(Color.WHITE));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class RanksCmd extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
public RanksCmd(@Nonnull GuildService service) {
|
||||
super("ranks", "Show your guild's ranks");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
GuildService.GuildContext c = service.contextFor(sender.getUuid());
|
||||
if (c == null) { ctx.sendMessage(Message.raw("You are not in a guild.").color(Color.GRAY)); return; }
|
||||
try {
|
||||
List<GuildRank> ranks = service.rankList(c.guild().id());
|
||||
ctx.sendMessage(Message.raw("=== Ranks ===").color(Color.YELLOW));
|
||||
for (GuildRank r : ranks) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (GuildPermission p : GuildPermission.values()) if (p.isIn(r.permissions())) sb.append(p.name()).append(",");
|
||||
if (sb.length() > 0) sb.setLength(sb.length() - 1);
|
||||
ctx.sendMessage(Message.raw(r.rankId() + " | " + r.name() + " | prio=" + r.priority() + (r.isOwnerRank() ? " [OWNER]" : "") + " | " + sb).color(Color.WHITE));
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ChatToggle extends AbstractPlayerCommand {
|
||||
private final GuildService service;
|
||||
private final ChatTargetService chatTargets;
|
||||
|
||||
public ChatToggle(@Nonnull GuildService service, @Nonnull ChatTargetService chatTargets) {
|
||||
super("chat", "Toggle guild chat mode (your chat goes to your guild)");
|
||||
this.service = service;
|
||||
this.chatTargets = chatTargets;
|
||||
requirePermission(SocialPermissionsNodes.GUILD_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
GuildService.GuildContext c = service.contextFor(sender.getUuid());
|
||||
if (c == null) { ctx.sendMessage(Message.raw("You are not in a guild.").color(Color.RED)); return; }
|
||||
if (!GuildPermission.CHAT.isIn(c.rank().permissions())) { ctx.sendMessage(Message.raw("Your rank can't use guild chat.").color(Color.RED)); return; }
|
||||
ChatTargetService.Target cur = chatTargets.get(sender.getUuid());
|
||||
if (cur.mode() == ChatTargetService.Mode.GUILD) {
|
||||
chatTargets.clear(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("Guild chat OFF.").color(Color.YELLOW));
|
||||
} else {
|
||||
chatTargets.setGuild(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("Guild chat ON. Your chat goes to your guild. Run /guild chat again to turn off.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static java.util.UUID lookupRosterMember(GuildService service, java.util.UUID actor, String name) {
|
||||
GuildService.GuildContext c = service.contextFor(actor);
|
||||
if (c == null) return null;
|
||||
try {
|
||||
for (GuildMember m : service.roster(c.guild().id())) {
|
||||
if (m.name().equalsIgnoreCase(name)) return m.uuid();
|
||||
}
|
||||
} catch (SQLException ignored) {}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package net.kewwbec.social.command.privacy;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.social.SocialPermissionsNodes;
|
||||
import net.kewwbec.social.model.PrivacyKey;
|
||||
import net.kewwbec.social.model.PrivacySettings;
|
||||
import net.kewwbec.social.model.PrivacyValue;
|
||||
import net.kewwbec.social.service.PrivacyService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public final class PrivacyCommand extends AbstractCommandCollection {
|
||||
|
||||
public PrivacyCommand(@Nonnull PrivacyService service) {
|
||||
super("privacy", "View and change your privacy settings");
|
||||
addSubCommand(new Show(service));
|
||||
addSubCommand(new SetCmd(service));
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
public static final class Show extends AbstractPlayerCommand {
|
||||
private final PrivacyService service;
|
||||
public Show(@Nonnull PrivacyService service) {
|
||||
super("show", "Show your current privacy settings");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.PRIVACY_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
try {
|
||||
PrivacySettings s = service.get(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("=== Privacy ===").color(Color.YELLOW));
|
||||
for (PrivacyKey k : PrivacyKey.values()) {
|
||||
ctx.sendMessage(Message.raw(k.name() + ": " + s.get(k).name()).color(Color.WHITE));
|
||||
}
|
||||
ctx.sendMessage(Message.raw("Change with: /privacy set <key> <EVERYONE|FRIENDS|NOBODY>").color(Color.GRAY));
|
||||
} catch (SQLException e) {
|
||||
ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final class SetCmd extends AbstractPlayerCommand {
|
||||
private final PrivacyService service;
|
||||
private final RequiredArg<String> keyArg = withRequiredArg("key", "DM | FRIEND_REQUEST | GUILD_INVITE", ArgTypes.STRING);
|
||||
private final RequiredArg<String> valArg = withRequiredArg("value", "EVERYONE | FRIENDS | NOBODY", ArgTypes.STRING);
|
||||
|
||||
public SetCmd(@Nonnull PrivacyService service) {
|
||||
super("set", "Change one of your privacy settings");
|
||||
this.service = service;
|
||||
requirePermission(SocialPermissionsNodes.PRIVACY_USE);
|
||||
}
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store, @Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender, @Nonnull World world) {
|
||||
String keyRaw = keyArg.get(ctx);
|
||||
String valRaw = valArg.get(ctx);
|
||||
if (keyRaw == null || valRaw == null) {
|
||||
ctx.sendMessage(Message.raw("Usage: /privacy set <key> <value>").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
PrivacyKey key = PrivacyKey.fromName(keyRaw);
|
||||
PrivacyValue value = PrivacyValue.fromName(valRaw);
|
||||
if (key == null) {
|
||||
ctx.sendMessage(Message.raw("Unknown key. Use: DM, FRIEND_REQUEST, GUILD_INVITE.").color(Color.RED));
|
||||
return;
|
||||
}
|
||||
if (value == null) {
|
||||
ctx.sendMessage(Message.raw("Unknown value. Use: EVERYONE, FRIENDS, NOBODY.").color(Color.RED));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
service.set(sender.getUuid(), key, value);
|
||||
ctx.sendMessage(Message.raw(key.name() + " -> " + value.name()).color(Color.GREEN));
|
||||
} catch (SQLException e) {
|
||||
ctx.sendMessage(Message.raw("DB error: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.kewwbec.social.config;
|
||||
|
||||
public final class SocialConfig {
|
||||
|
||||
public TableSection tables = new TableSection();
|
||||
public ChannelSection channels = new ChannelSection();
|
||||
public GuildSection guilds = new GuildSection();
|
||||
public FriendSection friends = new FriendSection();
|
||||
public DmSection dms = new DmSection();
|
||||
|
||||
public static final class TableSection {
|
||||
public String friends = "social_friends";
|
||||
public String guilds = "social_guilds";
|
||||
public String guild_ranks = "social_guild_ranks";
|
||||
public String guild_members = "social_guild_members";
|
||||
public String guild_invites = "social_guild_invites";
|
||||
public String privacy = "social_privacy";
|
||||
}
|
||||
|
||||
public static final class ChannelSection {
|
||||
public String friend_event = "social.friend";
|
||||
public String guild_event = "social.guild";
|
||||
public String guild_chat = "social.guild.chat";
|
||||
public String dm = "social.dm";
|
||||
}
|
||||
|
||||
public static final class GuildSection {
|
||||
public int name_min_length = 3;
|
||||
public int name_max_length = 24;
|
||||
public int tag_max_length = 6;
|
||||
public int max_members = 50;
|
||||
public String chat_prefix = "[G]";
|
||||
}
|
||||
|
||||
public static final class FriendSection {
|
||||
public int max_friends = 100;
|
||||
public int max_pending_requests = 25;
|
||||
}
|
||||
|
||||
public static final class DmSection {
|
||||
public String prefix_to = "[to {name}]";
|
||||
public String prefix_from = "[from {name}]";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.kewwbec.social.config;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public final class SocialConfigLoader {
|
||||
|
||||
private static final Gson GSON = new GsonBuilder().setPrettyPrinting().create();
|
||||
|
||||
private SocialConfigLoader() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static SocialConfig loadOrCreate(@Nonnull Path dataDirectory) throws IOException {
|
||||
Path file = dataDirectory.resolve("config.json");
|
||||
if (!Files.exists(file)) {
|
||||
Files.createDirectories(dataDirectory);
|
||||
SocialConfig fresh = new SocialConfig();
|
||||
Files.writeString(file, GSON.toJson(fresh), StandardCharsets.UTF_8);
|
||||
return fresh;
|
||||
}
|
||||
String json = Files.readString(file, StandardCharsets.UTF_8);
|
||||
SocialConfig parsed = GSON.fromJson(json, SocialConfig.class);
|
||||
if (parsed == null) parsed = new SocialConfig();
|
||||
if (parsed.tables == null) parsed.tables = new SocialConfig.TableSection();
|
||||
if (parsed.channels == null) parsed.channels = new SocialConfig.ChannelSection();
|
||||
if (parsed.guilds == null) parsed.guilds = new SocialConfig.GuildSection();
|
||||
if (parsed.friends == null) parsed.friends = new SocialConfig.FriendSection();
|
||||
if (parsed.dms == null) parsed.dms = new SocialConfig.DmSection();
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
package net.kewwbec.social.db;
|
||||
|
||||
import net.kewwbec.social.model.FriendStatus;
|
||||
import net.kewwbec.social.model.Friendship;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class FriendRepository {
|
||||
|
||||
private final DataSource ds;
|
||||
private final String table;
|
||||
|
||||
public FriendRepository(@Nonnull DataSource ds, @Nonnull String table) {
|
||||
this.ds = ds;
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public long createRequest(@Nonnull UUID fromUuid, @Nonnull String fromName,
|
||||
@Nonnull UUID toUuid, @Nonnull String toName) throws SQLException {
|
||||
String sql = "INSERT INTO " + table +
|
||||
" (from_uuid, from_name, to_uuid, to_name, status, created_at) VALUES (?, ?, ?, ?, 'PENDING', ?)";
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement(sql, java.sql.Statement.RETURN_GENERATED_KEYS)) {
|
||||
p.setString(1, fromUuid.toString());
|
||||
p.setString(2, fromName);
|
||||
p.setString(3, toUuid.toString());
|
||||
p.setString(4, toName);
|
||||
p.setLong(5, System.currentTimeMillis());
|
||||
p.executeUpdate();
|
||||
try (ResultSet rs = p.getGeneratedKeys()) {
|
||||
return rs.next() ? rs.getLong(1) : -1L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean accept(@Nonnull UUID fromUuid, @Nonnull UUID toUuid) throws SQLException {
|
||||
String sql = "UPDATE " + table + " SET status='ACCEPTED', accepted_at=? WHERE from_uuid=? AND to_uuid=? AND status='PENDING'";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setLong(1, System.currentTimeMillis());
|
||||
p.setString(2, fromUuid.toString());
|
||||
p.setString(3, toUuid.toString());
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int delete(@Nonnull UUID a, @Nonnull UUID b) throws SQLException {
|
||||
String sql = "DELETE FROM " + table +
|
||||
" WHERE (from_uuid=? AND to_uuid=?) OR (from_uuid=? AND to_uuid=?)";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, a.toString());
|
||||
p.setString(2, b.toString());
|
||||
p.setString(3, b.toString());
|
||||
p.setString(4, a.toString());
|
||||
return p.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<Friendship> findPending(@Nonnull UUID fromUuid, @Nonnull UUID toUuid) throws SQLException {
|
||||
String sql = "SELECT * FROM " + table + " WHERE from_uuid=? AND to_uuid=? AND status='PENDING'";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, fromUuid.toString());
|
||||
p.setString(2, toUuid.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return Optional.of(map(rs));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<Friendship> findAccepted(@Nonnull UUID a, @Nonnull UUID b) throws SQLException {
|
||||
String sql = "SELECT * FROM " + table +
|
||||
" WHERE status='ACCEPTED' AND ((from_uuid=? AND to_uuid=?) OR (from_uuid=? AND to_uuid=?))";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, a.toString());
|
||||
p.setString(2, b.toString());
|
||||
p.setString(3, b.toString());
|
||||
p.setString(4, a.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return Optional.of(map(rs));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Friendship> friendsOf(@Nonnull UUID user) throws SQLException {
|
||||
String sql = "SELECT * FROM " + table +
|
||||
" WHERE status='ACCEPTED' AND (from_uuid=? OR to_uuid=?) ORDER BY accepted_at DESC";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, user.toString());
|
||||
p.setString(2, user.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
List<Friendship> out = new ArrayList<>();
|
||||
while (rs.next()) out.add(map(rs));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Friendship> incomingPending(@Nonnull UUID user) throws SQLException {
|
||||
String sql = "SELECT * FROM " + table + " WHERE to_uuid=? AND status='PENDING' ORDER BY created_at DESC";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, user.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
List<Friendship> out = new ArrayList<>();
|
||||
while (rs.next()) out.add(map(rs));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Friendship> outgoingPending(@Nonnull UUID user) throws SQLException {
|
||||
String sql = "SELECT * FROM " + table + " WHERE from_uuid=? AND status='PENDING' ORDER BY created_at DESC";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, user.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
List<Friendship> out = new ArrayList<>();
|
||||
while (rs.next()) out.add(map(rs));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int countAccepted(@Nonnull UUID user) throws SQLException {
|
||||
String sql = "SELECT COUNT(*) FROM " + table + " WHERE status='ACCEPTED' AND (from_uuid=? OR to_uuid=?)";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, user.toString());
|
||||
p.setString(2, user.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
return rs.next() ? rs.getInt(1) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int countOutgoingPending(@Nonnull UUID user) throws SQLException {
|
||||
String sql = "SELECT COUNT(*) FROM " + table + " WHERE from_uuid=? AND status='PENDING'";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, user.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
return rs.next() ? rs.getInt(1) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Friendship map(@Nonnull ResultSet rs) throws SQLException {
|
||||
return new Friendship(
|
||||
rs.getLong("id"),
|
||||
UUID.fromString(rs.getString("from_uuid")),
|
||||
rs.getString("from_name"),
|
||||
UUID.fromString(rs.getString("to_uuid")),
|
||||
rs.getString("to_name"),
|
||||
FriendStatus.valueOf(rs.getString("status")),
|
||||
rs.getLong("created_at"),
|
||||
rs.getLong("accepted_at")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package net.kewwbec.social.db;
|
||||
|
||||
import net.kewwbec.social.model.GuildInvite;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class GuildInviteRepository {
|
||||
|
||||
private final DataSource ds;
|
||||
private final String table;
|
||||
|
||||
public GuildInviteRepository(@Nonnull DataSource ds, @Nonnull String table) {
|
||||
this.ds = ds;
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public long insert(long guildId, @Nonnull String guildName,
|
||||
@Nonnull UUID invitee, @Nonnull UUID inviter, @Nonnull String inviterName) throws SQLException {
|
||||
String sql = "INSERT IGNORE INTO " + table +
|
||||
" (guild_id, guild_name, invitee_uuid, inviter_uuid, inviter_name, created_at) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
p.setLong(1, guildId);
|
||||
p.setString(2, guildName);
|
||||
p.setString(3, invitee.toString());
|
||||
p.setString(4, inviter.toString());
|
||||
p.setString(5, inviterName);
|
||||
p.setLong(6, System.currentTimeMillis());
|
||||
int n = p.executeUpdate();
|
||||
if (n == 0) return -1L;
|
||||
try (ResultSet rs = p.getGeneratedKeys()) {
|
||||
return rs.next() ? rs.getLong(1) : -1L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean delete(long guildId, @Nonnull UUID invitee) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("DELETE FROM " + table + " WHERE guild_id=? AND invitee_uuid=?")) {
|
||||
p.setLong(1, guildId);
|
||||
p.setString(2, invitee.toString());
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int deleteAllForGuild(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("DELETE FROM " + table + " WHERE guild_id=?")) {
|
||||
p.setLong(1, guildId);
|
||||
return p.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<GuildInvite> find(long guildId, @Nonnull UUID invitee) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT * FROM " + table + " WHERE guild_id=? AND invitee_uuid=?")) {
|
||||
p.setLong(1, guildId);
|
||||
p.setString(2, invitee.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return Optional.of(map(rs));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<GuildInvite> findByInvitee(@Nonnull UUID invitee) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT * FROM " + table + " WHERE invitee_uuid=? ORDER BY created_at DESC")) {
|
||||
p.setString(1, invitee.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
List<GuildInvite> out = new ArrayList<>();
|
||||
while (rs.next()) out.add(map(rs));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private GuildInvite map(@Nonnull ResultSet rs) throws SQLException {
|
||||
return new GuildInvite(
|
||||
rs.getLong("id"),
|
||||
rs.getLong("guild_id"),
|
||||
rs.getString("guild_name"),
|
||||
UUID.fromString(rs.getString("invitee_uuid")),
|
||||
UUID.fromString(rs.getString("inviter_uuid")),
|
||||
rs.getString("inviter_name"),
|
||||
rs.getLong("created_at")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package net.kewwbec.social.db;
|
||||
|
||||
import net.kewwbec.social.model.GuildMember;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class GuildMemberRepository {
|
||||
|
||||
private final DataSource ds;
|
||||
private final String table;
|
||||
|
||||
public GuildMemberRepository(@Nonnull DataSource ds, @Nonnull String table) {
|
||||
this.ds = ds;
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public boolean add(long guildId, @Nonnull UUID uuid, @Nonnull String name, int rankId) throws SQLException {
|
||||
String sql = "INSERT IGNORE INTO " + table + " (guild_id, uuid, name, rank_id, joined_at) VALUES (?, ?, ?, ?, ?)";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setLong(1, guildId);
|
||||
p.setString(2, uuid.toString());
|
||||
p.setString(3, name);
|
||||
p.setInt(4, rankId);
|
||||
p.setLong(5, System.currentTimeMillis());
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean remove(@Nonnull UUID uuid) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("DELETE FROM " + table + " WHERE uuid=?")) {
|
||||
p.setString(1, uuid.toString());
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int removeAllForGuild(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("DELETE FROM " + table + " WHERE guild_id=?")) {
|
||||
p.setLong(1, guildId);
|
||||
return p.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setRank(@Nonnull UUID uuid, int rankId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("UPDATE " + table + " SET rank_id=? WHERE uuid=?")) {
|
||||
p.setInt(1, rankId);
|
||||
p.setString(2, uuid.toString());
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<GuildMember> find(@Nonnull UUID uuid) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT * FROM " + table + " WHERE uuid=?")) {
|
||||
p.setString(1, uuid.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return Optional.of(map(rs));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<GuildMember> findAll(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT * FROM " + table + " WHERE guild_id=? ORDER BY rank_id ASC, joined_at ASC")) {
|
||||
p.setLong(1, guildId);
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
List<GuildMember> out = new ArrayList<>();
|
||||
while (rs.next()) out.add(map(rs));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int countForGuild(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT COUNT(*) FROM " + table + " WHERE guild_id=?")) {
|
||||
p.setLong(1, guildId);
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
return rs.next() ? rs.getInt(1) : 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private GuildMember map(@Nonnull ResultSet rs) throws SQLException {
|
||||
return new GuildMember(
|
||||
rs.getLong("guild_id"),
|
||||
UUID.fromString(rs.getString("uuid")),
|
||||
rs.getString("name"),
|
||||
rs.getInt("rank_id"),
|
||||
rs.getLong("joined_at")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package net.kewwbec.social.db;
|
||||
|
||||
import net.kewwbec.social.model.GuildRank;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class GuildRankRepository {
|
||||
|
||||
private final DataSource ds;
|
||||
private final String table;
|
||||
|
||||
public GuildRankRepository(@Nonnull DataSource ds, @Nonnull String table) {
|
||||
this.ds = ds;
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public void insert(@Nonnull GuildRank rank) throws SQLException {
|
||||
String sql = "INSERT INTO " + table +
|
||||
" (guild_id, rank_id, name, priority, permissions, is_owner_rank) VALUES (?, ?, ?, ?, ?, ?)";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setLong(1, rank.guildId());
|
||||
p.setInt(2, rank.rankId());
|
||||
p.setString(3, rank.name());
|
||||
p.setInt(4, rank.priority());
|
||||
p.setInt(5, rank.permissions());
|
||||
p.setBoolean(6, rank.isOwnerRank());
|
||||
p.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean update(long guildId, int rankId, @Nonnull String name, int priority, int permissions) throws SQLException {
|
||||
String sql = "UPDATE " + table + " SET name=?, priority=?, permissions=? WHERE guild_id=? AND rank_id=?";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, name);
|
||||
p.setInt(2, priority);
|
||||
p.setInt(3, permissions);
|
||||
p.setLong(4, guildId);
|
||||
p.setInt(5, rankId);
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean delete(long guildId, int rankId) throws SQLException {
|
||||
String sql = "DELETE FROM " + table + " WHERE guild_id=? AND rank_id=? AND is_owner_rank=0";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setLong(1, guildId);
|
||||
p.setInt(2, rankId);
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public int deleteAllForGuild(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("DELETE FROM " + table + " WHERE guild_id=?")) {
|
||||
p.setLong(1, guildId);
|
||||
return p.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
public int nextRankId(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT COALESCE(MAX(rank_id), 0) FROM " + table + " WHERE guild_id=?")) {
|
||||
p.setLong(1, guildId);
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
return rs.next() ? rs.getInt(1) + 1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<GuildRank> findAll(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement(
|
||||
"SELECT * FROM " + table + " WHERE guild_id=? ORDER BY priority DESC, rank_id ASC")) {
|
||||
p.setLong(1, guildId);
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
List<GuildRank> out = new ArrayList<>();
|
||||
while (rs.next()) out.add(map(rs));
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<GuildRank> find(long guildId, int rankId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT * FROM " + table + " WHERE guild_id=? AND rank_id=?")) {
|
||||
p.setLong(1, guildId);
|
||||
p.setInt(2, rankId);
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return Optional.of(map(rs));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<GuildRank> findOwnerRank(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("SELECT * FROM " + table + " WHERE guild_id=? AND is_owner_rank=1 LIMIT 1")) {
|
||||
p.setLong(1, guildId);
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return Optional.of(map(rs));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private GuildRank map(@Nonnull ResultSet rs) throws SQLException {
|
||||
return new GuildRank(
|
||||
rs.getLong("guild_id"),
|
||||
rs.getInt("rank_id"),
|
||||
rs.getString("name"),
|
||||
rs.getInt("priority"),
|
||||
rs.getInt("permissions"),
|
||||
rs.getBoolean("is_owner_rank")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package net.kewwbec.social.db;
|
||||
|
||||
import net.kewwbec.social.model.Guild;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.sql.Types;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class GuildRepository {
|
||||
|
||||
private final DataSource ds;
|
||||
private final String table;
|
||||
|
||||
public GuildRepository(@Nonnull DataSource ds, @Nonnull String table) {
|
||||
this.ds = ds;
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
public long create(@Nonnull String name, @Nullable String tag,
|
||||
@Nonnull UUID ownerUuid, @Nonnull String ownerName) throws SQLException {
|
||||
String sql = "INSERT INTO " + table +
|
||||
" (name, tag, motd, owner_uuid, owner_name, created_at) VALUES (?, ?, '', ?, ?, ?)";
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) {
|
||||
p.setString(1, name);
|
||||
if (tag != null) p.setString(2, tag); else p.setNull(2, Types.VARCHAR);
|
||||
p.setString(3, ownerUuid.toString());
|
||||
p.setString(4, ownerName);
|
||||
p.setLong(5, System.currentTimeMillis());
|
||||
p.executeUpdate();
|
||||
try (ResultSet rs = p.getGeneratedKeys()) {
|
||||
return rs.next() ? rs.getLong(1) : -1L;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean delete(long guildId) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("DELETE FROM " + table + " WHERE id=?")) {
|
||||
p.setLong(1, guildId);
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setMotd(long guildId, @Nonnull String motd) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("UPDATE " + table + " SET motd=? WHERE id=?")) {
|
||||
p.setString(1, motd);
|
||||
p.setLong(2, guildId);
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setTag(long guildId, @Nullable String tag) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("UPDATE " + table + " SET tag=? WHERE id=?")) {
|
||||
if (tag != null) p.setString(1, tag); else p.setNull(1, Types.VARCHAR);
|
||||
p.setLong(2, guildId);
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean setOwner(long guildId, @Nonnull UUID newOwnerUuid, @Nonnull String newOwnerName) throws SQLException {
|
||||
try (Connection c = ds.getConnection();
|
||||
PreparedStatement p = c.prepareStatement("UPDATE " + table + " SET owner_uuid=?, owner_name=? WHERE id=?")) {
|
||||
p.setString(1, newOwnerUuid.toString());
|
||||
p.setString(2, newOwnerName);
|
||||
p.setLong(3, guildId);
|
||||
return p.executeUpdate() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<Guild> findById(long guildId) throws SQLException {
|
||||
return findOne("id=?", String.valueOf(guildId), true);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Optional<Guild> findByName(@Nonnull String name) throws SQLException {
|
||||
return findOne("name=?", name, false);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Optional<Guild> findOne(@Nonnull String whereClause, @Nonnull String value, boolean numeric) throws SQLException {
|
||||
String sql = "SELECT * FROM " + table + " WHERE " + whereClause + " LIMIT 1";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
if (numeric) p.setLong(1, Long.parseLong(value)); else p.setString(1, value);
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return Optional.of(map(rs));
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Guild map(@Nonnull ResultSet rs) throws SQLException {
|
||||
return new Guild(
|
||||
rs.getLong("id"),
|
||||
rs.getString("name"),
|
||||
rs.getString("tag"),
|
||||
rs.getString("motd"),
|
||||
UUID.fromString(rs.getString("owner_uuid")),
|
||||
rs.getString("owner_name"),
|
||||
rs.getLong("created_at")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package net.kewwbec.social.db;
|
||||
|
||||
import net.kewwbec.social.model.PrivacyKey;
|
||||
import net.kewwbec.social.model.PrivacySettings;
|
||||
import net.kewwbec.social.model.PrivacyValue;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.PreparedStatement;
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PrivacyRepository {
|
||||
|
||||
private final DataSource ds;
|
||||
private final String table;
|
||||
|
||||
public PrivacyRepository(@Nonnull DataSource ds, @Nonnull String table) {
|
||||
this.ds = ds;
|
||||
this.table = table;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PrivacySettings get(@Nonnull UUID uuid) throws SQLException {
|
||||
String sql = "SELECT * FROM " + table + " WHERE uuid=?";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, uuid.toString());
|
||||
try (ResultSet rs = p.executeQuery()) {
|
||||
if (rs.next()) return map(uuid, rs);
|
||||
}
|
||||
}
|
||||
return PrivacySettings.defaults(uuid);
|
||||
}
|
||||
|
||||
public void set(@Nonnull UUID uuid, @Nonnull PrivacyKey key, @Nonnull PrivacyValue value) throws SQLException {
|
||||
String column = switch (key) {
|
||||
case DM -> "dm_policy";
|
||||
case FRIEND_REQUEST -> "friend_request_policy";
|
||||
case GUILD_INVITE -> "guild_invite_policy";
|
||||
};
|
||||
String sql = "INSERT INTO " + table + " (uuid, " + column + ") VALUES (?, ?) " +
|
||||
"ON DUPLICATE KEY UPDATE " + column + "=VALUES(" + column + ")";
|
||||
try (Connection c = ds.getConnection(); PreparedStatement p = c.prepareStatement(sql)) {
|
||||
p.setString(1, uuid.toString());
|
||||
p.setString(2, value.name());
|
||||
p.executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private PrivacySettings map(@Nonnull UUID uuid, @Nonnull ResultSet rs) throws SQLException {
|
||||
Map<PrivacyKey, PrivacyValue> m = new EnumMap<>(PrivacyKey.class);
|
||||
m.put(PrivacyKey.DM, parse(rs.getString("dm_policy")));
|
||||
m.put(PrivacyKey.FRIEND_REQUEST, parse(rs.getString("friend_request_policy")));
|
||||
m.put(PrivacyKey.GUILD_INVITE, parse(rs.getString("guild_invite_policy")));
|
||||
return new PrivacySettings(uuid, m);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private PrivacyValue parse(@Nonnull String s) {
|
||||
try {
|
||||
return PrivacyValue.valueOf(s);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return PrivacyValue.EVERYONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package net.kewwbec.social.db;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import net.kewwbec.social.config.SocialConfig;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.sql.DataSource;
|
||||
import java.sql.Connection;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class SchemaBootstrap {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private SchemaBootstrap() {
|
||||
}
|
||||
|
||||
public static void apply(@Nonnull DataSource ds, @Nonnull SocialConfig.TableSection t) throws SQLException {
|
||||
try (Connection c = ds.getConnection(); Statement s = c.createStatement()) {
|
||||
s.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
from_uuid CHAR(36) NOT NULL,
|
||||
from_name VARCHAR(64) NOT NULL,
|
||||
to_uuid CHAR(36) NOT NULL,
|
||||
to_name VARCHAR(64) NOT NULL,
|
||||
status VARCHAR(16) NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
accepted_at BIGINT NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_pair (from_uuid, to_uuid),
|
||||
KEY idx_from (from_uuid, status),
|
||||
KEY idx_to (to_uuid, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""".formatted(t.friends));
|
||||
|
||||
s.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
name VARCHAR(32) NOT NULL,
|
||||
tag VARCHAR(16) NULL,
|
||||
motd VARCHAR(255) NOT NULL DEFAULT '',
|
||||
owner_uuid CHAR(36) NOT NULL,
|
||||
owner_name VARCHAR(64) NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_name (name),
|
||||
KEY idx_owner (owner_uuid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""".formatted(t.guilds));
|
||||
|
||||
s.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
guild_id BIGINT NOT NULL,
|
||||
rank_id INT NOT NULL,
|
||||
name VARCHAR(32) NOT NULL,
|
||||
priority INT NOT NULL DEFAULT 0,
|
||||
permissions INT NOT NULL DEFAULT 0,
|
||||
is_owner_rank TINYINT(1) NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (guild_id, rank_id),
|
||||
KEY idx_guild_priority (guild_id, priority)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""".formatted(t.guild_ranks));
|
||||
|
||||
s.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
guild_id BIGINT NOT NULL,
|
||||
uuid CHAR(36) NOT NULL,
|
||||
name VARCHAR(64) NOT NULL,
|
||||
rank_id INT NOT NULL,
|
||||
joined_at BIGINT NOT NULL,
|
||||
PRIMARY KEY (uuid),
|
||||
KEY idx_guild (guild_id),
|
||||
KEY idx_guild_rank (guild_id, rank_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""".formatted(t.guild_members));
|
||||
|
||||
s.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
guild_id BIGINT NOT NULL,
|
||||
guild_name VARCHAR(32) NOT NULL,
|
||||
invitee_uuid CHAR(36) NOT NULL,
|
||||
inviter_uuid CHAR(36) NOT NULL,
|
||||
inviter_name VARCHAR(64) NOT NULL,
|
||||
created_at BIGINT NOT NULL,
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uq_invite (guild_id, invitee_uuid),
|
||||
KEY idx_invitee (invitee_uuid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""".formatted(t.guild_invites));
|
||||
|
||||
s.executeUpdate("""
|
||||
CREATE TABLE IF NOT EXISTS %s (
|
||||
uuid CHAR(36) NOT NULL,
|
||||
dm_policy VARCHAR(16) NOT NULL DEFAULT 'EVERYONE',
|
||||
friend_request_policy VARCHAR(16) NOT NULL DEFAULT 'EVERYONE',
|
||||
guild_invite_policy VARCHAR(16) NOT NULL DEFAULT 'EVERYONE',
|
||||
PRIMARY KEY (uuid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
""".formatted(t.privacy));
|
||||
|
||||
LOGGER.at(Level.INFO).log("Social schema bootstrap complete (%s, %s, %s, %s, %s, %s)",
|
||||
t.friends, t.guilds, t.guild_ranks, t.guild_members, t.guild_invites, t.privacy);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package net.kewwbec.social.listener;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerChatEvent;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||
import net.kewwbec.social.service.ChatTargetService;
|
||||
import net.kewwbec.social.service.DmRoutingService;
|
||||
import net.kewwbec.social.service.GuildChatService;
|
||||
import net.kewwbec.social.service.GuildService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Routes chat events when the sender has /g or /msg toggle active.
|
||||
*
|
||||
* Hytale args are single-token, so we can't take "/msg name body" as one call. Instead the
|
||||
* sender runs /msg <name>, which sets DM mode; their next chat goes to that player and routing
|
||||
* snaps back to NORMAL automatically. /g works similarly but stays in GUILD mode until /chat.
|
||||
*/
|
||||
public final class SocialChatRouter {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final ChatTargetService targets;
|
||||
private final DmRoutingService dms;
|
||||
private final GuildChatService guildChat;
|
||||
private final GuildService guildService;
|
||||
private final PlayerPresence presence;
|
||||
|
||||
public SocialChatRouter(@Nonnull ChatTargetService targets,
|
||||
@Nonnull DmRoutingService dms,
|
||||
@Nonnull GuildChatService guildChat,
|
||||
@Nonnull GuildService guildService,
|
||||
@Nonnull PlayerPresence presence) {
|
||||
this.targets = targets;
|
||||
this.dms = dms;
|
||||
this.guildChat = guildChat;
|
||||
this.guildService = guildService;
|
||||
this.presence = presence;
|
||||
}
|
||||
|
||||
public void onChat(@Nonnull PlayerChatEvent event) {
|
||||
PlayerRef sender = event.getSender();
|
||||
if (sender == null) return;
|
||||
UUID uuid = sender.getUuid();
|
||||
ChatTargetService.Target t = targets.get(uuid);
|
||||
if (t.mode() == ChatTargetService.Mode.NORMAL) return;
|
||||
|
||||
String body = event.getContent();
|
||||
if (body == null || body.isEmpty()) return;
|
||||
event.setCancelled(true);
|
||||
|
||||
switch (t.mode()) {
|
||||
case DM -> {
|
||||
if (t.dmTarget() == null) return;
|
||||
PlayerLocation target = presence.getLocation(t.dmTarget());
|
||||
String targetName = (target != null) ? target.username() : (t.dmTargetName() != null ? t.dmTargetName() : "?");
|
||||
DmRoutingService.Result r = dms.send(sender, targetName, body);
|
||||
if (r != DmRoutingService.Result.OK) {
|
||||
sender.sendMessage(Message.raw(dmErrorText(r, targetName)).color(Color.RED));
|
||||
}
|
||||
// One-shot DM: drop back to NORMAL after the message
|
||||
targets.clear(uuid);
|
||||
}
|
||||
case GUILD -> {
|
||||
GuildService.GuildContext ctx = guildService.contextFor(uuid);
|
||||
if (ctx == null) {
|
||||
sender.sendMessage(Message.raw("You are no longer in a guild.").color(Color.RED));
|
||||
targets.clear(uuid);
|
||||
return;
|
||||
}
|
||||
guildChat.send(ctx.guild().id(), ctx.guild().name(), sender, body);
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String dmErrorText(@Nonnull DmRoutingService.Result r, @Nonnull String targetName) {
|
||||
return switch (r) {
|
||||
case SELF -> "You can't DM yourself.";
|
||||
case TARGET_OFFLINE -> targetName + " is not online.";
|
||||
case BLOCKED_BY_PRIVACY -> "You can't DM " + targetName + " right now.";
|
||||
case DB_ERROR -> "DB error sending DM.";
|
||||
case OK -> "";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package net.kewwbec.social.listener;
|
||||
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import net.kewwbec.social.service.ChatTargetService;
|
||||
import net.kewwbec.social.service.DmRoutingService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Clears in-memory state (chat target, last-DM-sender) for a player on disconnect, so a fresh
|
||||
* reconnect starts clean.
|
||||
*/
|
||||
public final class SocialDisconnectListener {
|
||||
|
||||
private final ChatTargetService targets;
|
||||
private final DmRoutingService dms;
|
||||
|
||||
public SocialDisconnectListener(@Nonnull ChatTargetService targets, @Nonnull DmRoutingService dms) {
|
||||
this.targets = targets;
|
||||
this.dms = dms;
|
||||
}
|
||||
|
||||
public void onDisconnect(@Nonnull PlayerDisconnectEvent event) {
|
||||
PlayerRef ref = event.getPlayerRef();
|
||||
if (ref == null) return;
|
||||
targets.clear(ref.getUuid());
|
||||
dms.clearLastSender(ref.getUuid());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
public enum FriendStatus {
|
||||
PENDING,
|
||||
ACCEPTED;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.UUID;
|
||||
|
||||
public record Friendship(
|
||||
long id,
|
||||
@Nonnull UUID fromUuid,
|
||||
@Nonnull String fromName,
|
||||
@Nonnull UUID toUuid,
|
||||
@Nonnull String toName,
|
||||
@Nonnull FriendStatus status,
|
||||
long createdAt,
|
||||
long acceptedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
public record Guild(
|
||||
long id,
|
||||
@Nonnull String name,
|
||||
@Nullable String tag,
|
||||
@Nonnull String motd,
|
||||
@Nonnull UUID ownerUuid,
|
||||
@Nonnull String ownerName,
|
||||
long createdAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.UUID;
|
||||
|
||||
public record GuildInvite(
|
||||
long id,
|
||||
long guildId,
|
||||
@Nonnull String guildName,
|
||||
@Nonnull UUID inviteeUuid,
|
||||
@Nonnull UUID inviterUuid,
|
||||
@Nonnull String inviterName,
|
||||
long createdAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.UUID;
|
||||
|
||||
public record GuildMember(
|
||||
long guildId,
|
||||
@Nonnull UUID uuid,
|
||||
@Nonnull String name,
|
||||
int rankId,
|
||||
long joinedAt
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Per-rank guild capabilities. Stored as a bitmask in social_guild_ranks.permissions.
|
||||
* Add new flags only at the end so old bitmasks stay valid.
|
||||
*/
|
||||
public enum GuildPermission {
|
||||
INVITE (1 << 0),
|
||||
KICK (1 << 1),
|
||||
PROMOTE (1 << 2),
|
||||
DEMOTE (1 << 3),
|
||||
MOTD (1 << 4),
|
||||
MANAGE_RANKS (1 << 5),
|
||||
DISBAND (1 << 6),
|
||||
CHAT (1 << 7);
|
||||
|
||||
private final int bit;
|
||||
|
||||
GuildPermission(int bit) {
|
||||
this.bit = bit;
|
||||
}
|
||||
|
||||
public int bit() {
|
||||
return bit;
|
||||
}
|
||||
|
||||
public boolean isIn(int mask) {
|
||||
return (mask & bit) != 0;
|
||||
}
|
||||
|
||||
public static int combine(@Nonnull GuildPermission... perms) {
|
||||
int m = 0;
|
||||
for (GuildPermission p : perms) m |= p.bit;
|
||||
return m;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static GuildPermission fromName(@Nonnull String name) {
|
||||
for (GuildPermission p : values()) if (p.name().equalsIgnoreCase(name)) return p;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public record GuildRank(
|
||||
long guildId,
|
||||
int rankId,
|
||||
@Nonnull String name,
|
||||
int priority,
|
||||
int permissions,
|
||||
boolean isOwnerRank
|
||||
) {
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public enum PrivacyKey {
|
||||
DM,
|
||||
FRIEND_REQUEST,
|
||||
GUILD_INVITE;
|
||||
|
||||
@Nullable
|
||||
public static PrivacyKey fromName(@Nonnull String name) {
|
||||
for (PrivacyKey k : values()) if (k.name().equalsIgnoreCase(name)) return k;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
public record PrivacySettings(
|
||||
@Nonnull UUID uuid,
|
||||
@Nonnull Map<PrivacyKey, PrivacyValue> values
|
||||
) {
|
||||
@Nonnull
|
||||
public static PrivacySettings defaults(@Nonnull UUID uuid) {
|
||||
Map<PrivacyKey, PrivacyValue> m = new EnumMap<>(PrivacyKey.class);
|
||||
m.put(PrivacyKey.DM, PrivacyValue.EVERYONE);
|
||||
m.put(PrivacyKey.FRIEND_REQUEST, PrivacyValue.EVERYONE);
|
||||
m.put(PrivacyKey.GUILD_INVITE, PrivacyValue.EVERYONE);
|
||||
return new PrivacySettings(uuid, m);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PrivacyValue get(@Nonnull PrivacyKey key) {
|
||||
return values.getOrDefault(key, PrivacyValue.EVERYONE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package net.kewwbec.social.model;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public enum PrivacyValue {
|
||||
EVERYONE,
|
||||
FRIENDS,
|
||||
NOBODY;
|
||||
|
||||
@Nullable
|
||||
public static PrivacyValue fromName(@Nonnull String name) {
|
||||
for (PrivacyValue v : values()) if (v.name().equalsIgnoreCase(name)) return v;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package net.kewwbec.social.service;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* Tracks where each player's chat is currently being routed:
|
||||
* - normal (default, public chat)
|
||||
* - GUILD (next chat goes to the player's guild)
|
||||
* - DM to a specific player UUID
|
||||
*
|
||||
* Set by /g and /msg commands. Cleared on /chat or on disconnect.
|
||||
*/
|
||||
public final class ChatTargetService {
|
||||
|
||||
public enum Mode {
|
||||
NORMAL,
|
||||
GUILD,
|
||||
DM;
|
||||
}
|
||||
|
||||
public record Target(@Nonnull Mode mode, @Nullable UUID dmTarget, @Nullable String dmTargetName) {
|
||||
public static final Target NORMAL = new Target(Mode.NORMAL, null, null);
|
||||
|
||||
@Nonnull
|
||||
public static Target guild() {
|
||||
return new Target(Mode.GUILD, null, null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Target dm(@Nonnull UUID uuid, @Nonnull String name) {
|
||||
return new Target(Mode.DM, uuid, name);
|
||||
}
|
||||
}
|
||||
|
||||
private final ConcurrentMap<UUID, Target> targets = new ConcurrentHashMap<>();
|
||||
|
||||
@Nonnull
|
||||
public Target get(@Nonnull UUID uuid) {
|
||||
return targets.getOrDefault(uuid, Target.NORMAL);
|
||||
}
|
||||
|
||||
public void setGuild(@Nonnull UUID uuid) {
|
||||
targets.put(uuid, Target.guild());
|
||||
}
|
||||
|
||||
public void setDm(@Nonnull UUID uuid, @Nonnull UUID target, @Nonnull String targetName) {
|
||||
targets.put(uuid, Target.dm(target, targetName));
|
||||
}
|
||||
|
||||
public void clear(@Nonnull UUID uuid) {
|
||||
targets.remove(uuid);
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return targets.size();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package net.kewwbec.social.service;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import net.kewwbec.networkcore.api.MessageBus;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||
import net.kewwbec.social.config.SocialConfig;
|
||||
import net.kewwbec.social.model.PrivacyKey;
|
||||
import net.kewwbec.social.service.payload.DmPayload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.awt.Color;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Cross-server DM routing.
|
||||
*
|
||||
* Sender side: validate privacy, then publish a DmPayload on the dm channel.
|
||||
* Each server's subscription checks whether the recipient is on its local universe; if so, it
|
||||
* delivers the message; otherwise it ignores. This means recipient privacy is checked on the
|
||||
* sender's server.
|
||||
*/
|
||||
public final class DmRoutingService {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final MessageBus bus;
|
||||
private final PlayerPresence presence;
|
||||
private final PrivacyService privacy;
|
||||
private final SocialConfig config;
|
||||
private final String serverId;
|
||||
|
||||
@Nullable private MessageBus.Subscription subscription;
|
||||
|
||||
private final java.util.Map<UUID, UUID> lastSender = new java.util.concurrent.ConcurrentHashMap<>();
|
||||
|
||||
public DmRoutingService(@Nonnull MessageBus bus,
|
||||
@Nonnull PlayerPresence presence,
|
||||
@Nonnull PrivacyService privacy,
|
||||
@Nonnull SocialConfig config,
|
||||
@Nonnull String serverId) {
|
||||
this.bus = bus;
|
||||
this.presence = presence;
|
||||
this.privacy = privacy;
|
||||
this.config = config;
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
this.subscription = bus.subscribe(config.channels.dm, DmPayload.class, this::onReceive);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (subscription != null) {
|
||||
try { subscription.close(); } catch (RuntimeException ignored) {}
|
||||
subscription = null;
|
||||
}
|
||||
lastSender.clear();
|
||||
}
|
||||
|
||||
public enum Result {
|
||||
OK,
|
||||
SELF,
|
||||
TARGET_OFFLINE,
|
||||
BLOCKED_BY_PRIVACY,
|
||||
DB_ERROR
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result send(@Nonnull PlayerRef sender, @Nonnull String targetName, @Nonnull String body) {
|
||||
if (sender.getUsername().equalsIgnoreCase(targetName)) return Result.SELF;
|
||||
PlayerLocation target = presence.findByName(targetName);
|
||||
if (target == null) return Result.TARGET_OFFLINE;
|
||||
try {
|
||||
if (!privacy.canContact(sender.getUuid(), target.uuid(), PrivacyKey.DM)) return Result.BLOCKED_BY_PRIVACY;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("DM privacy check failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
|
||||
DmPayload p = new DmPayload(
|
||||
sender.getUuid().toString(), sender.getUsername(),
|
||||
target.uuid().toString(), target.username(),
|
||||
body, serverId);
|
||||
bus.publish(config.channels.dm, p);
|
||||
|
||||
// Local echo to the sender
|
||||
Universe u = Universe.get();
|
||||
if (u != null) {
|
||||
for (PlayerRef ref : u.getPlayers()) {
|
||||
if (ref.getUuid().equals(sender.getUuid())) {
|
||||
ref.sendMessage(Message.raw(formatTo(target.username(), body)).color(Color.LIGHT_GRAY));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return Result.OK;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public UUID getLastSender(@Nonnull UUID recipient) {
|
||||
return lastSender.get(recipient);
|
||||
}
|
||||
|
||||
public void clearLastSender(@Nonnull UUID uuid) {
|
||||
lastSender.remove(uuid);
|
||||
}
|
||||
|
||||
private void onReceive(@Nonnull DmPayload p) {
|
||||
if (p.body == null || p.recipientUuid == null) return;
|
||||
Universe u = Universe.get();
|
||||
if (u == null) return;
|
||||
UUID recipientUuid;
|
||||
UUID senderUuid;
|
||||
try {
|
||||
recipientUuid = UUID.fromString(p.recipientUuid);
|
||||
senderUuid = UUID.fromString(p.senderUuid);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return;
|
||||
}
|
||||
for (PlayerRef ref : u.getPlayers()) {
|
||||
if (!ref.getUuid().equals(recipientUuid)) continue;
|
||||
ref.sendMessage(Message.raw(formatFrom(p.senderName, p.body)).color(Color.WHITE));
|
||||
lastSender.put(recipientUuid, senderUuid);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String formatTo(@Nonnull String name, @Nonnull String body) {
|
||||
return config.dms.prefix_to.replace("{name}", name) + " " + body;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String formatFrom(@Nonnull String name, @Nonnull String body) {
|
||||
return config.dms.prefix_from.replace("{name}", name) + " " + body;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package net.kewwbec.social.service;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import net.kewwbec.networkcore.api.MessageBus;
|
||||
import net.kewwbec.social.config.SocialConfig;
|
||||
import net.kewwbec.social.db.FriendRepository;
|
||||
import net.kewwbec.social.model.Friendship;
|
||||
import net.kewwbec.social.model.PrivacyKey;
|
||||
import net.kewwbec.social.service.payload.FriendEventPayload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class FriendService {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final FriendRepository repo;
|
||||
private final PrivacyService privacy;
|
||||
private final MessageBus bus;
|
||||
private final SocialConfig config;
|
||||
private final String serverId;
|
||||
|
||||
public FriendService(@Nonnull FriendRepository repo,
|
||||
@Nonnull PrivacyService privacy,
|
||||
@Nonnull MessageBus bus,
|
||||
@Nonnull SocialConfig config,
|
||||
@Nonnull String serverId) {
|
||||
this.repo = repo;
|
||||
this.privacy = privacy;
|
||||
this.bus = bus;
|
||||
this.config = config;
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
public enum Result {
|
||||
OK,
|
||||
SELF,
|
||||
ALREADY_PENDING,
|
||||
ALREADY_FRIENDS,
|
||||
BLOCKED_BY_PRIVACY,
|
||||
LIMIT_REACHED,
|
||||
NO_REQUEST,
|
||||
DB_ERROR
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result sendRequest(@Nonnull UUID fromUuid, @Nonnull String fromName,
|
||||
@Nonnull UUID toUuid, @Nonnull String toName) {
|
||||
if (fromUuid.equals(toUuid)) return Result.SELF;
|
||||
try {
|
||||
if (repo.findAccepted(fromUuid, toUuid).isPresent()) return Result.ALREADY_FRIENDS;
|
||||
if (repo.findPending(fromUuid, toUuid).isPresent()) return Result.ALREADY_PENDING;
|
||||
if (repo.findPending(toUuid, fromUuid).isPresent()) return Result.ALREADY_PENDING;
|
||||
if (repo.countAccepted(fromUuid) >= config.friends.max_friends) return Result.LIMIT_REACHED;
|
||||
if (repo.countOutgoingPending(fromUuid) >= config.friends.max_pending_requests) return Result.LIMIT_REACHED;
|
||||
if (!privacy.canContact(fromUuid, toUuid, PrivacyKey.FRIEND_REQUEST)) return Result.BLOCKED_BY_PRIVACY;
|
||||
|
||||
repo.createRequest(fromUuid, fromName, toUuid, toName);
|
||||
bus.publish(config.channels.friend_event, new FriendEventPayload(
|
||||
FriendEventPayload.TYPE_REQUEST,
|
||||
fromUuid.toString(), fromName,
|
||||
toUuid.toString(), toName,
|
||||
serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("sendRequest failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result accept(@Nonnull UUID fromUuid, @Nonnull String fromName,
|
||||
@Nonnull UUID toUuid, @Nonnull String toName) {
|
||||
try {
|
||||
if (!repo.accept(fromUuid, toUuid)) return Result.NO_REQUEST;
|
||||
bus.publish(config.channels.friend_event, new FriendEventPayload(
|
||||
FriendEventPayload.TYPE_ACCEPTED,
|
||||
fromUuid.toString(), fromName,
|
||||
toUuid.toString(), toName,
|
||||
serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("accept failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result reject(@Nonnull UUID fromUuid, @Nonnull UUID toUuid) {
|
||||
try {
|
||||
int n = repo.delete(fromUuid, toUuid);
|
||||
return n > 0 ? Result.OK : Result.NO_REQUEST;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("reject failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result remove(@Nonnull UUID a, @Nonnull String aName,
|
||||
@Nonnull UUID b, @Nonnull String bName) {
|
||||
try {
|
||||
int n = repo.delete(a, b);
|
||||
if (n == 0) return Result.NO_REQUEST;
|
||||
bus.publish(config.channels.friend_event, new FriendEventPayload(
|
||||
FriendEventPayload.TYPE_REMOVED,
|
||||
a.toString(), aName,
|
||||
b.toString(), bName,
|
||||
serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("remove failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Friendship> list(@Nonnull UUID user) throws SQLException {
|
||||
return repo.friendsOf(user);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Friendship> incomingPending(@Nonnull UUID user) throws SQLException {
|
||||
return repo.incomingPending(user);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<Friendship> outgoingPending(@Nonnull UUID user) throws SQLException {
|
||||
return repo.outgoingPending(user);
|
||||
}
|
||||
|
||||
public boolean areFriends(@Nonnull UUID a, @Nonnull UUID b) throws SQLException {
|
||||
return repo.findAccepted(a, b).isPresent();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package net.kewwbec.social.service;
|
||||
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import net.kewwbec.networkcore.api.MessageBus;
|
||||
import net.kewwbec.social.config.SocialConfig;
|
||||
import net.kewwbec.social.db.GuildMemberRepository;
|
||||
import net.kewwbec.social.model.GuildMember;
|
||||
import net.kewwbec.social.service.payload.GuildChatPayload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.awt.Color;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
|
||||
public final class GuildChatService {
|
||||
|
||||
private final MessageBus bus;
|
||||
private final GuildMemberRepository members;
|
||||
private final SocialConfig config;
|
||||
private final String serverId;
|
||||
|
||||
@Nullable private MessageBus.Subscription subscription;
|
||||
|
||||
public GuildChatService(@Nonnull MessageBus bus,
|
||||
@Nonnull GuildMemberRepository members,
|
||||
@Nonnull SocialConfig config,
|
||||
@Nonnull String serverId) {
|
||||
this.bus = bus;
|
||||
this.members = members;
|
||||
this.config = config;
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
this.subscription = bus.subscribe(config.channels.guild_chat, GuildChatPayload.class, this::onReceive);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (subscription != null) {
|
||||
try { subscription.close(); } catch (RuntimeException ignored) {}
|
||||
subscription = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void send(long guildId, @Nonnull String guildName,
|
||||
@Nonnull PlayerRef sender, @Nonnull String body) {
|
||||
bus.publish(config.channels.guild_chat, new GuildChatPayload(
|
||||
guildId, guildName,
|
||||
sender.getUuid().toString(), sender.getUsername(),
|
||||
body, serverId));
|
||||
}
|
||||
|
||||
private void onReceive(@Nonnull GuildChatPayload p) {
|
||||
if (p.body == null) return;
|
||||
Universe universe = Universe.get();
|
||||
if (universe == null) return;
|
||||
|
||||
String line = config.guilds.chat_prefix + " " + p.senderName + ": " + p.body;
|
||||
for (PlayerRef ref : universe.getPlayers()) {
|
||||
try {
|
||||
Optional<GuildMember> m = members.find(ref.getUuid());
|
||||
if (m.isEmpty()) continue;
|
||||
if (m.get().guildId() != p.guildId) continue;
|
||||
ref.sendMessage(Message.raw(line).color(Color.GREEN));
|
||||
} catch (SQLException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,385 @@
|
||||
package net.kewwbec.social.service;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import net.kewwbec.networkcore.api.MessageBus;
|
||||
import net.kewwbec.social.config.SocialConfig;
|
||||
import net.kewwbec.social.db.GuildInviteRepository;
|
||||
import net.kewwbec.social.db.GuildMemberRepository;
|
||||
import net.kewwbec.social.db.GuildRankRepository;
|
||||
import net.kewwbec.social.db.GuildRepository;
|
||||
import net.kewwbec.social.model.Guild;
|
||||
import net.kewwbec.social.model.GuildInvite;
|
||||
import net.kewwbec.social.model.GuildMember;
|
||||
import net.kewwbec.social.model.GuildPermission;
|
||||
import net.kewwbec.social.model.GuildRank;
|
||||
import net.kewwbec.social.model.PrivacyKey;
|
||||
import net.kewwbec.social.service.payload.GuildEventPayload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.sql.SQLException;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class GuildService {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final GuildRepository guilds;
|
||||
private final GuildRankRepository ranks;
|
||||
private final GuildMemberRepository members;
|
||||
private final GuildInviteRepository invites;
|
||||
private final PrivacyService privacy;
|
||||
private final MessageBus bus;
|
||||
private final SocialConfig config;
|
||||
private final String serverId;
|
||||
|
||||
public GuildService(@Nonnull GuildRepository guilds,
|
||||
@Nonnull GuildRankRepository ranks,
|
||||
@Nonnull GuildMemberRepository members,
|
||||
@Nonnull GuildInviteRepository invites,
|
||||
@Nonnull PrivacyService privacy,
|
||||
@Nonnull MessageBus bus,
|
||||
@Nonnull SocialConfig config,
|
||||
@Nonnull String serverId) {
|
||||
this.guilds = guilds;
|
||||
this.ranks = ranks;
|
||||
this.members = members;
|
||||
this.invites = invites;
|
||||
this.privacy = privacy;
|
||||
this.bus = bus;
|
||||
this.config = config;
|
||||
this.serverId = serverId;
|
||||
}
|
||||
|
||||
public enum Result {
|
||||
OK,
|
||||
NAME_INVALID,
|
||||
NAME_TAKEN,
|
||||
ALREADY_IN_GUILD,
|
||||
NOT_IN_GUILD,
|
||||
NOT_FOUND,
|
||||
NO_PERMISSION,
|
||||
SELF,
|
||||
BLOCKED_BY_PRIVACY,
|
||||
AT_CAPACITY,
|
||||
NO_INVITE,
|
||||
RANK_INVALID,
|
||||
OWNER_RANK_LOCKED,
|
||||
OWNER_ONLY,
|
||||
DB_ERROR
|
||||
}
|
||||
|
||||
public record GuildContext(@Nonnull Guild guild, @Nonnull GuildMember member, @Nonnull GuildRank rank) {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Guild findById(long id) {
|
||||
try { return guilds.findById(id).orElse(null); }
|
||||
catch (SQLException e) { LOGGER.at(Level.WARNING).log("findById: %s", e.getMessage()); return null; }
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Guild findByName(@Nonnull String name) {
|
||||
try { return guilds.findByName(name).orElse(null); }
|
||||
catch (SQLException e) { LOGGER.at(Level.WARNING).log("findByName: %s", e.getMessage()); return null; }
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public GuildContext contextFor(@Nonnull UUID uuid) {
|
||||
try {
|
||||
Optional<GuildMember> m = members.find(uuid);
|
||||
if (m.isEmpty()) return null;
|
||||
Optional<Guild> g = guilds.findById(m.get().guildId());
|
||||
if (g.isEmpty()) return null;
|
||||
Optional<GuildRank> r = ranks.find(m.get().guildId(), m.get().rankId());
|
||||
if (r.isEmpty()) return null;
|
||||
return new GuildContext(g.get(), m.get(), r.get());
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("contextFor: %s", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<GuildMember> roster(long guildId) throws SQLException {
|
||||
return members.findAll(guildId);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<GuildRank> rankList(long guildId) throws SQLException {
|
||||
return ranks.findAll(guildId);
|
||||
}
|
||||
|
||||
// ---- create / disband ----
|
||||
|
||||
@Nonnull
|
||||
public Result create(@Nonnull String name, @Nullable String tag,
|
||||
@Nonnull UUID ownerUuid, @Nonnull String ownerName) {
|
||||
if (!isValidName(name)) return Result.NAME_INVALID;
|
||||
if (tag != null && tag.length() > config.guilds.tag_max_length) return Result.NAME_INVALID;
|
||||
try {
|
||||
if (members.find(ownerUuid).isPresent()) return Result.ALREADY_IN_GUILD;
|
||||
if (guilds.findByName(name).isPresent()) return Result.NAME_TAKEN;
|
||||
|
||||
long id = guilds.create(name, tag, ownerUuid, ownerName);
|
||||
if (id <= 0) return Result.DB_ERROR;
|
||||
|
||||
// Seed default ranks: Member (lowest), Officer (mid), Owner (top, locked)
|
||||
ranks.insert(new GuildRank(id, 1, "Member", 10, GuildPermission.combine(GuildPermission.CHAT), false));
|
||||
ranks.insert(new GuildRank(id, 2, "Officer", 50, GuildPermission.combine(GuildPermission.CHAT, GuildPermission.INVITE, GuildPermission.KICK, GuildPermission.MOTD), false));
|
||||
ranks.insert(new GuildRank(id, 3, "Owner", 100, GuildPermission.combine(GuildPermission.values()), true));
|
||||
|
||||
members.add(id, ownerUuid, ownerName, 3);
|
||||
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_MEMBER_JOIN, id, name,
|
||||
ownerUuid.toString(), ownerName, null, null, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("create guild failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result disband(@Nonnull UUID actor) {
|
||||
GuildContext ctx = contextFor(actor);
|
||||
if (ctx == null) return Result.NOT_IN_GUILD;
|
||||
if (!ctx.rank.isOwnerRank()) return Result.OWNER_ONLY;
|
||||
try {
|
||||
invites.deleteAllForGuild(ctx.guild.id());
|
||||
members.removeAllForGuild(ctx.guild.id());
|
||||
ranks.deleteAllForGuild(ctx.guild.id());
|
||||
guilds.delete(ctx.guild.id());
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_DISBANDED, ctx.guild.id(), ctx.guild.name(),
|
||||
actor.toString(), ctx.member.name(), null, null, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("disband failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- invite / join / leave ----
|
||||
|
||||
@Nonnull
|
||||
public Result invite(@Nonnull UUID actor, @Nonnull UUID target, @Nonnull String targetName) {
|
||||
if (actor.equals(target)) return Result.SELF;
|
||||
GuildContext ctx = contextFor(actor);
|
||||
if (ctx == null) return Result.NOT_IN_GUILD;
|
||||
if (!GuildPermission.INVITE.isIn(ctx.rank.permissions())) return Result.NO_PERMISSION;
|
||||
try {
|
||||
if (members.find(target).isPresent()) return Result.ALREADY_IN_GUILD;
|
||||
if (members.countForGuild(ctx.guild.id()) >= config.guilds.max_members) return Result.AT_CAPACITY;
|
||||
if (!privacy.canContact(actor, target, PrivacyKey.GUILD_INVITE)) return Result.BLOCKED_BY_PRIVACY;
|
||||
|
||||
long id = invites.insert(ctx.guild.id(), ctx.guild.name(), target, actor, ctx.member.name());
|
||||
if (id <= 0) return Result.NO_INVITE; // already invited
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_INVITE, ctx.guild.id(), ctx.guild.name(),
|
||||
actor.toString(), ctx.member.name(),
|
||||
target.toString(), targetName, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("invite failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result acceptInvite(@Nonnull UUID actor, @Nonnull String actorName, long guildId) {
|
||||
try {
|
||||
if (members.find(actor).isPresent()) return Result.ALREADY_IN_GUILD;
|
||||
Optional<GuildInvite> inv = invites.find(guildId, actor);
|
||||
if (inv.isEmpty()) return Result.NO_INVITE;
|
||||
Optional<Guild> g = guilds.findById(guildId);
|
||||
if (g.isEmpty()) return Result.NOT_FOUND;
|
||||
if (members.countForGuild(guildId) >= config.guilds.max_members) return Result.AT_CAPACITY;
|
||||
|
||||
// Lowest-priority non-owner rank
|
||||
List<GuildRank> all = ranks.findAll(guildId);
|
||||
GuildRank lowest = null;
|
||||
for (GuildRank r : all) {
|
||||
if (r.isOwnerRank()) continue;
|
||||
if (lowest == null || r.priority() < lowest.priority()) lowest = r;
|
||||
}
|
||||
if (lowest == null) return Result.RANK_INVALID;
|
||||
|
||||
members.add(guildId, actor, actorName, lowest.rankId());
|
||||
invites.delete(guildId, actor);
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_MEMBER_JOIN, guildId, g.get().name(),
|
||||
actor.toString(), actorName, null, null, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("acceptInvite failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result leave(@Nonnull UUID actor) {
|
||||
GuildContext ctx = contextFor(actor);
|
||||
if (ctx == null) return Result.NOT_IN_GUILD;
|
||||
if (ctx.rank.isOwnerRank()) return Result.OWNER_RANK_LOCKED; // owner must disband or transfer
|
||||
try {
|
||||
members.remove(actor);
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_MEMBER_LEAVE, ctx.guild.id(), ctx.guild.name(),
|
||||
actor.toString(), ctx.member.name(), null, null, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("leave failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result kick(@Nonnull UUID actor, @Nonnull UUID target) {
|
||||
GuildContext actorCtx = contextFor(actor);
|
||||
if (actorCtx == null) return Result.NOT_IN_GUILD;
|
||||
if (!GuildPermission.KICK.isIn(actorCtx.rank.permissions())) return Result.NO_PERMISSION;
|
||||
GuildContext targetCtx = contextFor(target);
|
||||
if (targetCtx == null || targetCtx.guild.id() != actorCtx.guild.id()) return Result.NOT_IN_GUILD;
|
||||
if (targetCtx.rank.isOwnerRank()) return Result.OWNER_RANK_LOCKED;
|
||||
if (targetCtx.rank.priority() >= actorCtx.rank.priority()) return Result.NO_PERMISSION;
|
||||
try {
|
||||
members.remove(target);
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_MEMBER_KICK, actorCtx.guild.id(), actorCtx.guild.name(),
|
||||
actor.toString(), actorCtx.member.name(),
|
||||
target.toString(), targetCtx.member.name(), null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("kick failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result setRank(@Nonnull UUID actor, @Nonnull UUID target, int newRankId, boolean promote) {
|
||||
GuildContext actorCtx = contextFor(actor);
|
||||
if (actorCtx == null) return Result.NOT_IN_GUILD;
|
||||
boolean perm = promote
|
||||
? GuildPermission.PROMOTE.isIn(actorCtx.rank.permissions())
|
||||
: GuildPermission.DEMOTE.isIn(actorCtx.rank.permissions());
|
||||
if (!perm) return Result.NO_PERMISSION;
|
||||
|
||||
GuildContext targetCtx = contextFor(target);
|
||||
if (targetCtx == null || targetCtx.guild.id() != actorCtx.guild.id()) return Result.NOT_IN_GUILD;
|
||||
if (targetCtx.rank.isOwnerRank()) return Result.OWNER_RANK_LOCKED;
|
||||
if (targetCtx.rank.priority() >= actorCtx.rank.priority()) return Result.NO_PERMISSION;
|
||||
|
||||
try {
|
||||
Optional<GuildRank> newRank = ranks.find(actorCtx.guild.id(), newRankId);
|
||||
if (newRank.isEmpty()) return Result.RANK_INVALID;
|
||||
if (newRank.get().isOwnerRank()) return Result.OWNER_RANK_LOCKED;
|
||||
if (newRank.get().priority() >= actorCtx.rank.priority()) return Result.NO_PERMISSION;
|
||||
|
||||
members.setRank(target, newRankId);
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
promote ? GuildEventPayload.TYPE_PROMOTE : GuildEventPayload.TYPE_DEMOTE,
|
||||
actorCtx.guild.id(), actorCtx.guild.name(),
|
||||
actor.toString(), actorCtx.member.name(),
|
||||
target.toString(), targetCtx.member.name(),
|
||||
newRank.get().name(), serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("setRank failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result setMotd(@Nonnull UUID actor, @Nonnull String motd) {
|
||||
GuildContext ctx = contextFor(actor);
|
||||
if (ctx == null) return Result.NOT_IN_GUILD;
|
||||
if (!GuildPermission.MOTD.isIn(ctx.rank.permissions())) return Result.NO_PERMISSION;
|
||||
try {
|
||||
guilds.setMotd(ctx.guild.id(), motd);
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_MOTD, ctx.guild.id(), ctx.guild.name(),
|
||||
actor.toString(), ctx.member.name(), null, null, motd, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("setMotd failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- rank management ----
|
||||
|
||||
@Nonnull
|
||||
public Result createRank(@Nonnull UUID actor, @Nonnull String name, int priority, int permissions) {
|
||||
GuildContext ctx = contextFor(actor);
|
||||
if (ctx == null) return Result.NOT_IN_GUILD;
|
||||
if (!GuildPermission.MANAGE_RANKS.isIn(ctx.rank.permissions())) return Result.NO_PERMISSION;
|
||||
try {
|
||||
int nextId = ranks.nextRankId(ctx.guild.id());
|
||||
ranks.insert(new GuildRank(ctx.guild.id(), nextId, name, priority, permissions, false));
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_RANKS_CHANGED, ctx.guild.id(), ctx.guild.name(),
|
||||
actor.toString(), ctx.member.name(), null, null, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("createRank failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result deleteRank(@Nonnull UUID actor, int rankId) {
|
||||
GuildContext ctx = contextFor(actor);
|
||||
if (ctx == null) return Result.NOT_IN_GUILD;
|
||||
if (!GuildPermission.MANAGE_RANKS.isIn(ctx.rank.permissions())) return Result.NO_PERMISSION;
|
||||
try {
|
||||
Optional<GuildRank> r = ranks.find(ctx.guild.id(), rankId);
|
||||
if (r.isEmpty()) return Result.RANK_INVALID;
|
||||
if (r.get().isOwnerRank()) return Result.OWNER_RANK_LOCKED;
|
||||
ranks.delete(ctx.guild.id(), rankId);
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_RANKS_CHANGED, ctx.guild.id(), ctx.guild.name(),
|
||||
actor.toString(), ctx.member.name(), null, null, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("deleteRank failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Result updateRank(@Nonnull UUID actor, int rankId, @Nonnull String newName, int newPriority, int newPermissions) {
|
||||
GuildContext ctx = contextFor(actor);
|
||||
if (ctx == null) return Result.NOT_IN_GUILD;
|
||||
if (!GuildPermission.MANAGE_RANKS.isIn(ctx.rank.permissions())) return Result.NO_PERMISSION;
|
||||
try {
|
||||
Optional<GuildRank> r = ranks.find(ctx.guild.id(), rankId);
|
||||
if (r.isEmpty()) return Result.RANK_INVALID;
|
||||
if (r.get().isOwnerRank()) return Result.OWNER_RANK_LOCKED;
|
||||
ranks.update(ctx.guild.id(), rankId, newName, newPriority, newPermissions);
|
||||
bus.publish(config.channels.guild_event, new GuildEventPayload(
|
||||
GuildEventPayload.TYPE_RANKS_CHANGED, ctx.guild.id(), ctx.guild.name(),
|
||||
actor.toString(), ctx.member.name(), null, null, null, serverId));
|
||||
return Result.OK;
|
||||
} catch (SQLException e) {
|
||||
LOGGER.at(Level.WARNING).log("updateRank failed: %s", e.getMessage());
|
||||
return Result.DB_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- helpers ----
|
||||
|
||||
private boolean isValidName(@Nonnull String name) {
|
||||
int len = name.length();
|
||||
if (len < config.guilds.name_min_length || len > config.guilds.name_max_length) return false;
|
||||
for (int i = 0; i < name.length(); i++) {
|
||||
char ch = name.charAt(i);
|
||||
if (!Character.isLetterOrDigit(ch) && ch != '_' && ch != '-') return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package net.kewwbec.social.service;
|
||||
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import net.kewwbec.networkcore.api.MessageBus;
|
||||
import net.kewwbec.social.config.SocialConfig;
|
||||
import net.kewwbec.social.db.GuildMemberRepository;
|
||||
import net.kewwbec.social.model.GuildMember;
|
||||
import net.kewwbec.social.service.payload.FriendEventPayload;
|
||||
import net.kewwbec.social.service.payload.GuildEventPayload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.awt.Color;
|
||||
import java.sql.SQLException;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Subscribes to friend/guild event channels and surfaces in-chat notifications to the relevant
|
||||
* player if they are on this server. Cross-server delivery happens naturally because every
|
||||
* server runs this same subscriber.
|
||||
*/
|
||||
public final class NotificationDispatcher {
|
||||
|
||||
private final MessageBus bus;
|
||||
private final SocialConfig config;
|
||||
private final GuildMemberRepository members;
|
||||
|
||||
@Nullable private MessageBus.Subscription friendSub;
|
||||
@Nullable private MessageBus.Subscription guildSub;
|
||||
|
||||
public NotificationDispatcher(@Nonnull MessageBus bus,
|
||||
@Nonnull SocialConfig config,
|
||||
@Nonnull GuildMemberRepository members) {
|
||||
this.bus = bus;
|
||||
this.config = config;
|
||||
this.members = members;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
this.friendSub = bus.subscribe(config.channels.friend_event, FriendEventPayload.class, this::onFriend);
|
||||
this.guildSub = bus.subscribe(config.channels.guild_event, GuildEventPayload.class, this::onGuild);
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
if (friendSub != null) { try { friendSub.close(); } catch (RuntimeException ignored) {} friendSub = null; }
|
||||
if (guildSub != null) { try { guildSub.close(); } catch (RuntimeException ignored) {} guildSub = null; }
|
||||
}
|
||||
|
||||
private void onFriend(@Nonnull FriendEventPayload p) {
|
||||
if (p.type == null || p.toUuid == null) return;
|
||||
switch (p.type) {
|
||||
case FriendEventPayload.TYPE_REQUEST -> deliverTo(p.toUuid,
|
||||
p.fromName + " sent you a friend request. /friend accept " + p.fromName + " or /friend reject " + p.fromName,
|
||||
Color.YELLOW);
|
||||
case FriendEventPayload.TYPE_ACCEPTED -> {
|
||||
deliverTo(p.fromUuid, p.toName + " accepted your friend request.", Color.GREEN);
|
||||
deliverTo(p.toUuid, "You and " + p.fromName + " are now friends.", Color.GREEN);
|
||||
}
|
||||
case FriendEventPayload.TYPE_REMOVED -> {
|
||||
deliverTo(p.fromUuid, "Friendship with " + p.toName + " ended.", Color.GRAY);
|
||||
deliverTo(p.toUuid, "Friendship with " + p.fromName + " ended.", Color.GRAY);
|
||||
}
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private void onGuild(@Nonnull GuildEventPayload p) {
|
||||
if (p.type == null) return;
|
||||
switch (p.type) {
|
||||
case GuildEventPayload.TYPE_INVITE -> deliverTo(p.targetUuid,
|
||||
p.actorName + " invited you to '" + p.guildName + "'. /guild accept " + p.guildName,
|
||||
Color.YELLOW);
|
||||
case GuildEventPayload.TYPE_MEMBER_JOIN -> broadcastGuild(p,
|
||||
p.actorName + " joined the guild.", Color.GREEN);
|
||||
case GuildEventPayload.TYPE_MEMBER_LEAVE -> broadcastGuild(p,
|
||||
p.actorName + " left the guild.", Color.GRAY);
|
||||
case GuildEventPayload.TYPE_MEMBER_KICK -> broadcastGuild(p,
|
||||
p.targetName + " was kicked by " + p.actorName + ".", Color.YELLOW);
|
||||
case GuildEventPayload.TYPE_PROMOTE -> broadcastGuild(p,
|
||||
p.targetName + " was promoted to " + p.body + ".", Color.GREEN);
|
||||
case GuildEventPayload.TYPE_DEMOTE -> broadcastGuild(p,
|
||||
p.targetName + " was demoted to " + p.body + ".", Color.YELLOW);
|
||||
case GuildEventPayload.TYPE_MOTD -> broadcastGuild(p,
|
||||
"MOTD updated: " + p.body, Color.LIGHT_GRAY);
|
||||
case GuildEventPayload.TYPE_DISBANDED -> broadcastGuild(p,
|
||||
"Guild '" + p.guildName + "' was disbanded.", Color.RED);
|
||||
case GuildEventPayload.TYPE_RANKS_CHANGED -> broadcastGuild(p,
|
||||
"Guild ranks changed.", Color.LIGHT_GRAY);
|
||||
default -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private void deliverTo(@Nullable String uuidStr, @Nonnull String text, @Nonnull Color color) {
|
||||
if (uuidStr == null) return;
|
||||
UUID uuid;
|
||||
try { uuid = UUID.fromString(uuidStr); } catch (IllegalArgumentException e) { return; }
|
||||
Universe u = Universe.get();
|
||||
if (u == null) return;
|
||||
for (PlayerRef ref : u.getPlayers()) {
|
||||
if (ref.getUuid().equals(uuid)) {
|
||||
ref.sendMessage(Message.raw(text).color(color));
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void broadcastGuild(@Nonnull GuildEventPayload p, @Nonnull String text, @Nonnull Color color) {
|
||||
Universe u = Universe.get();
|
||||
if (u == null) return;
|
||||
for (PlayerRef ref : u.getPlayers()) {
|
||||
try {
|
||||
Optional<GuildMember> m = members.find(ref.getUuid());
|
||||
if (m.isEmpty() || m.get().guildId() != p.guildId) continue;
|
||||
ref.sendMessage(Message.raw(text).color(color));
|
||||
} catch (SQLException ignored) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package net.kewwbec.social.service;
|
||||
|
||||
import net.kewwbec.social.db.FriendRepository;
|
||||
import net.kewwbec.social.db.PrivacyRepository;
|
||||
import net.kewwbec.social.model.PrivacyKey;
|
||||
import net.kewwbec.social.model.PrivacySettings;
|
||||
import net.kewwbec.social.model.PrivacyValue;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.sql.SQLException;
|
||||
import java.util.UUID;
|
||||
|
||||
public final class PrivacyService {
|
||||
|
||||
private final PrivacyRepository repo;
|
||||
private final FriendRepository friends;
|
||||
|
||||
public PrivacyService(@Nonnull PrivacyRepository repo, @Nonnull FriendRepository friends) {
|
||||
this.repo = repo;
|
||||
this.friends = friends;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PrivacySettings get(@Nonnull UUID uuid) throws SQLException {
|
||||
return repo.get(uuid);
|
||||
}
|
||||
|
||||
public void set(@Nonnull UUID uuid, @Nonnull PrivacyKey key, @Nonnull PrivacyValue value) throws SQLException {
|
||||
repo.set(uuid, key, value);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if {@code from} is allowed to do action {@code key} towards {@code to}.
|
||||
*/
|
||||
public boolean canContact(@Nonnull UUID from, @Nonnull UUID to, @Nonnull PrivacyKey key) throws SQLException {
|
||||
PrivacyValue policy = repo.get(to).get(key);
|
||||
return switch (policy) {
|
||||
case EVERYONE -> true;
|
||||
case NOBODY -> false;
|
||||
case FRIENDS -> friends.findAccepted(from, to).isPresent();
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.kewwbec.social.service.payload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public final class DmPayload {
|
||||
|
||||
public String senderUuid;
|
||||
public String senderName;
|
||||
public String recipientUuid;
|
||||
public String recipientName;
|
||||
public String body;
|
||||
public String fromServerId;
|
||||
|
||||
public DmPayload() {}
|
||||
|
||||
public DmPayload(@Nonnull String senderUuid, @Nonnull String senderName,
|
||||
@Nonnull String recipientUuid, @Nonnull String recipientName,
|
||||
@Nonnull String body, @Nonnull String fromServerId) {
|
||||
this.senderUuid = senderUuid;
|
||||
this.senderName = senderName;
|
||||
this.recipientUuid = recipientUuid;
|
||||
this.recipientName = recipientName;
|
||||
this.body = body;
|
||||
this.fromServerId = fromServerId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package net.kewwbec.social.service.payload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Cross-server notification of a friend-related event.
|
||||
* Servers receive these to surface in-game prompts to the relevant online player.
|
||||
*/
|
||||
public final class FriendEventPayload {
|
||||
|
||||
public String type;
|
||||
public String fromUuid;
|
||||
public String fromName;
|
||||
public String toUuid;
|
||||
public String toName;
|
||||
public String fromServerId;
|
||||
|
||||
public FriendEventPayload() {}
|
||||
|
||||
public FriendEventPayload(@Nonnull String type,
|
||||
@Nonnull String fromUuid, @Nonnull String fromName,
|
||||
@Nonnull String toUuid, @Nonnull String toName,
|
||||
@Nullable String fromServerId) {
|
||||
this.type = type;
|
||||
this.fromUuid = fromUuid;
|
||||
this.fromName = fromName;
|
||||
this.toUuid = toUuid;
|
||||
this.toName = toName;
|
||||
this.fromServerId = fromServerId;
|
||||
}
|
||||
|
||||
public static final String TYPE_REQUEST = "REQUEST";
|
||||
public static final String TYPE_ACCEPTED = "ACCEPTED";
|
||||
public static final String TYPE_REMOVED = "REMOVED";
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package net.kewwbec.social.service.payload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public final class GuildChatPayload {
|
||||
|
||||
public long guildId;
|
||||
public String guildName;
|
||||
public String senderUuid;
|
||||
public String senderName;
|
||||
public String body;
|
||||
public String fromServerId;
|
||||
|
||||
public GuildChatPayload() {}
|
||||
|
||||
public GuildChatPayload(long guildId, @Nonnull String guildName,
|
||||
@Nonnull String senderUuid, @Nonnull String senderName,
|
||||
@Nonnull String body, @Nonnull String fromServerId) {
|
||||
this.guildId = guildId;
|
||||
this.guildName = guildName;
|
||||
this.senderUuid = senderUuid;
|
||||
this.senderName = senderName;
|
||||
this.body = body;
|
||||
this.fromServerId = fromServerId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package net.kewwbec.social.service.payload;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class GuildEventPayload {
|
||||
|
||||
public String type;
|
||||
public long guildId;
|
||||
public String guildName;
|
||||
public String actorUuid;
|
||||
public String actorName;
|
||||
public String targetUuid;
|
||||
public String targetName;
|
||||
public String body;
|
||||
public String fromServerId;
|
||||
|
||||
public GuildEventPayload() {}
|
||||
|
||||
public GuildEventPayload(@Nonnull String type, long guildId, @Nonnull String guildName,
|
||||
@Nullable String actorUuid, @Nullable String actorName,
|
||||
@Nullable String targetUuid, @Nullable String targetName,
|
||||
@Nullable String body, @Nullable String fromServerId) {
|
||||
this.type = type;
|
||||
this.guildId = guildId;
|
||||
this.guildName = guildName;
|
||||
this.actorUuid = actorUuid;
|
||||
this.actorName = actorName;
|
||||
this.targetUuid = targetUuid;
|
||||
this.targetName = targetName;
|
||||
this.body = body;
|
||||
this.fromServerId = fromServerId;
|
||||
}
|
||||
|
||||
public static final String TYPE_INVITE = "INVITE";
|
||||
public static final String TYPE_MEMBER_JOIN = "MEMBER_JOIN";
|
||||
public static final String TYPE_MEMBER_LEAVE = "MEMBER_LEAVE";
|
||||
public static final String TYPE_MEMBER_KICK = "MEMBER_KICK";
|
||||
public static final String TYPE_PROMOTE = "PROMOTE";
|
||||
public static final String TYPE_DEMOTE = "DEMOTE";
|
||||
public static final String TYPE_MOTD = "MOTD";
|
||||
public static final String TYPE_DISBANDED = "DISBANDED";
|
||||
public static final String TYPE_RANKS_CHANGED = "RANKS_CHANGED";
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package net.kewwbec.social.ui;
|
||||
|
||||
import au.ellie.hyui.builders.PageBuilder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.networkcore.NetworkCore;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||
import net.kewwbec.social.model.Friendship;
|
||||
import net.kewwbec.social.service.FriendService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.sql.SQLException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class FriendsListPage {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
public enum Tab { FRIENDS, INCOMING, OUTGOING }
|
||||
|
||||
private final PlayerRef playerRef;
|
||||
private final SocialUIContext ctx;
|
||||
private final Tab tab;
|
||||
private final PageBuilder page;
|
||||
|
||||
public FriendsListPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx) {
|
||||
this(playerRef, ctx, Tab.FRIENDS);
|
||||
}
|
||||
|
||||
public FriendsListPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx, @Nonnull Tab tab) {
|
||||
this.playerRef = playerRef;
|
||||
this.ctx = ctx;
|
||||
this.tab = tab;
|
||||
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("<p>Tabs:</p>");
|
||||
body.append("<button id=\"tabFriends\">").append(marker(tab, Tab.FRIENDS)).append("Friends</button>");
|
||||
body.append("<button id=\"tabIncoming\">").append(marker(tab, Tab.INCOMING)).append("Incoming</button>");
|
||||
body.append("<button id=\"tabOutgoing\">").append(marker(tab, Tab.OUTGOING)).append("Outgoing</button>");
|
||||
body.append("<p> </p>");
|
||||
|
||||
List<RowAction> actions = new ArrayList<>();
|
||||
|
||||
try {
|
||||
switch (tab) {
|
||||
case FRIENDS -> renderFriends(body, actions);
|
||||
case INCOMING -> renderIncoming(body, actions);
|
||||
case OUTGOING -> renderOutgoing(body, actions);
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Friends panel render failed");
|
||||
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
|
||||
}
|
||||
|
||||
body.append("<p> </p>");
|
||||
body.append("<button id=\"backBtn\">Back to Social</button>");
|
||||
|
||||
String html = """
|
||||
<div class="page-overlay">
|
||||
<div class="container" data-hyui-title="Friends">
|
||||
<div class="container-contents">
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(body.toString());
|
||||
|
||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||
.fromHtml(html);
|
||||
|
||||
page.addEventListener("tabFriends", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new FriendsListPage(playerRef, ctx, Tab.FRIENDS).open(s)));
|
||||
page.addEventListener("tabIncoming", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new FriendsListPage(playerRef, ctx, Tab.INCOMING).open(s)));
|
||||
page.addEventListener("tabOutgoing", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new FriendsListPage(playerRef, ctx, Tab.OUTGOING).open(s)));
|
||||
page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new SocialHubPage(playerRef, ctx).open(s)));
|
||||
|
||||
for (RowAction a : actions) {
|
||||
String btnId = a.buttonId;
|
||||
Tab cur = this.tab;
|
||||
page.addEventListener(btnId, CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
|
||||
a.runnable.run();
|
||||
openInStore(s -> new FriendsListPage(playerRef, ctx, cur).open(s));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void renderFriends(@Nonnull StringBuilder body, @Nonnull List<RowAction> actions) throws SQLException {
|
||||
List<Friendship> friends = ctx.friends.list(playerRef.getUuid());
|
||||
body.append("<p><b>Friends (").append(friends.size()).append(")</b></p>");
|
||||
if (friends.isEmpty()) {
|
||||
body.append("<p>None yet. Use /friend add <name> to send a request.</p>");
|
||||
return;
|
||||
}
|
||||
PlayerPresence presence = NetworkCore.getInstance().getPlayerPresence();
|
||||
int i = 0;
|
||||
for (Friendship f : friends) {
|
||||
UUID otherUuid = f.fromUuid().equals(playerRef.getUuid()) ? f.toUuid() : f.fromUuid();
|
||||
String otherName = f.fromUuid().equals(playerRef.getUuid()) ? f.toName() : f.fromName();
|
||||
PlayerLocation loc = (presence == null) ? null : presence.getLocation(otherUuid);
|
||||
String status = (loc == null) ? "offline" : "on " + loc.serverId();
|
||||
body.append("<p>").append(escape(otherName)).append(" (").append(escape(status)).append(")</p>");
|
||||
String removeId = "removeFriend" + i;
|
||||
body.append("<button id=\"").append(removeId).append("\">Remove ").append(escape(otherName)).append("</button>");
|
||||
UUID finalOther = otherUuid;
|
||||
String finalName = otherName;
|
||||
actions.add(new RowAction(removeId, () ->
|
||||
ctx.friends.remove(playerRef.getUuid(), playerRef.getUsername(), finalOther, finalName)
|
||||
));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private void renderIncoming(@Nonnull StringBuilder body, @Nonnull List<RowAction> actions) throws SQLException {
|
||||
List<Friendship> incoming = ctx.friends.incomingPending(playerRef.getUuid());
|
||||
body.append("<p><b>Incoming requests (").append(incoming.size()).append(")</b></p>");
|
||||
if (incoming.isEmpty()) {
|
||||
body.append("<p>No one has sent you a friend request.</p>");
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
for (Friendship f : incoming) {
|
||||
UUID fromUuid = f.fromUuid();
|
||||
String fromName = f.fromName();
|
||||
body.append("<p>from ").append(escape(fromName)).append("</p>");
|
||||
String acceptId = "accept" + i;
|
||||
String rejectId = "reject" + i;
|
||||
body.append("<button id=\"").append(acceptId).append("\">Accept ").append(escape(fromName)).append("</button>");
|
||||
body.append("<button id=\"").append(rejectId).append("\">Reject ").append(escape(fromName)).append("</button>");
|
||||
actions.add(new RowAction(acceptId, () ->
|
||||
ctx.friends.accept(fromUuid, fromName, playerRef.getUuid(), playerRef.getUsername())
|
||||
));
|
||||
actions.add(new RowAction(rejectId, () ->
|
||||
ctx.friends.reject(fromUuid, playerRef.getUuid())
|
||||
));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
private void renderOutgoing(@Nonnull StringBuilder body, @Nonnull List<RowAction> actions) throws SQLException {
|
||||
List<Friendship> outgoing = ctx.friends.outgoingPending(playerRef.getUuid());
|
||||
body.append("<p><b>Outgoing requests (").append(outgoing.size()).append(")</b></p>");
|
||||
if (outgoing.isEmpty()) {
|
||||
body.append("<p>No outgoing requests.</p>");
|
||||
return;
|
||||
}
|
||||
int i = 0;
|
||||
for (Friendship f : outgoing) {
|
||||
UUID toUuid = f.toUuid();
|
||||
String toName = f.toName();
|
||||
body.append("<p>to ").append(escape(toName)).append("</p>");
|
||||
String cancelId = "cancel" + i;
|
||||
body.append("<button id=\"").append(cancelId).append("\">Cancel request to ").append(escape(toName)).append("</button>");
|
||||
actions.add(new RowAction(cancelId, () ->
|
||||
ctx.friends.remove(playerRef.getUuid(), playerRef.getUsername(), toUuid, toName)
|
||||
));
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
public void open(@Nonnull Store<EntityStore> store) {
|
||||
page.open(store);
|
||||
}
|
||||
|
||||
private void openInStore(@Nonnull Consumer<Store<EntityStore>> action) {
|
||||
Ref<EntityStore> ref = playerRef.getReference();
|
||||
if (ref == null) return;
|
||||
action.accept(ref.getStore());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String marker(@Nonnull Tab current, @Nonnull Tab target) {
|
||||
return current == target ? "[*] " : "[ ] ";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String escape(@Nonnull String s) {
|
||||
return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
|
||||
private record RowAction(@Nonnull String buttonId, @Nonnull Runnable runnable) {}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package net.kewwbec.social.ui;
|
||||
|
||||
import au.ellie.hyui.builders.PageBuilder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.networkcore.NetworkCore;
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||
import net.kewwbec.social.model.GuildMember;
|
||||
import net.kewwbec.social.model.GuildRank;
|
||||
import net.kewwbec.social.service.GuildService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.sql.SQLException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class GuildRosterPage {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final PlayerRef playerRef;
|
||||
private final SocialUIContext ctx;
|
||||
private final PageBuilder page;
|
||||
|
||||
public GuildRosterPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx) {
|
||||
this.playerRef = playerRef;
|
||||
this.ctx = ctx;
|
||||
|
||||
StringBuilder body = new StringBuilder();
|
||||
GuildService.GuildContext gc = ctx.guilds.contextFor(playerRef.getUuid());
|
||||
String title = "Guild";
|
||||
if (gc == null) {
|
||||
body.append("<p>You are not in a guild.</p>");
|
||||
body.append("<p> </p>");
|
||||
body.append("<p>To get started:</p>");
|
||||
body.append("<p>- /guild create <name></p>");
|
||||
body.append("<p>- /guild accept <guild-name> (if you have an invite)</p>");
|
||||
} else {
|
||||
title = "Guild: " + gc.guild().name();
|
||||
body.append("<p><b>").append(escape(gc.guild().name())).append("</b></p>");
|
||||
body.append("<p>Owner: ").append(escape(gc.guild().ownerName())).append("</p>");
|
||||
String motd = gc.guild().motd();
|
||||
body.append("<p>MOTD: ").append(motd.isBlank() ? "(none)" : escape(motd)).append("</p>");
|
||||
body.append("<p>Your rank: <b>").append(escape(gc.rank().name())).append("</b> (priority ")
|
||||
.append(gc.rank().priority()).append(")</p>");
|
||||
body.append("<p> </p>");
|
||||
|
||||
try {
|
||||
List<GuildRank> ranks = ctx.guilds.rankList(gc.guild().id());
|
||||
Map<Integer, String> rankNames = new HashMap<>();
|
||||
for (GuildRank r : ranks) rankNames.put(r.rankId(), r.name());
|
||||
|
||||
List<GuildMember> roster = ctx.guilds.roster(gc.guild().id());
|
||||
int online = 0;
|
||||
PlayerPresence presence = NetworkCore.getInstance().getPlayerPresence();
|
||||
if (presence != null) {
|
||||
for (GuildMember m : roster) {
|
||||
if (presence.getLocation(m.uuid()) != null) online++;
|
||||
}
|
||||
}
|
||||
|
||||
body.append("<p><b>Members: ").append(roster.size())
|
||||
.append(" total, ").append(online).append(" online</b></p>");
|
||||
for (GuildMember m : roster) {
|
||||
String rankName = rankNames.getOrDefault(m.rankId(), "?");
|
||||
String self = m.uuid().equals(playerRef.getUuid()) ? " (you)" : "";
|
||||
PlayerLocation loc = (presence == null) ? null : presence.getLocation(m.uuid());
|
||||
String status = (loc == null) ? "offline" : "on " + loc.serverId();
|
||||
body.append("<p>")
|
||||
.append(escape(m.name()))
|
||||
.append(self)
|
||||
.append(" - ")
|
||||
.append(escape(rankName))
|
||||
.append(" - ")
|
||||
.append(escape(status))
|
||||
.append("</p>");
|
||||
}
|
||||
|
||||
body.append("<p> </p>");
|
||||
body.append("<p><b>Ranks (highest first)</b></p>");
|
||||
for (GuildRank r : ranks) {
|
||||
body.append("<p>")
|
||||
.append(r.rankId()).append(". ")
|
||||
.append(escape(r.name()))
|
||||
.append(" - prio ").append(r.priority())
|
||||
.append(r.isOwnerRank() ? " [OWNER]" : "")
|
||||
.append("</p>");
|
||||
}
|
||||
} catch (SQLException e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Roster fetch failed");
|
||||
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
|
||||
}
|
||||
}
|
||||
|
||||
body.append("<p> </p>");
|
||||
body.append("<button id=\"backBtn\">Back to Social</button>");
|
||||
|
||||
String html = """
|
||||
<div class="page-overlay">
|
||||
<div class="container" data-hyui-title="%s">
|
||||
<div class="container-contents">
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(escape(title), body.toString());
|
||||
|
||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||
.fromHtml(html);
|
||||
|
||||
page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new SocialHubPage(playerRef, ctx).open(s)));
|
||||
}
|
||||
|
||||
public void open(@Nonnull Store<EntityStore> store) {
|
||||
page.open(store);
|
||||
}
|
||||
|
||||
private void openInStore(@Nonnull Consumer<Store<EntityStore>> action) {
|
||||
Ref<EntityStore> ref = playerRef.getReference();
|
||||
if (ref == null) return;
|
||||
action.accept(ref.getStore());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String escape(@Nonnull String s) {
|
||||
return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.kewwbec.social.ui;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Standard outer wrapper for every Social HyUI page: 800x600 decorated container, scrolling contents.
|
||||
*/
|
||||
public final class PageLayout {
|
||||
|
||||
public static final int DEFAULT_WIDTH = 800;
|
||||
public static final int DEFAULT_HEIGHT = 600;
|
||||
|
||||
private PageLayout() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String wrap(@Nonnull String title, @Nonnull String contentsHtml) {
|
||||
return wrap(title, contentsHtml, DEFAULT_WIDTH, DEFAULT_HEIGHT);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String wrap(@Nonnull String title, @Nonnull String contentsHtml, int width, int height) {
|
||||
return """
|
||||
<div class="page-overlay">
|
||||
<div class="decorated-container" data-hyui-title="%s" style="anchor-width: %d; anchor-height: %d;">
|
||||
<div class="container-contents" style="layout-mode: topscrolling;">
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(escape(title), width, height, contentsHtml);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String escape(@Nonnull String s) {
|
||||
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package net.kewwbec.social.ui;
|
||||
|
||||
import au.ellie.hyui.builders.PageBuilder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.social.model.PrivacyKey;
|
||||
import net.kewwbec.social.model.PrivacySettings;
|
||||
import net.kewwbec.social.model.PrivacyValue;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.sql.SQLException;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class PrivacySettingsPage {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final PlayerRef playerRef;
|
||||
private final SocialUIContext ctx;
|
||||
private final PageBuilder page;
|
||||
|
||||
public PrivacySettingsPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx) {
|
||||
this.playerRef = playerRef;
|
||||
this.ctx = ctx;
|
||||
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("<p>Click a setting to cycle through values.</p>");
|
||||
body.append("<p><b>EVERYONE</b> = anyone, <b>FRIENDS</b> = friends only, <b>NOBODY</b> = block all.</p>");
|
||||
body.append("<p> </p>");
|
||||
|
||||
PrivacySettings settings;
|
||||
try {
|
||||
settings = ctx.privacy.get(playerRef.getUuid());
|
||||
} catch (SQLException e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Privacy fetch failed");
|
||||
body.append("<p>DB error: ").append(escape(e.getMessage())).append("</p>");
|
||||
settings = PrivacySettings.defaults(playerRef.getUuid());
|
||||
}
|
||||
|
||||
for (PrivacyKey k : PrivacyKey.values()) {
|
||||
PrivacyValue v = settings.get(k);
|
||||
String label = describe(k) + ": " + v.name();
|
||||
body.append("<p>").append(escape(label)).append("</p>");
|
||||
body.append("<button id=\"cycle").append(k.name()).append("\">Cycle ")
|
||||
.append(escape(describe(k))).append(" -> ")
|
||||
.append(next(v).name())
|
||||
.append("</button>");
|
||||
}
|
||||
|
||||
body.append("<p> </p>");
|
||||
body.append("<button id=\"backBtn\">Back to Social</button>");
|
||||
|
||||
String html = """
|
||||
<div class="page-overlay">
|
||||
<div class="container" data-hyui-title="Privacy">
|
||||
<div class="container-contents">
|
||||
%s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(body.toString());
|
||||
|
||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||
.fromHtml(html);
|
||||
|
||||
for (PrivacyKey k : PrivacyKey.values()) {
|
||||
final PrivacyValue cur = settings.get(k);
|
||||
final PrivacyKey key = k;
|
||||
page.addEventListener("cycle" + k.name(), CustomUIEventBindingType.Activating, (ignored, pCtx) -> {
|
||||
try {
|
||||
ctx.privacy.set(playerRef.getUuid(), key, next(cur));
|
||||
} catch (SQLException e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e)).log("Privacy update failed");
|
||||
}
|
||||
openInStore(s -> new PrivacySettingsPage(playerRef, ctx).open(s));
|
||||
});
|
||||
}
|
||||
|
||||
page.addEventListener("backBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new SocialHubPage(playerRef, ctx).open(s)));
|
||||
}
|
||||
|
||||
public void open(@Nonnull Store<EntityStore> store) {
|
||||
page.open(store);
|
||||
}
|
||||
|
||||
private void openInStore(@Nonnull Consumer<Store<EntityStore>> action) {
|
||||
Ref<EntityStore> ref = playerRef.getReference();
|
||||
if (ref == null) return;
|
||||
action.accept(ref.getStore());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static PrivacyValue next(@Nonnull PrivacyValue cur) {
|
||||
return switch (cur) {
|
||||
case EVERYONE -> PrivacyValue.FRIENDS;
|
||||
case FRIENDS -> PrivacyValue.NOBODY;
|
||||
case NOBODY -> PrivacyValue.EVERYONE;
|
||||
};
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String describe(@Nonnull PrivacyKey k) {
|
||||
return switch (k) {
|
||||
case DM -> "Who can DM me";
|
||||
case FRIEND_REQUEST -> "Who can friend-request me";
|
||||
case GUILD_INVITE -> "Who can invite me to a guild";
|
||||
};
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String escape(@Nonnull String s) {
|
||||
return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package net.kewwbec.social.ui;
|
||||
|
||||
import au.ellie.hyui.builders.PageBuilder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomPageLifetime;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.CustomUIEventBindingType;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.social.service.GuildService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.sql.SQLException;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
public final class SocialHubPage {
|
||||
|
||||
private final PlayerRef playerRef;
|
||||
private final SocialUIContext ctx;
|
||||
private final PageBuilder page;
|
||||
|
||||
public SocialHubPage(@Nonnull PlayerRef playerRef, @Nonnull SocialUIContext ctx) {
|
||||
this.playerRef = playerRef;
|
||||
this.ctx = ctx;
|
||||
|
||||
int friendCount = countFriends();
|
||||
int incoming = countIncomingRequests();
|
||||
String guildLine = guildSummary();
|
||||
|
||||
String html = """
|
||||
<div class="page-overlay">
|
||||
<div class="container" data-hyui-title="Social">
|
||||
<div class="container-contents">
|
||||
<p>Signed in as %s.</p>
|
||||
<p> </p>
|
||||
<p><b>Friends</b></p>
|
||||
<button id="friendsBtn">Friends List - %d friend(s), %d pending request(s)</button>
|
||||
<p><b>Guild</b></p>
|
||||
<button id="guildBtn">%s</button>
|
||||
<p><b>Privacy</b></p>
|
||||
<button id="privacyBtn">Settings - who can DM, friend-request, or invite you</button>
|
||||
<p> </p>
|
||||
<button id="closeBtn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
""".formatted(escape(playerRef.getUsername()), friendCount, incoming, escape(guildLine));
|
||||
|
||||
this.page = PageBuilder.pageForPlayer(playerRef)
|
||||
.withLifetime(CustomPageLifetime.CanDismiss)
|
||||
.fromHtml(html);
|
||||
|
||||
page.addEventListener("friendsBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new FriendsListPage(playerRef, ctx).open(s)));
|
||||
page.addEventListener("guildBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new GuildRosterPage(playerRef, ctx).open(s)));
|
||||
page.addEventListener("privacyBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) ->
|
||||
openInStore(s -> new PrivacySettingsPage(playerRef, ctx).open(s)));
|
||||
page.addEventListener("closeBtn", CustomUIEventBindingType.Activating, (ignored, pCtx) -> {});
|
||||
}
|
||||
|
||||
public void open(@Nonnull Store<EntityStore> store) {
|
||||
page.open(playerRef, store);
|
||||
}
|
||||
|
||||
private int countFriends() {
|
||||
try { return ctx.friends.list(playerRef.getUuid()).size(); }
|
||||
catch (SQLException e) { return 0; }
|
||||
}
|
||||
|
||||
private int countIncomingRequests() {
|
||||
try { return ctx.friends.incomingPending(playerRef.getUuid()).size(); }
|
||||
catch (SQLException e) { return 0; }
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private String guildSummary() {
|
||||
GuildService.GuildContext gc = ctx.guilds.contextFor(playerRef.getUuid());
|
||||
if (gc == null) return "Roster - you are not in a guild yet";
|
||||
return "Roster - " + gc.guild().name() + " (your rank: " + gc.rank().name() + ")";
|
||||
}
|
||||
|
||||
private void openInStore(@Nonnull Consumer<Store<EntityStore>> action) {
|
||||
Ref<EntityStore> ref = playerRef.getReference();
|
||||
if (ref == null) return;
|
||||
action.accept(ref.getStore());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String escape(@Nonnull String s) {
|
||||
return s == null ? "" : s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.kewwbec.social.ui;
|
||||
|
||||
import net.kewwbec.social.service.FriendService;
|
||||
import net.kewwbec.social.service.GuildService;
|
||||
import net.kewwbec.social.service.PrivacyService;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public final class SocialUIContext {
|
||||
|
||||
public final FriendService friends;
|
||||
public final GuildService guilds;
|
||||
public final PrivacyService privacy;
|
||||
|
||||
public SocialUIContext(@Nonnull FriendService friends,
|
||||
@Nonnull GuildService guilds,
|
||||
@Nonnull PrivacyService privacy) {
|
||||
this.friends = friends;
|
||||
this.guilds = guilds;
|
||||
this.privacy = privacy;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package net.kewwbec.social.util;
|
||||
|
||||
import net.kewwbec.networkcore.api.PlayerLocation;
|
||||
import net.kewwbec.networkcore.api.PlayerPresence;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Resolves a username to a (uuid, name, serverId) via NetworkCore's PlayerPresence.
|
||||
* Only resolves online (network-wide) players. Offline lookup is out of scope for v1;
|
||||
* Social commands target online players.
|
||||
*/
|
||||
public final class PlayerResolver {
|
||||
|
||||
private final PlayerPresence presence;
|
||||
|
||||
public PlayerResolver(@Nonnull PlayerPresence presence) {
|
||||
this.presence = presence;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerLocation resolve(@Nonnull String username) {
|
||||
return presence.findByName(username);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerLocation resolveByUuid(@Nonnull UUID uuid) {
|
||||
return presence.getLocation(uuid);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Group": "kewwbec",
|
||||
"Name": "Social",
|
||||
"Version": "0.1.0",
|
||||
"Description": "Social: friends, guilds, direct messages, privacy",
|
||||
"IncludesAssetPack": false,
|
||||
"Authors": [
|
||||
{"Name": "kewwbec"}
|
||||
],
|
||||
"ServerVersion": "*",
|
||||
"Dependencies": {
|
||||
"kewwbec:NetworkCore": "*"
|
||||
},
|
||||
"OptionalDependencies": {},
|
||||
"DisabledByDefault": false,
|
||||
"Main": "net.kewwbec.social.SocialPlugin"
|
||||
}
|
||||
Reference in New Issue
Block a user