Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4ee3b8ef7 | |||
| 64c872423f | |||
| 86c67bcf29 |
@@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
|
|||||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.7.9] - 2026-05-23
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- **Ombi webhook PascalCase payload parsing (Re-release)** — Correctly packaged and released the Ombi webhook parsing logic to handle PascalCase property names (e.g., `NotificationType`, `RequestId`) sent by C#/.NET applications. This ensures standard Ombi webhook integrations function correctly. Resolves Gitea Issue [#42](https://git.i3omb.com/Gandalf/sofarr/issues/42).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## [1.7.8] - 2026-05-23
|
## [1.7.8] - 2026-05-23
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.8",
|
"version": "1.7.9",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.8",
|
"version": "1.7.9",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "sofarr",
|
"name": "sofarr",
|
||||||
"version": "1.7.8",
|
"version": "1.7.9",
|
||||||
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
||||||
"main": "server/index.js",
|
"main": "server/index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -717,9 +717,12 @@ router.post('/ombi', webhookLimiter, (req, res) => {
|
|||||||
return res.status(401).json({ error: 'Unauthorized' });
|
return res.status(401).json({ error: 'Unauthorized' });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ombi uses notificationType instead of eventType
|
// Ombi uses notificationType instead of eventType. Support PascalCase for .NET apps
|
||||||
const { notificationType, requestId, requestedUser, applicationUrl } = req.body;
|
const notificationType = req.body.notificationType || req.body.NotificationType;
|
||||||
const eventType = notificationType || req.body.eventType;
|
const requestId = req.body.requestId || req.body.RequestId;
|
||||||
|
const applicationUrl = req.body.applicationUrl || req.body.ApplicationUrl;
|
||||||
|
|
||||||
|
const eventType = notificationType || req.body.eventType || req.body.EventType;
|
||||||
|
|
||||||
// Extract username from requestedUser (handles both object and string formats)
|
// Extract username from requestedUser (handles both object and string formats)
|
||||||
const username = extractRequestedUser(req.body);
|
const username = extractRequestedUser(req.body);
|
||||||
@@ -732,7 +735,7 @@ router.post('/ombi', webhookLimiter, (req, res) => {
|
|||||||
// Use applicationUrl as instance identifier for replay protection
|
// Use applicationUrl as instance identifier for replay protection
|
||||||
const instanceName = applicationUrl || 'ombi';
|
const instanceName = applicationUrl || 'ombi';
|
||||||
// Use requestId + eventType + current time as replay key
|
// Use requestId + eventType + current time as replay key
|
||||||
const eventDate = req.body.requestedDate || new Date().toISOString();
|
const eventDate = req.body.requestedDate || req.body.RequestedDate || new Date().toISOString();
|
||||||
|
|
||||||
if (isReplay(eventType, instanceName, `${requestId}-${eventDate}`)) {
|
if (isReplay(eventType, instanceName, `${requestId}-${eventDate}`)) {
|
||||||
logToFile(`[Webhook] Ombi duplicate event ignored: ${eventType} requestId=${requestId}`);
|
logToFile(`[Webhook] Ombi duplicate event ignored: ${eventType} requestId=${requestId}`);
|
||||||
|
|||||||
@@ -15,17 +15,19 @@
|
|||||||
function extractRequestedUser(request) {
|
function extractRequestedUser(request) {
|
||||||
if (!request) return '';
|
if (!request) return '';
|
||||||
|
|
||||||
|
const requestedUser = request.requestedUser || request.RequestedUser;
|
||||||
|
|
||||||
// Handle object format: OmbiStore.Entities.OmbiUser
|
// Handle object format: OmbiStore.Entities.OmbiUser
|
||||||
if (request.requestedUser && typeof request.requestedUser === 'object') {
|
if (requestedUser && typeof requestedUser === 'object') {
|
||||||
// Priority: alias > userAlias > userName > normalizedUserName > requestedByAlias
|
// Priority: alias > userAlias > userName > normalizedUserName > requestedByAlias
|
||||||
return request.requestedUser.alias ||
|
return requestedUser.alias || requestedUser.Alias ||
|
||||||
request.requestedUser.userAlias ||
|
requestedUser.userAlias || requestedUser.UserAlias ||
|
||||||
request.requestedUser.userName ||
|
requestedUser.userName || requestedUser.UserName ||
|
||||||
request.requestedUser.normalizedUserName ||
|
requestedUser.normalizedUserName || requestedUser.NormalizedUserName ||
|
||||||
request.requestedByAlias || '';
|
request.requestedByAlias || request.RequestedByAlias || '';
|
||||||
}
|
}
|
||||||
// Handle string format (fallback for compatibility)
|
// Handle string format (fallback for compatibility)
|
||||||
return request.requestedUser || request.requestedByAlias || '';
|
return requestedUser || request.requestedByAlias || request.RequestedByAlias || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterRequestsByUser(requests, username, showAll) {
|
function filterRequestsByUser(requests, username, showAll) {
|
||||||
|
|||||||
@@ -647,5 +647,32 @@ describe('POST /api/webhook/ombi', () => {
|
|||||||
expect(res2.status).toBe(200);
|
expect(res2.status).toBe(200);
|
||||||
expect(res2.body.duplicate).toBe(true);
|
expect(res2.body.duplicate).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns 200 { received: true } for a valid NewRequest event with PascalCase payload', async () => {
|
||||||
|
const app = makeApp();
|
||||||
|
|
||||||
|
nock('https://ombi.test')
|
||||||
|
.get('/api/v1/Request/movie')
|
||||||
|
.reply(200, []);
|
||||||
|
nock('https://ombi.test')
|
||||||
|
.get('/api/v1/Request/tv')
|
||||||
|
.reply(200, []);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
NotificationType: 'NewRequest',
|
||||||
|
RequestId: 126,
|
||||||
|
RequestedUser: { UserName: 'gordon_pascal' },
|
||||||
|
Title: 'Pascal Movie',
|
||||||
|
Type: 'Movie',
|
||||||
|
RequestStatus: 'Pending',
|
||||||
|
ApplicationUrl: 'https://ombi.test',
|
||||||
|
RequestedDate: '2026-05-23T20:33:00.000Z'
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await postOmbi(app, payload);
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.received).toBe(true);
|
||||||
|
expect(res.body.duplicate).toBeUndefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user