mirror of
https://github.com/Empostor/Empostor.git
synced 2026-10-20 01:46:07 +00:00
Update Webhook
This commit is contained in:
1 parent
04059279f6
commit
24eb5d3371
24 files changed
+136
-1472
No files matched your search
@@ -1,175 +0,0 @@
|
||||
# Admin Panel
|
||||
|
||||
Empostor includes a built-in web-based admin panel available at `http://your-server:22023/admin`.
|
||||
|
||||
## Authentication
|
||||
|
||||
The panel is protected by a password set in `config.json`:
|
||||
|
||||
```json
|
||||
"Admin": {
|
||||
"Password": "your-strong-password-here",
|
||||
"MarketplaceUrl": "https://raw.githubusercontent.com/your-org/your-repo/main/marketplace/plugins.json"
|
||||
}
|
||||
```
|
||||
|
||||
The password is hashed (SHA256) in memory and in cookies — the plaintext is never stored in the browser or compared directly. On first visit the login page is shown. After a successful login a session cookie (HttpOnly, Secure, SameSite=Strict) valid for 8 hours is set. Clicking **Sign out** clears it immediately.
|
||||
|
||||
After 5 failed login attempts from the same IP, that IP is locked out for 15 minutes.
|
||||
|
||||
All API endpoints under `/api/admin/` return `401 Unauthorized` if the cookie is absent or incorrect.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The main dashboard shows at a glance:
|
||||
|
||||
- Total and public game count
|
||||
- Active games (in progress)
|
||||
- Connected player count
|
||||
- Total ban count (IP + friend code)
|
||||
- Server uptime
|
||||
|
||||
A live game list is displayed below the stat cards, refreshing every 3 seconds.
|
||||
|
||||
---
|
||||
|
||||
## Games
|
||||
|
||||
Lists all active rooms with:
|
||||
|
||||
- Room code
|
||||
- State (NotStarted / Starting / Started / Ended)
|
||||
- Visibility (Public / Private)
|
||||
- Map name
|
||||
- Player count vs. max players
|
||||
- Host name and friend code
|
||||
- All players in the room (hover for friend code and IP)
|
||||
|
||||
---
|
||||
|
||||
## Clients
|
||||
|
||||
Lists all connected clients with:
|
||||
|
||||
- Client ID
|
||||
- Name
|
||||
- Friend code
|
||||
- IP address
|
||||
- Among Us client version
|
||||
- Platform
|
||||
- Whether they are in a game, and which room code
|
||||
- Reactor mod count badge (if the client runs Reactor)
|
||||
|
||||
Clicking **Detail** on any client row opens a side panel showing:
|
||||
|
||||
- Full player identity (name, friend code, PUID, IP, client version, language, platform)
|
||||
- Current game code
|
||||
- Reactor protocol version and full mod list (mod ID, version, required flag)
|
||||
|
||||
---
|
||||
|
||||
## Broadcast
|
||||
|
||||
Sends a chat message to every active game on behalf of the host of that game. The message is prefixed with `[Server]`.
|
||||
|
||||
---
|
||||
|
||||
## Kick
|
||||
|
||||
Removes a player from the server without banning. The player can reconnect immediately.
|
||||
|
||||
Players can be kicked by entering a Client ID, or by using the **Quick Kick** table which lists all connected clients with a one-click button.
|
||||
|
||||
---
|
||||
|
||||
## Ban
|
||||
|
||||
### Ban by IP
|
||||
|
||||
Bans an IP address and immediately disconnects all clients connected from that address. The reason is saved to `bans.json` and persists across server restarts.
|
||||
|
||||
### Ban by Friend Code
|
||||
|
||||
Bans a specific friend code and immediately disconnects any client currently connected with it.
|
||||
|
||||
Both ban types can be removed from the **Ban List** tab at any time.
|
||||
|
||||
---
|
||||
|
||||
## Ban List
|
||||
|
||||
Shows all active IP bans and friend code bans with:
|
||||
|
||||
- Banned value (IP or friend code)
|
||||
- Reason
|
||||
- Timestamp
|
||||
|
||||
Each entry has an **Unban** button that takes effect immediately.
|
||||
|
||||
---
|
||||
|
||||
## Message
|
||||
|
||||
Sends a targeted chat message to a specific game room by code. Useful for communicating with a single room without broadcasting server-wide.
|
||||
|
||||
---
|
||||
|
||||
## End Game
|
||||
|
||||
Force-terminates a game by kicking all players in it. The room is destroyed immediately. A confirmation dialog is shown before the action is executed.
|
||||
|
||||
The tab also lists all active games with a one-click **End** button per row.
|
||||
|
||||
---
|
||||
|
||||
## Privacy
|
||||
|
||||
Toggles a game between Public and Private. This affects whether the room appears in the public lobby list.
|
||||
|
||||
---
|
||||
|
||||
## Plugin Marketplace
|
||||
|
||||
Fetches the plugin list from the URL configured in `Admin.MarketplaceUrl` (a raw GitHub JSON file). For each plugin, the latest version, supported Empostor version, and description are shown.
|
||||
|
||||
Clicking **Install** downloads the `.dll` to the `plugins/` folder. The server must be restarted to load the new plugin.
|
||||
|
||||
If the plugin was built for a different Empostor version, a warning is displayed but installation is not blocked.
|
||||
|
||||
---
|
||||
|
||||
## Updates
|
||||
|
||||
Queries the GitHub Releases API for the latest Empostor release and compares it with the running version. If a newer version is available, a link to the release page is shown. The server does not update itself automatically.
|
||||
|
||||
The GitHub repository checked is `Empostor/Empostor` by default. If you maintain a fork, this is hardcoded in `MarketplaceController.cs`.
|
||||
|
||||
---
|
||||
|
||||
## Reports
|
||||
|
||||
Shows the last 200 player reports submitted in-game (via the Among Us report button):
|
||||
|
||||
| Column | Description |
|
||||
|--------|-------------|
|
||||
| Time | UTC timestamp of the report |
|
||||
| Game | Room code |
|
||||
| Reporter | Name and friend code of the reporting player |
|
||||
| Reported | Name and friend code of the reported player |
|
||||
| Reason | `InappropriateName`, `InappropriateChat`, `Cheating_Hacking`, `Harassment_Misconduct` |
|
||||
| Outcome | `NotReportedUnknown`, `NotReportedNoAccount`, `Reported`, etc. |
|
||||
|
||||
Reports are held in memory and cleared on server restart. Plugins can listen to `IPlayerReportEvent` to persist reports or trigger further actions.
|
||||
|
||||
---
|
||||
|
||||
## Server Info
|
||||
|
||||
Displays runtime details:
|
||||
|
||||
- Server start time and uptime
|
||||
- Process ID
|
||||
- .NET runtime version and OS
|
||||
- Current ban and auth session counts
|
||||
@@ -1,29 +0,0 @@
|
||||
# Building from source
|
||||
|
||||
The solution contains the Empostor server and its dependencies, like Hazel and the plugin API. The server is built using [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0).
|
||||
|
||||
## Cloning Empostor
|
||||
|
||||
You need to clone Empostor using git:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Empostor/Empostor.git
|
||||
```
|
||||
|
||||
## Building the server
|
||||
|
||||
### Dependencies
|
||||
|
||||
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
|
||||
- [Rider](https://www.jetbrains.com/rider/) or [Visual Studio](https://visualstudio.microsoft.com/vs/) (Optional, only if you want the full IDE experience)
|
||||
|
||||
### Build using the CLI
|
||||
|
||||
```bash
|
||||
cd src/Empostor.Server/
|
||||
dotnet build
|
||||
# Or if you want a single file, ready for production: (change linux-x64 to win-x64 if you use Windows)
|
||||
dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true
|
||||
```
|
||||
|
||||
To setup the server, please look at [Running the server](Running-the-server.md).
|
||||
@@ -1,82 +0,0 @@
|
||||
# Chat Filtering
|
||||
|
||||
Empostor can filter in-game chat messages by blocking specific words and limiting message spam. This helps server administrators maintain a healthier chat environment.
|
||||
|
||||
This feature is **disabled by default**. Enable it in `config.json` or from the admin panel to activate filtering.
|
||||
|
||||
## Admin Panel
|
||||
|
||||
A **Chat Filter** tab is available in the admin panel under the Monitor section. It allows you to manage all settings at runtime without editing `config.json` or restarting the server:
|
||||
|
||||
- **Enable/Disable** filtering with a checkbox
|
||||
- **Add/Remove blocked words** — type a word and click Add, or click the × on any tag to remove it
|
||||
- **Spam settings** — adjust the message threshold and time window
|
||||
- **Block/log toggle** — choose whether to block messages or only log warnings
|
||||
|
||||
Settings changed via the admin panel take effect immediately but are runtime-only — they are lost on server restart. For permanent settings, edit `config.json`.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add the `ChatFilter` section to your `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"ChatFilter": {
|
||||
"Enabled": false,
|
||||
"BlockedWords": [],
|
||||
"BlockMessage": true,
|
||||
"SpamThreshold": 5,
|
||||
"SpamWindowSeconds": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Enabled** | `false` | Whether chat filtering is enabled. |
|
||||
| **BlockedWords** | `[]` | A list of words to block. Matching is **case-insensitive** and uses substring matching (e.g., blocking `"bad"` also blocks `"badword"`). |
|
||||
| **BlockMessage** | `true` | Whether to actually block filtered messages. When `true`, the message is cancelled and never reaches other players. When `false`, the message is allowed through but a warning is logged. |
|
||||
| **SpamThreshold** | `5` | Number of messages allowed within the spam window before rate limiting kicks in. |
|
||||
| **SpamWindowSeconds** | `10` | The sliding time window in seconds for spam detection. |
|
||||
|
||||
## How Filtering Works
|
||||
|
||||
### Word Filter
|
||||
|
||||
When a player sends a chat message, it is checked against the `BlockedWords` list:
|
||||
|
||||
1. The message text is compared against each blocked word (case-insensitive substring match).
|
||||
2. If a match is found and `BlockMessage` is `true`, the message is blocked — other players will not see it.
|
||||
3. If `BlockMessage` is `false`, the message goes through but a warning is logged.
|
||||
|
||||
### Spam Filter
|
||||
|
||||
Each player has a per-player sliding window to detect rapid messaging:
|
||||
|
||||
1. Every chat message timestamp is recorded per player.
|
||||
2. If a player sends `SpamThreshold` or more messages within the last `SpamWindowSeconds` seconds, the message is handled according to `BlockMessage`.
|
||||
3. Old timestamps outside the window are automatically discarded.
|
||||
|
||||
Both filters run independently — a message can be blocked by either the word filter or the spam filter.
|
||||
|
||||
## Example
|
||||
|
||||
To block profanity and prevent players from sending more than 3 messages in 5 seconds:
|
||||
|
||||
```json
|
||||
{
|
||||
"ChatFilter": {
|
||||
"Enabled": true,
|
||||
"BlockedWords": ["badword1", "badword2"],
|
||||
"BlockMessage": true,
|
||||
"SpamThreshold": 3,
|
||||
"SpamWindowSeconds": 5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **Messages are not being blocked**: Ensure `Enabled` is `true` and `BlockedWords` contains entries. Remember that matching is substring-based — short words may match unexpectedly.
|
||||
- **False positives**: Use specific, longer words in your `BlockedWords` list. Substring matching means `"ass"` would match `"pass"` and `"classic"`.
|
||||
- **Legitimate players get spam-blocked**: Increase `SpamThreshold` or decrease `SpamWindowSeconds`. The defaults (5 messages / 10 seconds) are generous for normal conversation.
|
||||
@@ -1,55 +0,0 @@
|
||||
# Discord Webhook Integration
|
||||
|
||||
Empostor can send game events to a Discord channel via [webhooks](https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks). Each event is sent as a rich embed with relevant details.
|
||||
|
||||
This feature is **disabled by default**. Enable it in `config.json` and provide a webhook URL to activate it.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add the `DiscordWebhook` section to your `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"DiscordWebhook": {
|
||||
"Enabled": false,
|
||||
"WebhookUrl": "",
|
||||
"NotifyOnGameCreated": true,
|
||||
"NotifyOnBan": true,
|
||||
"NotifyOnReport": true,
|
||||
"NotifyOnPlayerJoin": false,
|
||||
"NotifyOnGameEnded": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Enabled** | `false` | Whether the Discord webhook integration is enabled. |
|
||||
| **WebhookUrl** | `""` | The Discord webhook URL to POST messages to. Create one in Discord under Channel Settings → Integrations → Webhooks. |
|
||||
| **NotifyOnGameCreated** | `true` | Send a notification when a new game lobby is created. |
|
||||
| **NotifyOnBan** | `true` | Send a notification when a player is banned from the server. |
|
||||
| **NotifyOnReport** | `true` | Send a notification when a player reports a dead body in-game. |
|
||||
| **NotifyOnPlayerJoin** | `false` | Send a notification when a player joins a game lobby. |
|
||||
| **NotifyOnGameEnded** | `false` | Send a notification when a game ends. |
|
||||
|
||||
## Events
|
||||
|
||||
Each notification is sent as a Discord embed with a colored sidebar:
|
||||
|
||||
| Event | Embed Color | Fields |
|
||||
| :--- | :--- | :--- |
|
||||
| Game Created | Green | Game code, host name, host friend code |
|
||||
| Player Banned | Red | Player name, friend code, game code |
|
||||
| Player Reported | Yellow | Reporter, reported player, friend codes, game code, reason |
|
||||
| Player Joined | Green | Player name, friend code, game code, player count |
|
||||
| Game Ended | Purple | Game code, result, player count |
|
||||
|
||||
## Rate Limiting
|
||||
|
||||
To avoid hitting Discord's rate limits, duplicate events for the same game within 2 seconds are suppressed.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- **No messages appear in Discord**: Verify `WebhookUrl` is correct and the webhook is active in your Discord channel. Check the server logs for `[Discord]` warnings.
|
||||
- **Webhook returns HTTP errors**: Discord may rate-limit or the webhook URL may be invalid. The listener logs the HTTP status code on failure.
|
||||
- **Events trigger but some fields are blank**: Friendly names and friend codes are read from client data — some fields may be empty (`—`) if the client hasn't fully connected yet.
|
||||
-23
@@ -1,23 +0,0 @@
|
||||
# Frequently Answered Questions
|
||||
|
||||
## What is this?
|
||||
|
||||
The Empostor project is a reverse engineered and open source server for the game Among Us. The game itself is developed by [Innersloth](http://www.innersloth.com/) while this project is maintained by the community. This project was built out of frustration for the lack of server availability in certain regions and the inability for the core developer, [AeonLucid](https://github.com/AeonLucid), to join a public game. As of this time, it has not been officially endorsed by the studio.
|
||||
|
||||
## Can this be used with the mobile version of the game?
|
||||
|
||||
Yes, Empostor can be used with both the Android and iOS versions of the game. You can add a custom server via a link, you can do this using the [site](https://empostor.github.io/Empostor)
|
||||
|
||||
## Can this be used with the console version of the game?
|
||||
|
||||
Sadly not, as these don't allow adding custom servers.
|
||||
|
||||
## Can this be used with Among Us 3D/VR?
|
||||
|
||||
No, it's an entirely different game with a completely different network stack. So we'd need to develop another server completely from scratch. We're currently not interested in doing this.
|
||||
|
||||
## How can I get started?
|
||||
|
||||
See [Setting up your Server](Running-the-server.md) for more information on running the server and [Client Setup](https://empostor.github.io/Empostor/) for helping your friends join in!
|
||||
|
||||
If you're having issues with the server software, check the [Troubleshooting page](TROUBLESHOOTING.md).
|
||||
@@ -1,41 +0,0 @@
|
||||
# Hello Page (Home Page)
|
||||
|
||||
Empostor serves a customisable home page at `http://your-server:22023/`.
|
||||
|
||||
## How it works
|
||||
|
||||
On startup, the server checks for a `Pages/` directory next to the executable. If it does not exist it is created automatically, and a default `index.html` is written into it.
|
||||
|
||||
```
|
||||
Empostor.Server (binary)
|
||||
Pages/
|
||||
index.html ← served at GET /
|
||||
```
|
||||
|
||||
Every request to `/` reads the file from disk at that moment, so edits take effect without restarting the server.
|
||||
|
||||
## Customising the page
|
||||
|
||||
Edit `Pages/index.html` with any HTML content you want. Common uses:
|
||||
|
||||
- Server rules and information
|
||||
- Discord invite link
|
||||
- Region file download instructions
|
||||
- Link to the admin panel
|
||||
|
||||
If the file is deleted, a plain fallback response is returned until the server is restarted and regenerates the default file.
|
||||
|
||||
## Default page
|
||||
|
||||
The generated default page contains:
|
||||
|
||||
- A status indicator showing the server is online
|
||||
- A link to the admin panel (`/admin`)
|
||||
- A link to the [region file generator](https://empostor.github.io/Empostor)
|
||||
- The server start time
|
||||
|
||||
## Notes
|
||||
|
||||
- The page is served as `text/html; charset=utf-8`.
|
||||
- There is no template engine. The file is served as-is.
|
||||
- For HTTPS access, configure a reverse proxy. See [Http-server.md](Http-server.md).
|
||||
@@ -1,82 +0,0 @@
|
||||
# HTTP Server
|
||||
|
||||
Since Empostor 1.9.0 a HTTP service is included for matchmaking, which is required for clients to connect to the Empostor server.
|
||||
|
||||
Starting from Among Us 16.0.5, you need to set up HTTPS for players to connect to your server.
|
||||
Unfortunately Empostor doesn't handle SSL certificates, so you need to set up a reverse proxy.
|
||||
|
||||
## Use a reverse proxy
|
||||
|
||||
A reverse proxy allows you to forward HTTP requests from users to multiple services. If you already have one, you should configure it to add Empostor.
|
||||
|
||||
If you have never set up a reverse proxy before, we recommend you to set up [Caddy](https://caddyserver.com/). It is easy to set up and comes with support for requesting SSL certificates out of the box.
|
||||
|
||||
To prevent people from connecting directly to Empostor, we recommend changing the "ListenIp" in the "HttpServer" section to "127.0.0.1". This makes sure people can't connect to your HTTP server other than via your reverse proxy. Keep the "ListenIp" in the "Server" section at "0.0.0.0" though, running the normal game traffic through a proxy is not supported.
|
||||
|
||||
### Caddy
|
||||
|
||||
To install Caddy, follow the [official installation guide](https://caddyserver.com/docs/install). Then use the following lines as your `Caddyfile` configuration file:
|
||||
|
||||
```
|
||||
example.com # replace example.com with your domain name
|
||||
|
||||
reverse_proxy :22023
|
||||
```
|
||||
|
||||
Now run `caddy run` in the folder with this Caddyfile and it should set up a server for you with a free SSL certificate.
|
||||
If this works, you should set up [Caddy to run in the background](https://caddyserver.com/docs/running)
|
||||
|
||||
### Nginx
|
||||
|
||||
Nginx is an alternative to Caddy that is a bit harder to set up. If you already use Nginx, you can use our snippet to add Empostor:
|
||||
|
||||
<details><summary>Nginx configuration</summary>
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name example.com; # replace example.com with your domain name
|
||||
|
||||
# Assuming you're using Certbot, replace example.com with your domain name
|
||||
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
|
||||
ssl_trusted_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:22023; # Change the port to your HttpServer's ListenPort
|
||||
proxy_set_header X-Forwarded-For $remote_addr;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# Redirect all traffic to HTTPS
|
||||
server {
|
||||
listen 80 default_server;
|
||||
location / {
|
||||
return 307 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
## Stop exposing Empostor's HTTP server to the internet directly
|
||||
|
||||
To only allow connections to Empostor's HTTP servers via the reverse proxy, we recommend to set the ListenIp of the HTTP server to 127.0.0.1:
|
||||
|
||||
```json
|
||||
{
|
||||
// NOTE: merge this snippet into the rest of your configuration
|
||||
"HttpServer": {
|
||||
"ListenIp": "127.0.0.1"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Or if you're configuring Empostor with environment variables:
|
||||
|
||||
```sh
|
||||
IMPOSTOR_HttpServer__ListenIp=127.0.0.1
|
||||
```
|
||||
|
||||
For more information on server configuration, see [Server-configuration.md](Server Configuration).
|
||||
@@ -1,64 +0,0 @@
|
||||
# Player Statistics
|
||||
|
||||
Empostor can track per-player game statistics including wins, kills, tasks completed, and more. Statistics are viewable through the admin panel and via the `/stat` in-game command.
|
||||
|
||||
This feature is **disabled by default**. Enable it in `config.json` to begin tracking.
|
||||
|
||||
## Configuration
|
||||
|
||||
Add the `PlayerStats` section to your `config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"PlayerStats": {
|
||||
"Enabled": false,
|
||||
"PersistToFile": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Key | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Enabled** | `false` | Whether player statistics tracking is enabled. |
|
||||
| **PersistToFile** | `true` | Whether to save statistics to `player_stats.json` on disk. If disabled, stats are kept in memory only and lost on server restart. |
|
||||
|
||||
## Tracked Statistics
|
||||
|
||||
The following metrics are recorded per player (keyed by Friend Code):
|
||||
|
||||
| Stat | Description |
|
||||
| :--- | :--- |
|
||||
| Games Played | Total number of games the player participated in |
|
||||
| Wins | Number of games won as a crewmate |
|
||||
| Losses | Number of games lost as a crewmate |
|
||||
| Impostor Wins | Number of games won as an impostor |
|
||||
| Kills | Total kills performed |
|
||||
| Deaths | Total times killed |
|
||||
| Tasks Completed | Total tasks completed |
|
||||
| Times Exiled | Total times voted out |
|
||||
|
||||
## Admin Panel
|
||||
|
||||
When enabled, a **Statistics** tab appears in the admin panel under the Monitor section. It displays a ranked table sorted by games played, with columns for all tracked stats. Use the **Refresh** button to reload data and **Reset All** to clear all statistics (requires confirmation).
|
||||
|
||||
## In-Game Command
|
||||
|
||||
Players can check their own stats by typing `/stat` (or `/stats`, `/mystats`) in the in-game chat. The response is sent privately and is localized based on the player's language.
|
||||
|
||||
## Localization
|
||||
|
||||
The `/stat` command response supports all languages configured in Empostor's language system. If no translation exists for the player's language, English is used as a fallback.
|
||||
|
||||
## API Endpoints
|
||||
|
||||
The admin panel uses these API endpoints:
|
||||
|
||||
| Method | Path | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| `GET` | `/api/admin/player/stats` | Returns all player statistics sorted by games played. Response: `{"enabled": true, "players": [...]}` |
|
||||
| `GET` | `/api/admin/player/stats/{friendCode}` | Returns statistics for a single player by Friend Code. |
|
||||
| `POST` | `/api/admin/player/stats/reset` | Clears all player statistics. Requires admin authentication. |
|
||||
|
||||
## File Persistence
|
||||
|
||||
When `PersistToFile` is enabled, statistics are saved to `player_stats.json` in the server directory. The file is updated after each game ends, following the same pattern as the ban and report stores.
|
||||
@@ -1,50 +0,0 @@
|
||||
# Plugin Marketplace Format
|
||||
|
||||
The admin panel plugin marketplace reads `marketplace/plugins.json` at startup.
|
||||
|
||||
## JSON Schema
|
||||
|
||||
```jsonc
|
||||
[
|
||||
{
|
||||
// Unique plugin identifier (reverse-domain style recommended)
|
||||
"id": "cn.hayashiume.welcome",
|
||||
|
||||
// Display name shown in the marketplace
|
||||
"name": "Welcome Messages",
|
||||
|
||||
// Short description (shown as subtitle)
|
||||
"description": "Sends a localised welcome message when players join a room.",
|
||||
|
||||
// Author attribution
|
||||
"author": "BunchHanpiDev & HayashiUme",
|
||||
|
||||
// Version history — latest version should be LAST in the array
|
||||
"versions": [
|
||||
{
|
||||
// Semantic version of this plugin release
|
||||
"version": "1.0.0",
|
||||
|
||||
// Minimum Empostor / Empostor version required
|
||||
"empostor_version": "2.0.0",
|
||||
|
||||
// Direct download URL for the .dll file
|
||||
"download_url": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/Empostor.Plugins.Welcome.dll"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Versioning Rules
|
||||
|
||||
- **`versions` array**: Each entry represents one downloadable release. List older versions first; the **last entry** is treated as the latest and is selected by default in the admin panel UI.
|
||||
- **Single-version plugins**: An array with one entry is valid. The version dropdown is hidden and the sole version is used directly.
|
||||
- **Multi-version plugins**: When two or more entries exist, the admin panel shows a `<select>` dropdown letting the operator pick which version to install.
|
||||
- **`download_url`**: Must point directly to a compiled `.dll` plugin file. The admin panel downloads this URL and saves it to the plugins directory.
|
||||
|
||||
## Adding a Plugin
|
||||
|
||||
1. Place the `.dll` file in `marketplace/` (or host it at an accessible URL).
|
||||
2. Add a new entry to `plugins.json` with the fields above.
|
||||
3. The admin panel reads `GET /api/admin/marketplace/plugins` which returns the full `plugins.json` contents — no server restart needed for the list to appear, but a restart is required after a plugin is installed.
|
||||
@@ -1,7 +0,0 @@
|
||||
# Empostor Documentation
|
||||
|
||||
1. [Running the server](Running-the-server.md)
|
||||
2. [Server configuration](Server-configuration.md)
|
||||
3. [Building from source](Building-from-source.md)
|
||||
4. [Writing a plugin](Writing-a-plugin.md)
|
||||
5. [Frequently answered questions](FAQ.md)
|
||||
@@ -1,37 +0,0 @@
|
||||
# Running the server
|
||||
|
||||
There are currently two install methods for Empostor: You can install it normally or inside of a Docker container. If you do not have a particular preference, we recommend the normal installation over the Docker container method.
|
||||
|
||||
## General remarks for both install methods
|
||||
|
||||
This section applies to both the normal installation as well as the Docker (Compose) installation
|
||||
|
||||
To connect to the server, you need to configure and install a region file on https://empostor.github.io/Empostor/
|
||||
|
||||
Among Us connects to the server using two network services: the (TCP) HTTP service points Among Us to the UDP service, then the UDP service hosts the actual game traffic. Because of this, Empostor uses port 22023 using **both** the TCP and UDP protocols.
|
||||
|
||||
To connect to your Among Us version from another PC, you need to set up a HTTP reverse proxy to support HTTPS. If you're just running a version of Empostor for testing Setup instructions for this are in the [Http Server documentation](Http-server.md).
|
||||
|
||||
If you want to test if your Among Us HTTP server is working, open `http://localhost:22023` (assuming you're using the default settings, change the IP and port respectively) in a browser
|
||||
|
||||
Depending on your host you may also need to port forward Empostor to the internet or pass Empostor traffic by your firewall. Port 22023 UDP needs to be accessible for everyone that wants to play on the server, then you also need to portforward your HTTP reverse proxy or port 22023 TCP if you don't use a reverse proxy. As port forwarding changes per host or router configuration, port forwarding is not covered by this guide.
|
||||
|
||||
## Normal installation
|
||||
|
||||
1. Install [.NET 8.0](https://dotnet.microsoft.com/download/dotnet/8.0). We recommend either the ASP.NET Core Runtime or the SDK. The SDK is necessary in case you want to develop Empostor or Empostor plugins.
|
||||
2. Download the [latest release](https://github.com/HayashiUme/Empostor/releases) or the [latest CI build](https://nightly.link/HayashiUme/Empostor/workflows/ci/master). Note that Empostor is built for multiple CPU-architectures and operating systems, you most likely want the x64 version, unless you are running on a Raspberry Pi or another device/VPS with an Arm processor.
|
||||
3. Extract the zip.
|
||||
4. Modify `config.json` to your liking. Documentation can be found [here](Server-configuration.md). You need to at least change `PublicIp` to the address people will connect to your server to.
|
||||
5. Run `Empostor.Server` (Linux/macOS) or `Empostor.Server.exe` (Windows)
|
||||
6. Set up a reverse proxy to support HTTPS, so you can connect to your server from another device. See [reverse proxy configuration](Http-server.md)
|
||||
7. (OPTIONAL - Linux) Configure a systemd definition file and enable the service to start on boot, see [systemd configuration](Server-configuration.md#systemd)
|
||||
|
||||
## Ues Dockerfile with compose.yml
|
||||
Place the `Docker.Postfix.zip` file from the distribution into the pre-built files provided by the distribution, then run
|
||||
```bash
|
||||
# Enter File Path
|
||||
cd /yourdir/empostor/
|
||||
# Build and Run
|
||||
docker-compose up -d --build
|
||||
```
|
||||
Then the docker will run successfully.
|
||||
@@ -1,224 +0,0 @@
|
||||
# Server configuration
|
||||
|
||||
## Options
|
||||
|
||||
### Required Server Configuration
|
||||
|
||||
| Key | Default | Description |
|
||||
| -------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **PublicIp** | `127.0.0.1` | This needs to the public IPv4 address of the server which you give to others to connect. You can find your IPv4 address [on this website](http://whatismyip.host/). Unless you are only planning to use Empostor privately, on your local network, you should change this to your public ip. It is also possible to use hostnames instead of IPv4 addresses, which will be resolved to IPv4 addresses. |
|
||||
| **PublicPort** | `22023` | The public port of the server which you give to others to connect. (**This is the external port you configure on your router when port forwarding.**) Usually `22023`. |
|
||||
| **ListenIp** | `0.0.0.0` | The network interface to listen on. If you do not know what to put here, use `0.0.0.0`. Since 1.2.2 it is also possible to use hostnames instead of IPv4 addresses, these must resolve to a valid IPv4 address. |
|
||||
| **ListenPort** | `22023` | The listen port of the server, usually `22023`. For port forwarding purposes: this is an UDP port |
|
||||
|
||||
### HttpServer
|
||||
|
||||
Empostor has an Http Server that is used by recent versions of Among Us to connect to. See [the Http Server page](Http-server.md) for more details on how to set this up.
|
||||
|
||||
| Key | Default | Description |
|
||||
|----------------|-----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Enabled** | `true` | Whether the http server should be enabled. |
|
||||
| **ListenIp** | `0.0.0.0` | The network interface to listen on. Use `127.0.0.1` if you use a reverse proxy or just run locally. Use `0.0.0.0` if you are directly exposing this server to the internet |
|
||||
| **ListenPort** | `22023` | The listen port of this server. For port forwarding purposes, this is an TCP port. |
|
||||
|
||||
### AntiCheat
|
||||
|
||||
Empostor has an Anticheat that makes it possible to kick cheaters from games automatically. Note that the anticheat is tuned on the vanilla version of the game, so client-side modifications could trigger the Anticheat if you're playing with them.
|
||||
|
||||
| Key | Default | Value |
|
||||
|-------------------------------|-----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **Enabled** | `true` | Whether the anticheat should be enabled. |
|
||||
| **BanIpFromGame** | `true` | When anticheat is enabled and a player is caught hacking, they will be kicked from the server. If this value is set to `true`, the player will be banned instead and will not be able to rejoin that specific game. |
|
||||
| **AllowCheatingHosts** | `"Never"` | Configure whether hosts are allowed to cheat. "Never" forbids it, "Always" allows it. "IfRequested" allows hosts to cheat if they connect with the DisableServerAuthorityFlag set. |
|
||||
| **EnableGameFlowChecks** | `true` | Enable checks that check if certain actions are done in the appropriate order or at the appropriate moment in the game. This includes changing cosmetics while in game or murdering too fast. |
|
||||
| **EnableInvalidObjectChecks** | `true` | Enables checks that check if network objects are spawned properly. Disabling this option also implies disabling EnableRoleChecks. |
|
||||
| **EnableMustBeHostChecks** | `true` | Enables checks that check if players are the host before they can do actions that require them to be host of the game. This includes starting the game and spawning objects. |
|
||||
| **EnableColorLimitChecks** | `true` | Enables checks that checks if players request colors that are already in use. |
|
||||
| **EnableNameLimitChecks** | `true` | Enables checks that checks if player names have a length that is possible to set using the user interface. |
|
||||
| **EnableOwnershipChecks** | `true` | Enables checks that check if players are allowed to perform a certain action on themself or another player. |
|
||||
| **EnableRoleChecks** | `true` | Enables checks that check if players have the correct role when performing certain role abilities like venting or murdering. |
|
||||
| **EnableTargetChecks** | `true` | Enables checks that check if certain packets to everyone that should only have been sent to certain players or vice versa. This includes sending votes or network objects. |
|
||||
| **ForbidProtocolExtensions** | `true` | If disabled allows players to send network packets that go beyond the network packets sent by the vanilla game. This is necessary for most mods that need all players to install it. |
|
||||
|
||||
### Compatibility
|
||||
|
||||
Empostor has some compatibility options which allow some extra flexibility but may not work properly. Enabling any of these options is not recommended. When contacting support, please mention which of these options are enabled.
|
||||
|
||||
| Key | Default | Value |
|
||||
|-----------------------------|---------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| **AllowFutureGameVersions** | `false` | Warning: Setting this option to `true` is unsupported and may cause issues when large updates to Among Us are released, but can be useful when a small patch is released. Allows future versions of Among Us to join your server. |
|
||||
| **AllowHostAuthority** | `false` | Certain Among Us mods allow disabling some server-authoritative features, which also changes some code paths in the client. These code paths have not undergone as much testing and contain bugs, which can't be fixed from the Empostor side. Use with caution. |
|
||||
| **AllowVersionMixing** | `false` | Allows players using different game versions to play in one lobby that have not been marked by the Empostor developers as compatible. |
|
||||
|
||||
### Debug
|
||||
|
||||
The Debug configuration is used to enable the game recorder. This is mostly useful when developing Empostor.
|
||||
|
||||
| Key | Default | Value |
|
||||
| ----------------------- | ------- | -------------------------------------------- |
|
||||
| **GameRecorderEnabled** | `false` | Enables the Game Recorder. |
|
||||
| **GameRecorderPath** | _empty_ | Path where the recorded games will be saved. |
|
||||
|
||||
### Admin
|
||||
|
||||
The `Admin` configuration is used to set the admin password and plugin marketplace URL for server management.
|
||||
|
||||
| Key | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| Password | *(empty — must be set)* | The admin password for the server, used for authentication. Set a strong, complex password. The server logs a warning at startup if this is left empty or set to a known default. |
|
||||
| MarketplaceUrl | `https://raw.githubusercontent.com/HayashiUme/Empostor/main/marketplace/plugins.json` | A URL pointing to a `plugins.json` file. This file defines the list of plugins available in the plugin marketplace. |
|
||||
|
||||
### DiscordWebhook
|
||||
|
||||
Send game events to a Discord channel via webhooks. See [Discord Webhook](Discord-webhook.md) for details.
|
||||
|
||||
| Key | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Enabled** | `false` | Whether the Discord webhook integration is enabled. |
|
||||
| **WebhookUrl** | `""` | The Discord webhook URL to POST messages to. |
|
||||
| **NotifyOnGameCreated** | `true` | Send a notification when a new game lobby is created. |
|
||||
| **NotifyOnBan** | `true` | Send a notification when a player is banned. |
|
||||
| **NotifyOnReport** | `true` | Send a notification when a player reports a body. |
|
||||
| **NotifyOnPlayerJoin** | `false` | Send a notification when a player joins a game. |
|
||||
| **NotifyOnGameEnded** | `false` | Send a notification when a game ends. |
|
||||
|
||||
### PlayerStats
|
||||
|
||||
Track per-player game statistics viewable in the admin panel and via `/stat` command. See [Player Statistics](Player-statistics.md) for details.
|
||||
|
||||
| Key | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Enabled** | `false` | Whether player statistics tracking is enabled. |
|
||||
| **PersistToFile** | `true` | Whether to save statistics to `player_stats.json` on disk. |
|
||||
|
||||
### ChatFilter
|
||||
|
||||
Filter in-game chat for blocked words and rate-limit spam. See [Chat Filtering](Chat-filtering.md) for details.
|
||||
|
||||
| Key | Default | Description |
|
||||
| :--- | :--- | :--- |
|
||||
| **Enabled** | `false` | Whether chat filtering is enabled. |
|
||||
| **BlockedWords** | `[]` | List of words to block (case-insensitive substring match). |
|
||||
| **BlockMessage** | `true` | Whether to actually block filtered messages. When `false`, messages pass through and only a warning is logged. |
|
||||
| **SpamThreshold** | `5` | Messages allowed within the spam window before rate limiting. |
|
||||
| **SpamWindowSeconds** | `10` | Sliding time window in seconds for spam detection. |
|
||||
|
||||
### Serilog (Logging)
|
||||
|
||||
Empostor's log framework, Serilog, can be configured in the config file. You can change its default log level and you can add additional sinks.
|
||||
|
||||
| Key | Default | Value |
|
||||
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **MinimumLevel** | `Information` | Minimum log level for a message to be printed. If a log entry is as severe or more severe than this level, it will be printed. From most severe to least severe: `Fatal`,`Error`, `Warning`, `Information`, `Debug`, `Verbose` |
|
||||
| **Using** | `[]` | List of additional Serilog Sinks assemblies to load. |
|
||||
| **WriteTo** | `[]` | Additional logging sinks. See the Serilog documentation or the example in this section. Serilog |
|
||||
|
||||
For more information, check the [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration) documentation.
|
||||
|
||||
For example, to add logging to a file, you should add the following snippet to your configuration:
|
||||
|
||||
```json
|
||||
"Serilog": {
|
||||
"Using": [
|
||||
"Serilog.Sinks.File"
|
||||
],
|
||||
"WriteTo": [
|
||||
{
|
||||
"Name": "File",
|
||||
"Args": {
|
||||
"path": "logs/log.txt",
|
||||
"rollingInterval": "Day"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Next to that, you also need to copy over Serilog.Sinks.File.dll from [NuGet](https://www.nuget.org/packages/Serilog.Sinks.File/). See the [Serilog.Sinks.File documentation](https://github.com/serilog/serilog-sinks-file#json-appsettingsjson-configuration) for a list of parameters that can be configured.
|
||||
|
||||
Other Serilog sinks are also supported, but are out of scope for this documentation.
|
||||
|
||||
## Config providers
|
||||
|
||||
### File
|
||||
|
||||
The `config.json` file contains all the configurable options.
|
||||
|
||||
### Environment variables
|
||||
|
||||
If you're unable to edit the `config.json` file, you can instead set Environment variables to configure Empostor. The general pattern for each variable is `EMPOSTOR_SectionName__VariableName`. For example, to disable the anticheat, you can set the environment variable `EMPOSTOR_AntiCheat__Enabled=false`. Here are some more examples:
|
||||
|
||||
```
|
||||
EMPOSTOR_Server__PublicIp=127.0.0.1
|
||||
EMPOSTOR_Server__PublicPort=22023
|
||||
EMPOSTOR_Server__ListenIp=0.0.0.0
|
||||
EMPOSTOR_Server__ListenPort=22023
|
||||
EMPOSTOR_AntiCheat__Enabled=true
|
||||
EMPOSTOR_AntiCheat__BanIpFromGame=true
|
||||
```
|
||||
|
||||
# systemd
|
||||
|
||||
Linux users will likely want to create a systemd service definition to manage running the Empostor application. Benefits to using a systemd service include the following:
|
||||
|
||||
- The server process can automatically restart if the process dies
|
||||
- Empostor can be set to start on boot (handy if you do system updates regularly)
|
||||
- You can use systemd targets to ensure the process doesn't start until the system dependencies are met
|
||||
|
||||
Using your favorite text editor, create the following service file:
|
||||
|
||||
```
|
||||
sudoedit /etc/systemd/system/empostor.service
|
||||
```
|
||||
|
||||
Modify the following template to your needs and populate the service file.
|
||||
|
||||
```
|
||||
[Unit]
|
||||
Description=Empostor private Among Us server - https://github.com/HayashiUme/Empostor
|
||||
|
||||
[Service]
|
||||
# Directory where Empostor is installed
|
||||
WorkingDirectory=/opt/Empostor
|
||||
# Path to Empostor binary
|
||||
ExecStart=/opt/Empostor/EMPOSTOR.Server
|
||||
Restart=always
|
||||
# Restart service after 10 seconds if the dotnet service crashes
|
||||
RestartSec=10
|
||||
KillSignal=SIGTERM
|
||||
SyslogIdentifier=empostor
|
||||
|
||||
# User and group that will run the Empostor server process -- ensure that user is allowed to execute the binary
|
||||
User=empostor
|
||||
Group=empostor
|
||||
TimeoutStopSec=10
|
||||
|
||||
[Install]
|
||||
# Wait until most system services have started before starting Empostor
|
||||
After=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
Reload all systemd unit files to source in the new file you've created.
|
||||
|
||||
```
|
||||
sudo systemctl daemon-reload
|
||||
```
|
||||
|
||||
Start the service.
|
||||
|
||||
```
|
||||
sudo systemctl start empostor.service
|
||||
```
|
||||
|
||||
Check the log for the service and verify it has started correctly -- Hint: Use the Q key to quit out of journalctl.
|
||||
|
||||
```
|
||||
sudo journalctl -u empostor.service
|
||||
```
|
||||
|
||||
If everything has started correctly, ensure the service is set to start on boot.
|
||||
|
||||
```
|
||||
sudo systemctl enable empostor.service
|
||||
```
|
||||
@@ -1,63 +0,0 @@
|
||||
# Troubleshooting
|
||||
|
||||
If you're reading this, something went wrong.
|
||||
Don't worry though, as this is the most thorough guide to help you!
|
||||
|
||||
## `./Empostor.Server: line 1: ELF: not found` (plus other errors)
|
||||
|
||||
No idea where you got that system. But we clearly do **NOT** support it.
|
||||
|
||||
## `cannot execute binary file: Exec format error`
|
||||
|
||||
Please check that you have downloaded the right version of Empostor, as we mantain two CPU architectures (x64 and ARM).
|
||||
Unless you are running Empostor on a computer like the Raspberry Pi, you most likely want to use the x64 version.
|
||||
|
||||
## `./Empostor.Server: Permission denied`
|
||||
|
||||
This is an error related to Linux file permissions.
|
||||
Some files do not hold their executable bit (the permission that allows them to run) during a download.
|
||||
You can solve this by doing: `chmod +x Empostor.Server`
|
||||
|
||||
## Disconnected with `Your client is too new, please update your Empostor to play`
|
||||
|
||||
This happens when your game is too new for your copy of Empostor to support.
|
||||
|
||||
If you're using an Empostor release, check if there is a newer release available that supports your version. If this doesn't exist, you can try the [latest build from CI](https://nightly.link/Empostor/Empostor/workflows/ci/master).
|
||||
|
||||
If even that build doesn't work, you can try looking through the Pull Requests to see if there is a pending pull request that aims to update it to the next version, but note these may be unstable and may contain malicious code, as they haven't been vetted by the Empostor development team yet.
|
||||
|
||||
## Disconnected with `Please update your game to play on this server`
|
||||
|
||||
This happens when you're using a version of Empostor that is too new for your game version. You can download an older version from [the GitHub release page](https://github.com/Empostor/Empostor/releases). We however don't support older versions of Empostor with new API's and bugfixes so we recommend to update your game instead.
|
||||
|
||||
## Disconnected with `You are using an older version of the game`
|
||||
|
||||
You are using a version of Empostor that is not designed for the version of the game you're playing. The game does not really check who is outdated and blames it on the user. Newer versions (v1.5.0+) do warn correctly and send the correct message.
|
||||
|
||||
Look at which version of the game you're playing, which you can see in the top left corner of the main menu, then download an Empostor version for that game. Every [release on the release page](https://github.com/Empostor/Empostor/releases) shows which version of the game it is compatible with. If your game version is newer than the latest release, check if the [latest build from CI works](https://nightly.link/Empostor/Empostor/workflows/ci/master).
|
||||
|
||||
## Disconnected with `You disconnected from the server. Reliable Packet 1 ...`
|
||||
|
||||
Please double-check that you have followed the [Server Configuration](Server-configuration.md) correctly.
|
||||
**NOTE: Your public ip does not start with `10`, `127` or `192`**
|
||||
Also check if the port Empostor (ListenPort) is listening on is correctly port-forwarded for UDP (or TCP/UDP).
|
||||
|
||||
## `Could not load file or assembly...`
|
||||
|
||||
Please check that you only have **working** plugins in the `plugins` folder.
|
||||
This error can be caused by non-plugin files or plugins that are not working correctly.
|
||||
|
||||
## Disconnected with `You disconnected from the server. If this happens often, check your network strength. This may also be a server issue`
|
||||
|
||||
Usually this means that Among Us couldn't connect to Empostor's HTTP server.
|
||||
|
||||
Open the address of your server in a web browser, it should show you a small page confirming that Empostor is available.
|
||||
|
||||
## My question is not yet answered and I'm still having problems!
|
||||
|
||||
That's unfortunate. Join the [Empostor Discord](https://discord.gg/Mk3w6Tb), ask your question there and we'll try to help you out. Note that we're not always available, so it may take some time to get an answer. To make answering your question easier, please add the following details:
|
||||
|
||||
- Which version of Empostor are you using?
|
||||
- Which version or Among Us are you using?
|
||||
- Which operating system are you running Empostor on?
|
||||
- If you have console logs, attaching these will also help.
|
||||
@@ -1,12 +0,0 @@
|
||||
# Upgrading Empostor
|
||||
|
||||
Sometimes we make incompatible changes to existing code. This document lists these changes and which changes you should make as a server administrator.
|
||||
|
||||
## Empostor 1.9.0
|
||||
|
||||
Previously we recommended using the Empostor.Http plugin for HTTP matchmaking. Because Among Us now relies on HTTP matchmaking, this plugin is now part of the default installation. As a result, you should change your server as follows:
|
||||
|
||||
- If you have Empostor.Http installed, you should remove that plugin. If you have changed the default settings, you need to move these changes to the [HttpServer section in config.json](Server-configuration.md#HttpServer).
|
||||
- If you have plugins that required Empostor.Http's API (like Reactor.Empostor.Http), you should check that plugin for updates
|
||||
- It is no longer necessary to add the ASP.NET Core folder to the PluginLoader's LibraryPaths
|
||||
|
||||
@@ -1,332 +0,0 @@
|
||||
# Writing a plugin
|
||||
|
||||
Empostor has support for plugins. This document will help you to setup a development environment for writing a plugin.
|
||||
|
||||
- [1. Install .NET Core SDK](#1-install-net-core-sdk)
|
||||
- [2. Create a C# project](#2-create-a-c-project)
|
||||
- [3. Add the Empostor.Api library](#3-add-the-empostorapi-library)
|
||||
- [Quick](#quick)
|
||||
- [Visual Studio](#visual-studio)
|
||||
- [4. The plugin class](#4-the-plugin-class)
|
||||
- [5. Adding an event listener](#5-adding-an-event-listener)
|
||||
- [6. Registering the event listener](#6-registering-the-event-listener)
|
||||
- [7. Build and run your plugin](#7-build-and-run-your-plugin)
|
||||
- [8. Extra](#8-extra)
|
||||
- [Event listeners](#event-listeners)
|
||||
- [Dependency injection](#dependency-injection)
|
||||
- [Server configuration](#server-configuration)
|
||||
- [Using other libraries](#using-other-libraries)
|
||||
- [Empostor versions](#empostor-versions)
|
||||
- [9. Missing/invalid data or want more functions?](#9-missinginvalid-data-or-want-more-functions)
|
||||
|
||||
## 1. Install .NET SDK
|
||||
|
||||
Download and install the latest .NET SDK.
|
||||
|
||||
https://dotnet.microsoft.com/download
|
||||
|
||||
## 2. Create a C# project
|
||||
|
||||
The first step is creating a new C# project, it must be a **Class Library (.NET Standard)**. The target framework can be any of those compatible with .NET 8, but we recommend sticking with **.NET 8.0**.
|
||||
|
||||
For more information about compatibility, see https://docs.microsoft.com/en-us/dotnet/standard/net-standard.
|
||||
|
||||
When the project has been created, you should have `Class.cs` and `Project.csproj` files. Your `Project.csproj` should look something like this.
|
||||
|
||||
```xml
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
```
|
||||
|
||||
## 3. Add the Empostor.Api library
|
||||
|
||||
You only have to follow the instructions of one below.
|
||||
|
||||
### Quick
|
||||
|
||||
Install the `Empostor.Api` NuGet package.
|
||||
Make sure to get a prerelease if you are writing a plugin for a dev release of the server.
|
||||
|
||||
### Visual Studio
|
||||
|
||||
1. Right click your project.
|
||||
2. Click `Manage NuGet Packages`.
|
||||
3. Click `Browse`.
|
||||
4. Search for `Empostor.Api`.
|
||||
5. Click the `Empostor.Api` result and press install on the right side.
|
||||
|
||||
### Dotnet CLI
|
||||
|
||||
1. Open your project folder in command prompt / bash.
|
||||
2. Run `dotnet add package Empostor.Api`.
|
||||
|
||||
## 4. The plugin class
|
||||
|
||||
Now the `Empostor.Api` is installed, you need to create a class for your plugin. A plugin **must** contain exactly one. See the code below for an example.
|
||||
|
||||
```csharp
|
||||
using System.Threading.Tasks;
|
||||
using Empostor.Api.Events.Managers;
|
||||
using Empostor.Api.Plugins;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Empostor.Plugins.Example
|
||||
{
|
||||
/// <summary>
|
||||
/// The metadata information of your plugin, this is required.
|
||||
/// </summary>
|
||||
[EmpostorPlugin(
|
||||
package: "gg.empostor.example",
|
||||
name: "Example",
|
||||
author: "AeonLucid",
|
||||
version: "1.0.0")]
|
||||
public class ExamplePlugin : PluginBase // This is also required ": PluginBase".
|
||||
{
|
||||
/// <summary>
|
||||
/// A logger that works seamlessly with the server.
|
||||
/// </summary>
|
||||
private readonly ILogger<ExamplePlugin> _logger;
|
||||
|
||||
/// <summary>
|
||||
/// The constructor of the plugin. There are a few parameters you can add here and they
|
||||
/// will be injected automatically by the server, two examples are used here.
|
||||
///
|
||||
/// They are not necessary but very recommended.
|
||||
/// </summary>
|
||||
/// <param name="logger">
|
||||
/// A logger to write messages in the console.
|
||||
/// </param>
|
||||
/// <param name="eventManager">
|
||||
/// An event manager to register event listeners.
|
||||
/// Useful if you want your plugin to interact with the game.
|
||||
/// </param>
|
||||
public ExamplePlugin(ILogger<ExamplePlugin> logger, IEventManager eventManager)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called when your plugin is enabled by the server.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override ValueTask EnableAsync()
|
||||
{
|
||||
_logger.LogInformation("Example is being enabled.");
|
||||
return default;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is called when your plugin is disabled by the server.
|
||||
/// Most likely because it is shutting down, this is the place to clean up any managed resources.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public override ValueTask DisableAsync()
|
||||
{
|
||||
_logger.LogInformation("Example is being disabled.");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 5. Adding an event listener
|
||||
|
||||
Currently you should have a plugin that loads and does nothing. In order to get some actual functionality, you need to add an event listener.
|
||||
|
||||
Create a new class called `GameEventListener`. Example code:
|
||||
|
||||
```csharp
|
||||
using Empostor.Api.Events;
|
||||
using Empostor.Api.Events.Player;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Empostor.Plugins.Example.Handlers
|
||||
{
|
||||
/// <summary>
|
||||
/// A class that listens for two events.
|
||||
/// It may be more but this is just an example.
|
||||
///
|
||||
/// Make sure your class implements <see cref="IEventListener"/>.
|
||||
/// </summary>
|
||||
public class GameEventListener : IEventListener
|
||||
{
|
||||
private readonly ILogger<ExamplePlugin> _logger;
|
||||
|
||||
public GameEventListener(ILogger<ExamplePlugin> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An example event listener.
|
||||
/// </summary>
|
||||
/// <param name="e">
|
||||
/// The event you want to listen for.
|
||||
/// </param>
|
||||
[EventListener]
|
||||
public void OnGameStarted(IGameStartedEvent e)
|
||||
{
|
||||
_logger.LogInformation($"Game is starting.");
|
||||
|
||||
// This prints out for all players if they are impostor or crewmate.
|
||||
foreach (var player in e.Game.Players)
|
||||
{
|
||||
var info = player.Character.PlayerInfo;
|
||||
var isImpostor = info.IsImpostor;
|
||||
if (isImpostor)
|
||||
{
|
||||
_logger.LogInformation($"- {info.PlayerName} is an impostor.");
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogInformation($"- {info.PlayerName} is a crewmate.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[EventListener]
|
||||
public void OnGameEnded(IGameEndedEvent e)
|
||||
{
|
||||
_logger.LogInformation($"Game has ended.");
|
||||
}
|
||||
|
||||
[EventListener]
|
||||
public void OnPlayerChat(IPlayerChatEvent e)
|
||||
{
|
||||
_logger.LogInformation($"{e.PlayerControl.PlayerInfo.PlayerName} said {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 6. Registering the event listener
|
||||
|
||||
The last step to get your plugin working is to register the event listener, so the server knows about it. Go back to your plugin class and modify it as below.
|
||||
|
||||
```csharp
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using Empostor.Api.Events.Managers;
|
||||
using Empostor.Api.Plugins;
|
||||
using Empostor.Plugins.Example.Handlers;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Empostor.Plugins.Example
|
||||
{
|
||||
[EmpostorPlugin(
|
||||
package: "gg.empostor.example",
|
||||
name: "Example",
|
||||
author: "AeonLucid",
|
||||
version: "1.0.0")]
|
||||
public class ExamplePlugin : PluginBase
|
||||
{
|
||||
private readonly ILogger<ExamplePlugin> _logger;
|
||||
// Add the line below!
|
||||
private readonly IEventManager _eventManager;
|
||||
// Add the line below!
|
||||
private IDisposable _unregister;
|
||||
|
||||
public ExamplePlugin(ILogger<ExamplePlugin> logger, IEventManager eventManager)
|
||||
{
|
||||
_logger = logger;
|
||||
// Add the line below!
|
||||
_eventManager = eventManager;
|
||||
}
|
||||
|
||||
public override ValueTask EnableAsync()
|
||||
{
|
||||
_logger.LogInformation("Example is being enabled.");
|
||||
// Add the line below!
|
||||
_unregister = _eventManager.RegisterListener(new GameEventListener(_logger));
|
||||
return default;
|
||||
}
|
||||
|
||||
public override ValueTask DisableAsync()
|
||||
{
|
||||
_logger.LogInformation("Example is being disabled.");
|
||||
// Add the line below!
|
||||
_unregister.Dispose();
|
||||
return default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## 7. Build and run your plugin
|
||||
|
||||
Now your plugin is ready to be tested.
|
||||
|
||||
1. Right click your project and press `Build`.
|
||||
2. Right click your project and press `Open Folder in File Explorer`.
|
||||
3. Go to `bin/Debug/net8.0/`.
|
||||
4. In this directory, you should find your plugin named `Project.dll`.
|
||||
5. Copy the `Project.dll` to the `plugins` directory in your Empostor server directory.
|
||||
6. (Re)start your Empostor server.
|
||||
7. Open Among Us, create a game and send a chat message. In the console you should see your plugin being loaded and the messages from the example.
|
||||
|
||||
## 8. Extra
|
||||
|
||||
Some extra information that might be useful for those developing plugins.
|
||||
|
||||
### Event listeners
|
||||
|
||||
- You can have multiple event listener on the same event.
|
||||
- An event listener can be given a priority `[EventListener(EventPriority.Normal)]` and is called in order.
|
||||
- It is not recommended to block for a long time inside `EventListener` because the events are called from inside the packet handlers. Blocking too long causes the client to time out. You should create a new `Task` for operations that will take a lot of time.
|
||||
|
||||
### Dependency injection
|
||||
|
||||
- The main plugin class is constructed by the `IServiceProvider` of the server and can inject everything the server uses. A few examples are:
|
||||
- `ILogger<T>`
|
||||
- `IEventManager`
|
||||
- `IClientManager`
|
||||
- `IOptions<ServerConfig>`
|
||||
- You can add your own classes and `EventListener` implementation to the `IServiceProvider` by creating a new class and implementing `IPluginStartup`. Make sure to register them as a singleton `services.AddSingleton<IEventListener, GameEventListener>();`.
|
||||
|
||||
### Server configuration
|
||||
|
||||
Constantly copying the plugin dll to your server directory can be pretty annoying. Luckily we have a solution for that. In your Empostor server open the `config.json` and add the `PluginLoader` from the example below, replace the path with the build destination of your plugin.
|
||||
|
||||
```json
|
||||
{
|
||||
"Server": {
|
||||
"PublicIp": "127.0.0.1",
|
||||
"PublicPort": 22023,
|
||||
"ListenIp": "0.0.0.0",
|
||||
"ListenPort": 22023
|
||||
},
|
||||
"PluginLoader": {
|
||||
"Paths": [
|
||||
"D:\\Projects\\Empostor\\src\\Empostor.Plugins.Example\\bin\\Debug\\net8.0"
|
||||
],
|
||||
"LibraryPaths": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using other libraries
|
||||
|
||||
Sometimes you need to use libraries that the original Empostor server does not provide. The dll files of these libraries must be placed in the `libraries` folder next to the server executable. You could also provide them by modifying the `PluginLoader.LibraryPaths` option in the `config.json`, similarly to the `PluginLoader.Paths` option.
|
||||
|
||||
### Empostor versions
|
||||
|
||||
It is important to use the correct versions when working with `Empostor.Api` prereleases and the `Empostor` dev builds to reduce the chances of mismatching assemblies.
|
||||
|
||||
**Example**
|
||||
|
||||
The prerelease `Empostor.Api` package `1.2.0-ci.54` belongs to build `54` on AppVeyor, which can be found here https://ci.appveyor.com/project/Empostor/Empostor/build/54. Notice the `54` on the end of the url.
|
||||
|
||||
## 9. Missing/invalid data or want more functions?
|
||||
|
||||
The `Empostor.Api` is currently in beta. There are a lot of things still missing and we would like to hear from you what you need to develop a plugin.
|
||||
|
||||
Create an issue:
|
||||
|
||||
- [Suggest a function](https://github.com/Empostor/Empostor/issues/new?template=3--api-suggestion.md)
|
||||
- [Data is invalid](https://github.com/Empostor/Empostor/issues/new?template=4--api-invalid.md)
|
||||
- [Data is unavailable](https://github.com/Empostor/Empostor/issues/new?template=5--api-missing.md)
|
||||
- [Other](https://github.com/Empostor/Empostor/issues/new?template=6--api-other.md)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 50 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 3.8 KiB |
@@ -4,18 +4,15 @@ namespace Empostor.Api.Config
|
||||
{
|
||||
public const string Section = "DiscordWebhook";
|
||||
|
||||
public bool Enabled { get; set; } = false;
|
||||
public string MatchmakerUrl { get; set; } = string.Empty;
|
||||
|
||||
public string WebhookUrl { get; set; } = string.Empty;
|
||||
|
||||
public bool NotifyOnGameCreated { get; set; } = true;
|
||||
|
||||
public bool NotifyOnBan { get; set; } = true;
|
||||
|
||||
public bool NotifyOnReport { get; set; } = true;
|
||||
|
||||
public bool NotifyOnPlayerJoin { get; set; } = false;
|
||||
|
||||
public bool NotifyOnGameEnded { get; set; } = false;
|
||||
public string AdminUrl { get; set; } = string.Empty;
|
||||
public bool? Enabled { get; set; }
|
||||
public string? WebhookUrl { get; set; }
|
||||
public bool? NotifyOnGameCreated { get; set; }
|
||||
public bool? NotifyOnBan { get; set; }
|
||||
public bool? NotifyOnReport { get; set; }
|
||||
public bool? NotifyOnPlayerJoin { get; set; }
|
||||
public bool? NotifyOnGameEnded { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -719,13 +719,8 @@ namespace Empostor.Server.Http
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
enabled = _discordWebhook.Enabled,
|
||||
webhookUrl = _discordWebhook.WebhookUrl,
|
||||
notifyOnGameCreated = _discordWebhook.NotifyOnGameCreated,
|
||||
notifyOnBan = _discordWebhook.NotifyOnBan,
|
||||
notifyOnReport = _discordWebhook.NotifyOnReport,
|
||||
notifyOnPlayerJoin = _discordWebhook.NotifyOnPlayerJoin,
|
||||
notifyOnGameEnded = _discordWebhook.NotifyOnGameEnded,
|
||||
matchmakerUrl = _discordWebhook.MatchmakerUrl,
|
||||
adminUrl = _discordWebhook.AdminUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -737,52 +732,22 @@ namespace Empostor.Server.Http
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
if (req.Enabled.HasValue)
|
||||
if (req.MatchmakerUrl != null)
|
||||
{
|
||||
_discordWebhook.Enabled = req.Enabled.Value;
|
||||
_discordWebhook.MatchmakerUrl = req.MatchmakerUrl;
|
||||
}
|
||||
|
||||
if (req.WebhookUrl != null)
|
||||
if (req.AdminUrl != null)
|
||||
{
|
||||
_discordWebhook.WebhookUrl = req.WebhookUrl;
|
||||
}
|
||||
|
||||
if (req.NotifyOnGameCreated.HasValue)
|
||||
{
|
||||
_discordWebhook.NotifyOnGameCreated = req.NotifyOnGameCreated.Value;
|
||||
}
|
||||
|
||||
if (req.NotifyOnBan.HasValue)
|
||||
{
|
||||
_discordWebhook.NotifyOnBan = req.NotifyOnBan.Value;
|
||||
}
|
||||
|
||||
if (req.NotifyOnReport.HasValue)
|
||||
{
|
||||
_discordWebhook.NotifyOnReport = req.NotifyOnReport.Value;
|
||||
}
|
||||
|
||||
if (req.NotifyOnPlayerJoin.HasValue)
|
||||
{
|
||||
_discordWebhook.NotifyOnPlayerJoin = req.NotifyOnPlayerJoin.Value;
|
||||
}
|
||||
|
||||
if (req.NotifyOnGameEnded.HasValue)
|
||||
{
|
||||
_discordWebhook.NotifyOnGameEnded = req.NotifyOnGameEnded.Value;
|
||||
_discordWebhook.AdminUrl = req.AdminUrl;
|
||||
}
|
||||
|
||||
await _discordWebhook.SaveAsync();
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
enabled = _discordWebhook.Enabled,
|
||||
webhookUrl = _discordWebhook.WebhookUrl,
|
||||
notifyOnGameCreated = _discordWebhook.NotifyOnGameCreated,
|
||||
notifyOnBan = _discordWebhook.NotifyOnBan,
|
||||
notifyOnReport = _discordWebhook.NotifyOnReport,
|
||||
notifyOnPlayerJoin = _discordWebhook.NotifyOnPlayerJoin,
|
||||
notifyOnGameEnded = _discordWebhook.NotifyOnGameEnded,
|
||||
matchmakerUrl = _discordWebhook.MatchmakerUrl,
|
||||
adminUrl = _discordWebhook.AdminUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -914,7 +879,7 @@ namespace Empostor.Server.Http
|
||||
|
||||
public sealed record ChatFilterSettingsReq(bool? Enabled, bool? BlockMessage, int? SpamThreshold, int? SpamWindowSeconds);
|
||||
|
||||
public sealed record DiscordWebhookSettingsReq(bool? Enabled, string? WebhookUrl, bool? NotifyOnGameCreated, bool? NotifyOnBan, bool? NotifyOnReport, bool? NotifyOnPlayerJoin, bool? NotifyOnGameEnded);
|
||||
public sealed record DiscordWebhookSettingsReq(string? MatchmakerUrl, string? AdminUrl);
|
||||
|
||||
// HTML pages are auto-generated to BaseDirectory/Pages/ on first access.
|
||||
// Server operators can customize them by editing the files on disk.
|
||||
|
||||
@@ -1044,37 +1044,17 @@ internal static class AdminTemplateDefaults
|
||||
<h2 style="margin:0"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg><span data-i18n="discord.title">Discord Webhook</span></h2>
|
||||
<button class="bp bsm" onclick="saveDwSettings()"><span data-i18n="discord.save">Save Settings</span></button>
|
||||
<div style="margin-top:12px">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="dw-enabled" onchange="saveDwSettings()">
|
||||
<span data-i18n="discord.enabled">Enable Discord Webhook</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="field" style="margin-top:10px">
|
||||
<label data-i18n="discord.webhook_url">Webhook URL</label>
|
||||
<input type="text" id="dw-url" data-i18n-placeholder="discord.webhook_url_placeholder" placeholder="https://discord.com/api/webhooks/..." onchange="saveDwSettings()">
|
||||
</div>
|
||||
<h4 data-i18n="discord.notifications" style="margin:12px 0 8px">Notifications</h4>
|
||||
<div style="display:flex;flex-direction:column;gap:6px">
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="dw-game-created" onchange="saveDwSettings()">
|
||||
<span data-i18n="discord.notify_game_created">Game Created</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="dw-ban" onchange="saveDwSettings()">
|
||||
<span data-i18n="discord.notify_ban">Player Banned</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="dw-report" onchange="saveDwSettings()">
|
||||
<span data-i18n="discord.notify_report">Player Reported</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="dw-player-join" onchange="saveDwSettings()">
|
||||
<span data-i18n="discord.notify_player_join">Player Joined</span>
|
||||
</label>
|
||||
<label style="display:flex;align-items:center;gap:6px;cursor:pointer">
|
||||
<input type="checkbox" id="dw-game-ended" onchange="saveDwSettings()">
|
||||
<span data-i18n="discord.notify_game_ended">Game Ended</span>
|
||||
</label>
|
||||
<div class="field">
|
||||
<label data-i18n="discord.matchmaker_url">Matchmaker Webhook URL</label>
|
||||
<input type="text" id="dw-matchmaker-url" data-i18n-placeholder="discord.matchmaker_url_placeholder" placeholder="https://discord.com/api/webhooks/..." onchange="saveDwSettings()">
|
||||
<div style="font-size:11px;color:var(--m);margin-top:4px"><span data-i18n="discord.matchmaker_desc">Game Created, Game Started, Game Ended, Player Joined</span></div>
|
||||
</div>
|
||||
<div class="field" style="margin-top:10px">
|
||||
<label data-i18n="discord.admin_url">Admin Webhook URL</label>
|
||||
<input type="text" id="dw-admin-url" data-i18n-placeholder="discord.admin_url_placeholder" placeholder="https://discord.com/api/webhooks/..." onchange="saveDwSettings()">
|
||||
<div style="font-size:11px;color:var(--m);margin-top:4px"><span data-i18n="discord.admin_desc">Player Banned, Player Reported</span></div>
|
||||
</div>
|
||||
<div style="font-size:12px;color:var(--m);margin-top:10px"><span data-i18n="discord.hint">Leave a URL empty to disable its notifications. Leave both empty to disable all webhooks.</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="dw-r" class="msg"></div>
|
||||
@@ -1418,27 +1398,20 @@ internal static class AdminTemplateDefaults
|
||||
|
||||
async function fDiscordWebhook() {
|
||||
const { data } = await api('GET', '/api/admin/discord');
|
||||
document.getElementById('dw-enabled').checked = data.enabled;
|
||||
const urlEl = document.getElementById('dw-url');
|
||||
if (document.activeElement !== urlEl) {
|
||||
urlEl.value = data.webhookUrl || '';
|
||||
const mmUrlEl = document.getElementById('dw-matchmaker-url');
|
||||
const adminUrlEl = document.getElementById('dw-admin-url');
|
||||
if (document.activeElement !== mmUrlEl) {
|
||||
mmUrlEl.value = data.matchmakerUrl || '';
|
||||
}
|
||||
if (document.activeElement !== adminUrlEl) {
|
||||
adminUrlEl.value = data.adminUrl || '';
|
||||
}
|
||||
document.getElementById('dw-game-created').checked = data.notifyOnGameCreated;
|
||||
document.getElementById('dw-ban').checked = data.notifyOnBan;
|
||||
document.getElementById('dw-report').checked = data.notifyOnReport;
|
||||
document.getElementById('dw-player-join').checked = data.notifyOnPlayerJoin;
|
||||
document.getElementById('dw-game-ended').checked = data.notifyOnGameEnded;
|
||||
}
|
||||
|
||||
async function saveDwSettings() {
|
||||
const { ok, data } = await api('POST', '/api/admin/discord', {
|
||||
enabled: document.getElementById('dw-enabled').checked,
|
||||
webhookUrl: document.getElementById('dw-url').value.trim(),
|
||||
notifyOnGameCreated: document.getElementById('dw-game-created').checked,
|
||||
notifyOnBan: document.getElementById('dw-ban').checked,
|
||||
notifyOnReport: document.getElementById('dw-report').checked,
|
||||
notifyOnPlayerJoin: document.getElementById('dw-player-join').checked,
|
||||
notifyOnGameEnded: document.getElementById('dw-game-ended').checked
|
||||
matchmakerUrl: document.getElementById('dw-matchmaker-url').value.trim(),
|
||||
adminUrl: document.getElementById('dw-admin-url').value.trim()
|
||||
});
|
||||
if (ok) {
|
||||
msg('dw-r', true, _('discord.saved', 'Discord webhook settings saved.'));
|
||||
|
||||
@@ -217,14 +217,12 @@
|
||||
|
||||
"discord.title": "Discord Webhook",
|
||||
"discord.save": "Save Settings",
|
||||
"discord.enabled": "Enable Discord Webhook",
|
||||
"discord.webhook_url": "Webhook URL",
|
||||
"discord.webhook_url_placeholder": "https://discord.com/api/webhooks/...",
|
||||
"discord.notifications": "Notifications",
|
||||
"discord.notify_game_created": "Game Created",
|
||||
"discord.notify_ban": "Player Banned",
|
||||
"discord.notify_report": "Player Reported",
|
||||
"discord.notify_player_join": "Player Joined",
|
||||
"discord.notify_game_ended": "Game Ended",
|
||||
"discord.matchmaker_url": "Matchmaker Webhook URL",
|
||||
"discord.matchmaker_url_placeholder": "https://discord.com/api/webhooks/...",
|
||||
"discord.matchmaker_desc": "Game Created, Game Started, Game Ended, Player Joined",
|
||||
"discord.admin_url": "Admin Webhook URL",
|
||||
"discord.admin_url_placeholder": "https://discord.com/api/webhooks/...",
|
||||
"discord.admin_desc": "Player Banned, Player Reported",
|
||||
"discord.hint": "Leave a URL empty to disable its notifications. Leave both empty to disable all webhooks.",
|
||||
"discord.saved": "Discord webhook settings saved."
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
@@ -19,7 +18,6 @@ internal sealed class DiscordWebhookListener : IEventListener
|
||||
private readonly ILogger<DiscordWebhookListener> _logger;
|
||||
private readonly IHttpClientFactory _http;
|
||||
private readonly DiscordWebhookStore _config;
|
||||
private readonly ConcurrentDictionary<string, DateTime> _throttle = new();
|
||||
|
||||
public DiscordWebhookListener(
|
||||
ILogger<DiscordWebhookListener> logger,
|
||||
@@ -34,12 +32,13 @@ internal sealed class DiscordWebhookListener : IEventListener
|
||||
[EventListener]
|
||||
public ValueTask OnGameCreated(IGameCreatedEvent e)
|
||||
{
|
||||
if (!_config.Enabled || !_config.NotifyOnGameCreated)
|
||||
var url = _config.MatchmakerUrl;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return SendAsync("Game Created", 3066993, new()
|
||||
return SendAsync(url, "Game Created", 3066993, new()
|
||||
{
|
||||
["Game"] = GameCodeParser.IntToGameName(e.Game.Code),
|
||||
["Host"] = e.Host?.Name ?? "—",
|
||||
@@ -48,49 +47,33 @@ internal sealed class DiscordWebhookListener : IEventListener
|
||||
}
|
||||
|
||||
[EventListener]
|
||||
public ValueTask OnPlayerLeft(IGamePlayerLeftEvent e)
|
||||
public ValueTask OnGameStarted(IGameStartedEvent e)
|
||||
{
|
||||
if (!_config.Enabled || !_config.NotifyOnBan || !e.IsBan)
|
||||
var url = _config.MatchmakerUrl;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return SendAsync("Player Banned", 15158332, new()
|
||||
return SendAsync(url, "Game Started", 3447003, new()
|
||||
{
|
||||
["Player"] = e.Player.Client.Name,
|
||||
["Friend Code"] = e.Player.Client.FriendCode ?? "—",
|
||||
["Game"] = GameCodeParser.IntToGameName(e.Game.Code),
|
||||
});
|
||||
}
|
||||
|
||||
[EventListener]
|
||||
public ValueTask OnPlayerReport(IPlayerReportEvent e)
|
||||
{
|
||||
if (!_config.Enabled || !_config.NotifyOnReport)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return SendAsync("Player Reported", 16776960, new()
|
||||
{
|
||||
["Reporter"] = e.ClientPlayer.Client.Name,
|
||||
["Reporter FC"] = e.ClientPlayer.Client.FriendCode ?? "—",
|
||||
["Reported"] = e.ReportedClient?.Name ?? "body",
|
||||
["Reported FC"] = e.ReportedClient?.FriendCode ?? "—",
|
||||
["Game"] = GameCodeParser.IntToGameName(e.Game.Code),
|
||||
["Reason"] = e.Reason.ToString(),
|
||||
["Map"] = e.Game.Options.Map.ToString(),
|
||||
["Players"] = e.Game.PlayerCount.ToString(),
|
||||
["Impostors"] = e.Game.Options.NumImpostors.ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
[EventListener]
|
||||
public ValueTask OnPlayerJoin(IGamePlayerJoinedEvent e)
|
||||
{
|
||||
if (!_config.Enabled || !_config.NotifyOnPlayerJoin)
|
||||
var url = _config.MatchmakerUrl;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return SendAsync("Player Joined", 3066993, new()
|
||||
return SendAsync(url, "Player Joined", 3066993, new()
|
||||
{
|
||||
["Player"] = e.Player.Client.Name,
|
||||
["Friend Code"] = e.Player.Client.FriendCode ?? "—",
|
||||
@@ -102,12 +85,13 @@ internal sealed class DiscordWebhookListener : IEventListener
|
||||
[EventListener]
|
||||
public ValueTask OnGameEnded(IGameEndedEvent e)
|
||||
{
|
||||
if (!_config.Enabled || !_config.NotifyOnGameEnded)
|
||||
var url = _config.MatchmakerUrl;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return SendAsync("Game Ended", 10181046, new()
|
||||
return SendAsync(url, "Game Ended", 10181046, new()
|
||||
{
|
||||
["Game"] = GameCodeParser.IntToGameName(e.Game.Code),
|
||||
["Result"] = e.GameOverReason.ToString(),
|
||||
@@ -115,22 +99,51 @@ internal sealed class DiscordWebhookListener : IEventListener
|
||||
});
|
||||
}
|
||||
|
||||
private async ValueTask SendAsync(string title, int color, Dictionary<string, string> fields)
|
||||
// ── Admin events ───────────────────────────────────────────────
|
||||
[EventListener]
|
||||
public ValueTask OnPlayerLeft(IGamePlayerLeftEvent e)
|
||||
{
|
||||
var url = _config.WebhookUrl;
|
||||
if (!e.IsBan)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var url = _config.AdminUrl;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return;
|
||||
return default;
|
||||
}
|
||||
|
||||
var key = title + (fields.TryGetValue("Game", out var gc) ? gc : string.Empty);
|
||||
if (_throttle.TryGetValue(key, out var last) && (DateTime.UtcNow - last).TotalSeconds < 2)
|
||||
return SendAsync(url, "Player Banned", 15158332, new()
|
||||
{
|
||||
return;
|
||||
["Player"] = e.Player.Client.Name,
|
||||
["Friend Code"] = e.Player.Client.FriendCode ?? "—",
|
||||
["Game"] = GameCodeParser.IntToGameName(e.Game.Code),
|
||||
});
|
||||
}
|
||||
|
||||
[EventListener]
|
||||
public ValueTask OnPlayerReport(IPlayerReportEvent e)
|
||||
{
|
||||
var url = _config.AdminUrl;
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
_throttle[key] = DateTime.UtcNow;
|
||||
return SendAsync(url, "Player Reported", 16776960, new()
|
||||
{
|
||||
["Reporter"] = e.ClientPlayer.Client.Name,
|
||||
["Reporter FC"] = e.ClientPlayer.Client.FriendCode ?? "—",
|
||||
["Reported"] = e.ReportedClient?.Name ?? "body",
|
||||
["Reported FC"] = e.ReportedClient?.FriendCode ?? "—",
|
||||
["Game"] = GameCodeParser.IntToGameName(e.Game.Code),
|
||||
["Reason"] = e.Reason.ToString(),
|
||||
});
|
||||
}
|
||||
|
||||
private async ValueTask SendAsync(string url, string title, int color, Dictionary<string, string> fields)
|
||||
{
|
||||
try
|
||||
{
|
||||
var embed = new
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Empostor.Api.Config;
|
||||
@@ -12,7 +13,11 @@ namespace Empostor.Server.Service.Api;
|
||||
public sealed class DiscordWebhookStore
|
||||
{
|
||||
private static readonly string ConfigFile = Path.Combine(Directory.GetCurrentDirectory(), "discord_webhook.json");
|
||||
private static readonly JsonSerializerOptions JsonOpts = new() { WriteIndented = true };
|
||||
private static readonly JsonSerializerOptions JsonOpts = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly ILogger<DiscordWebhookStore> _logger;
|
||||
private readonly SemaphoreSlim _saveLock = new(1, 1);
|
||||
@@ -21,23 +26,17 @@ public sealed class DiscordWebhookStore
|
||||
{
|
||||
_logger = logger;
|
||||
|
||||
DiscordWebhookConfig? cfg = null;
|
||||
|
||||
if (File.Exists(ConfigFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(ConfigFile);
|
||||
var saved = JsonSerializer.Deserialize<DiscordWebhookConfig>(json);
|
||||
if (saved != null)
|
||||
cfg = JsonSerializer.Deserialize<DiscordWebhookConfig>(json);
|
||||
if (cfg != null)
|
||||
{
|
||||
Enabled = saved.Enabled;
|
||||
WebhookUrl = saved.WebhookUrl;
|
||||
NotifyOnGameCreated = saved.NotifyOnGameCreated;
|
||||
NotifyOnBan = saved.NotifyOnBan;
|
||||
NotifyOnReport = saved.NotifyOnReport;
|
||||
NotifyOnPlayerJoin = saved.NotifyOnPlayerJoin;
|
||||
NotifyOnGameEnded = saved.NotifyOnGameEnded;
|
||||
_logger.LogInformation("DiscordWebhookLoaded from {File}", ConfigFile);
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -47,39 +46,23 @@ public sealed class DiscordWebhookStore
|
||||
}
|
||||
|
||||
// Fall back to config.json values
|
||||
var cfg = config.Value;
|
||||
Enabled = cfg.Enabled;
|
||||
WebhookUrl = cfg.WebhookUrl;
|
||||
NotifyOnGameCreated = cfg.NotifyOnGameCreated;
|
||||
NotifyOnBan = cfg.NotifyOnBan;
|
||||
NotifyOnReport = cfg.NotifyOnReport;
|
||||
NotifyOnPlayerJoin = cfg.NotifyOnPlayerJoin;
|
||||
NotifyOnGameEnded = cfg.NotifyOnGameEnded;
|
||||
cfg ??= config.Value;
|
||||
|
||||
// Migrate legacy WebhookUrl to new fields if present and new fields are empty
|
||||
MigrateLegacy(cfg);
|
||||
|
||||
MatchmakerUrl = cfg.MatchmakerUrl;
|
||||
AdminUrl = cfg.AdminUrl;
|
||||
}
|
||||
|
||||
public bool Enabled { get; set; }
|
||||
public string MatchmakerUrl { get; set; } = string.Empty;
|
||||
|
||||
public string WebhookUrl { get; set; } = string.Empty;
|
||||
|
||||
public bool NotifyOnGameCreated { get; set; } = true;
|
||||
|
||||
public bool NotifyOnBan { get; set; } = true;
|
||||
|
||||
public bool NotifyOnReport { get; set; } = true;
|
||||
|
||||
public bool NotifyOnPlayerJoin { get; set; }
|
||||
|
||||
public bool NotifyOnGameEnded { get; set; }
|
||||
public string AdminUrl { get; set; } = string.Empty;
|
||||
|
||||
public DiscordWebhookConfig Snapshot => new()
|
||||
{
|
||||
Enabled = Enabled,
|
||||
WebhookUrl = WebhookUrl,
|
||||
NotifyOnGameCreated = NotifyOnGameCreated,
|
||||
NotifyOnBan = NotifyOnBan,
|
||||
NotifyOnReport = NotifyOnReport,
|
||||
NotifyOnPlayerJoin = NotifyOnPlayerJoin,
|
||||
NotifyOnGameEnded = NotifyOnGameEnded,
|
||||
MatchmakerUrl = MatchmakerUrl,
|
||||
AdminUrl = AdminUrl,
|
||||
};
|
||||
|
||||
public async ValueTask SaveAsync()
|
||||
@@ -104,4 +87,20 @@ public sealed class DiscordWebhookStore
|
||||
_saveLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static void MigrateLegacy(DiscordWebhookConfig cfg)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(cfg.WebhookUrl))
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(cfg.MatchmakerUrl))
|
||||
{
|
||||
cfg.MatchmakerUrl = cfg.WebhookUrl;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(cfg.AdminUrl))
|
||||
{
|
||||
cfg.AdminUrl = cfg.WebhookUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -54,13 +54,8 @@
|
||||
"MarketplaceUrl": "https://raw.githubusercontent.com/Empostor/Empostor/master/marketplace/plugins.json"
|
||||
},
|
||||
"DiscordWebhook": {
|
||||
"Enabled": false,
|
||||
"WebhookUrl": "",
|
||||
"NotifyOnGameCreated": true,
|
||||
"NotifyOnBan": true,
|
||||
"NotifyOnReport": true,
|
||||
"NotifyOnPlayerJoin": false,
|
||||
"NotifyOnGameEnded": false
|
||||
"MatchmakerUrl": "",
|
||||
"AdminUrl": ""
|
||||
},
|
||||
"PlayerStats": {
|
||||
"Enabled": false,
|
||||
|
||||
Reference in new issue
Block a user