Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b7df5a3fde | |||
| 17f7dc2e0d | |||
| 12af1b2981 | |||
| 1f8d050325 | |||
| de437fd914 |
+3
-12
@@ -1,28 +1,19 @@
|
||||
# ── Stage 1: Build ────────────────────────────────────────────────────────────
|
||||
FROM eclipse-temurin:21-jdk-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pom.xml .
|
||||
COPY src ./src
|
||||
|
||||
# Download dependencies (cached layer), then build
|
||||
RUN apk add --no-cache maven && \
|
||||
mvn dependency:go-offline -q && \
|
||||
mvn package -DskipTests -q
|
||||
|
||||
# ── Stage 2: Runtime ──────────────────────────────────────────────────────────
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
FROM gcr.io/distroless/java21-debian12:nonroot
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the fat JAR from the build stage
|
||||
COPY --from=build /app/target/overseerr-webhook-proxy-*.jar app.jar
|
||||
|
||||
# The application.yml can be overridden by mounting a file at:
|
||||
# /app/config/application.yml
|
||||
VOLUME ["/app/config"]
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
ENTRYPOINT ["java", \
|
||||
"-Dspring.config.additional-location=optional:file:/app/config/application.yml", \
|
||||
"-jar", "app.jar"]
|
||||
"-jar", "app.jar"]
|
||||
+3
-25
@@ -1,44 +1,22 @@
|
||||
services:
|
||||
webhook-proxy:
|
||||
image: viziona.dev/taiqane/discord-webhook-proxy:nightly
|
||||
build: .
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
# Mount your custom config here — overrides the defaults inside the image
|
||||
- ./config:/app/config:ro
|
||||
environment:
|
||||
# Optional: override the port
|
||||
- LOG_LEVEL=debug
|
||||
- SERVER_PORT=8080
|
||||
- "127.0.0.1:8080:8080"
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- seerr-net
|
||||
|
||||
# ── Jellyseerr (local test instance) ─────────────────────────────────────
|
||||
# Access the UI at: http://localhost:5055
|
||||
#
|
||||
# After first start, go through the setup wizard. When asked for a media
|
||||
# server you can skip it or point to a local Jellyfin (see below).
|
||||
#
|
||||
# Webhook setup in Jellyseerr:
|
||||
# Settings → Notifications → Webhook
|
||||
# URL: http://webhook-proxy:8080/webhook ← uses the internal Docker hostname
|
||||
# Payload: leave as default
|
||||
seerr:
|
||||
image: seerr/seerr:latest
|
||||
container_name: seerr
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "5055:5055"
|
||||
environment:
|
||||
- LOG_LEVEL=debug
|
||||
- TZ=Europe/Berlin
|
||||
networks:
|
||||
- seerr-net
|
||||
|
||||
# ── Jellyfin (optional — only needed if Jellyseerr asks for a media server)
|
||||
# Access the UI at: http://localhost:8096
|
||||
# Remove this block if you don't want a full media server locally.
|
||||
jellyfin:
|
||||
image: jellyfin/jellyfin:latest
|
||||
container_name: jellyfin
|
||||
|
||||
@@ -4,19 +4,13 @@ import lombok.*;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "proxy")
|
||||
public class ProxyConfig {
|
||||
|
||||
/**
|
||||
* Map of notification_type → list of Discord webhook URLs.
|
||||
* Use the key "default" to catch all unmatched notification types.
|
||||
*/
|
||||
private List<String> routes = List.of();
|
||||
private Filters filters;
|
||||
|
||||
|
||||
@@ -9,12 +9,6 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* Receives incoming Overseerr webhook POST requests and
|
||||
* delegates forwarding to {@link DiscordForwardingService}.
|
||||
*
|
||||
* Endpoint: POST /webhook
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@@ -24,10 +18,6 @@ public class WebhookController {
|
||||
private final ObjectMapper objectMapper;
|
||||
private final ProxyConfig proxyConfig;
|
||||
|
||||
/**
|
||||
* Main webhook endpoint.
|
||||
* Overseerr should be configured to POST to: http://<your-host>:<port>/webhook
|
||||
*/
|
||||
@PostMapping("/webhook")
|
||||
public ResponseEntity<String> receiveWebhook(@RequestBody String rawBody) {
|
||||
log.info("Received webhook payload: {}", rawBody);
|
||||
@@ -51,17 +41,8 @@ public class WebhookController {
|
||||
}
|
||||
}
|
||||
|
||||
// Forward asynchronously — we return 200 immediately to Overseerr
|
||||
forwardingService.forward(payload, rawBody);
|
||||
|
||||
return ResponseEntity.ok("OK");
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple health / test endpoint.
|
||||
*/
|
||||
@GetMapping("/health")
|
||||
public ResponseEntity<String> health() {
|
||||
return ResponseEntity.ok("Webhook proxy is running.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,10 +23,7 @@ public class SeerrPayload {
|
||||
|
||||
/** Media overview / synopsis, or issue description. */
|
||||
private String message;
|
||||
|
||||
/** URL to the media poster image. */
|
||||
private String image;
|
||||
|
||||
private Media media;
|
||||
private Request request;
|
||||
private Issue issue;
|
||||
@@ -39,14 +36,9 @@ public class SeerrPayload {
|
||||
|
||||
@JsonProperty("media_type")
|
||||
private String mediaType;
|
||||
|
||||
private String tmdbId;
|
||||
private String tvdbId;
|
||||
|
||||
/** Availability status: UNKNOWN | PENDING | PROCESSING | PARTIALLY_AVAILABLE | AVAILABLE */
|
||||
private String status;
|
||||
|
||||
/** 4K availability status (same values as status). */
|
||||
private String status4k;
|
||||
}
|
||||
|
||||
@@ -80,11 +72,9 @@ public class SeerrPayload {
|
||||
@JsonProperty("issue_id")
|
||||
private String issueId;
|
||||
|
||||
/** VIDEO | AUDIO | SUBTITLES | OTHER */
|
||||
@JsonProperty("issue_type")
|
||||
private String issueType;
|
||||
|
||||
/** OPEN | IN_PROGRESS | RESOLVED */
|
||||
@JsonProperty("issue_status")
|
||||
private String issueStatus;
|
||||
|
||||
|
||||
@@ -18,10 +18,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
/**
|
||||
* Builds a Discord embed message from the Overseerr payload
|
||||
* and forwards it concurrently to all configured webhook URLs.
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@@ -34,12 +30,6 @@ public class DiscordForwardingService {
|
||||
.connectTimeout(Duration.ofSeconds(5))
|
||||
.build();
|
||||
|
||||
/**
|
||||
* Forwards the payload to all Discord webhooks matching the notification type.
|
||||
*
|
||||
* @param payload the parsed Overseerr payload
|
||||
* @param rawBody the original raw JSON body (used as fallback)
|
||||
*/
|
||||
public void forward(SeerrPayload payload, String rawBody) {
|
||||
String mediaName = payload.getSubject();
|
||||
List<String> targets = proxyConfig.getRoutes();
|
||||
@@ -53,20 +43,14 @@ public class DiscordForwardingService {
|
||||
|
||||
String discordBody = buildDiscordPayload(payload);
|
||||
|
||||
// Fire all requests concurrently
|
||||
List<CompletableFuture<Void>> futures = new ArrayList<>();
|
||||
for (String webhookUrl : targets) {
|
||||
futures.add(sendAsync(webhookUrl, discordBody, mediaName));
|
||||
}
|
||||
|
||||
// Wait for all to complete (best-effort, errors are logged but not re-thrown)
|
||||
CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Internal helpers
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private CompletableFuture<Void> sendAsync(String webhookUrl, String body, String notificationType) {
|
||||
HttpRequest request = HttpRequest.newBuilder()
|
||||
.uri(URI.create(webhookUrl))
|
||||
@@ -92,10 +76,6 @@ public class DiscordForwardingService {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a Discord-compatible JSON payload with an embed.
|
||||
* Discord webhooks expect: { "embeds": [ { "title": "...", ... } ] }
|
||||
*/
|
||||
private String buildDiscordPayload(SeerrPayload payload) {
|
||||
try {
|
||||
int color = colorForType(payload.getNotificationType());
|
||||
@@ -110,7 +90,6 @@ public class DiscordForwardingService {
|
||||
embed.put("thumbnail", Map.of("url", payload.getImage()));
|
||||
}
|
||||
|
||||
// Field: requested by
|
||||
List<Map<String, Object>> fields = new ArrayList<>();
|
||||
if (payload.getRequest() != null && payload.getRequest().getRequestedByUsername() != null) {
|
||||
fields.add(field("Requested by", payload.getRequest().getRequestedByUsername(), true));
|
||||
@@ -153,9 +132,6 @@ public class DiscordForwardingService {
|
||||
return Map.of("name", name, "value", value, "inline", inline);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a Discord embed color (decimal) based on the notification type.
|
||||
*/
|
||||
private int colorForType(String type) {
|
||||
if (type == null) return 0x95A5A6; // grey
|
||||
return switch (type) {
|
||||
@@ -166,11 +142,6 @@ public class DiscordForwardingService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Masks the webhook token in the URL for safe logging.
|
||||
* Input: https://discord.com/api/webhooks/1234567890/AbCdEfGhIj...
|
||||
* Output: https://discord.com/api/webhooks/1234567890/Ab***
|
||||
*/
|
||||
private String maskUrl(String url) {
|
||||
int lastSlash = url.lastIndexOf('/');
|
||||
if (lastSlash < 0 || lastSlash >= url.length() - 1) return url;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
server:
|
||||
port: 8080
|
||||
|
||||
proxy:
|
||||
filters:
|
||||
usernameFilter:
|
||||
- private
|
||||
routes:
|
||||
- https://discord.com/api/webhooks/1498794785475920022/bwoytZA_iDvza86fbR9aOFXNOcv9_Fl5P5taoOaMHjey_X3YLRt5FGbDjt9uPiKHJ8yi
|
||||
|
||||
# Spring Boot Actuator — exposes /actuator/health endpoint
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, info
|
||||
|
||||
logging:
|
||||
level:
|
||||
dev.webhookproxy: DEBUG
|
||||
@@ -1,5 +0,0 @@
|
||||
/home/mathias/Downloads/overseerr-webhook-proxy/src/main/java/dev/webhookproxy/WebhookProxyApplication.java
|
||||
/home/mathias/Downloads/overseerr-webhook-proxy/src/main/java/dev/webhookproxy/config/ProxyConfig.java
|
||||
/home/mathias/Downloads/overseerr-webhook-proxy/src/main/java/dev/webhookproxy/model/SeerrPayload.java
|
||||
/home/mathias/Downloads/overseerr-webhook-proxy/src/main/java/dev/webhookproxy/service/DiscordForwardingService.java
|
||||
/home/mathias/Downloads/overseerr-webhook-proxy/src/main/java/dev/webhookproxy/controller/WebhookController.java
|
||||
Reference in New Issue
Block a user