mirror of
https://github.com/Athou/commafeed.git
synced 2026-11-02 18:34:48 +00:00
Compare commits
70 Commits
7.4.0
...
renovate/r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e382ed73e | ||
|
|
93edb47dca | ||
|
|
801680e373 | ||
|
|
5704e1e1fa | ||
|
|
a2a6720cd1 | ||
|
|
4f8cddeb54 | ||
|
|
90d0846bad | ||
|
|
698b2f4dfd | ||
|
|
0053c54ae4 | ||
|
|
16b6077496 | ||
|
|
edb4d5e178 | ||
|
|
4b091fb90b | ||
|
|
30c049d10a | ||
|
|
fa8a447787 | ||
|
|
20ec5d3eb9 | ||
|
|
796dc88c33 | ||
|
|
f979f5426f | ||
|
|
8f9be28002 | ||
|
|
89845681be | ||
|
|
8fdf63dd73 | ||
|
|
8f07454270 | ||
|
|
946d82d8ee | ||
|
|
f8d625b651 | ||
|
|
eefdd40a02 | ||
|
|
08f2889f1d | ||
|
|
45453fcf4f | ||
|
|
ef7bab4841 | ||
|
|
ac277f9ca5 | ||
|
|
44dab0287b | ||
|
|
88b75ec0cb | ||
|
|
cbb45ce68c | ||
|
|
b0c4dbf533 | ||
|
|
1891b63a0e | ||
|
|
c420fec8ee | ||
|
|
2ee2a5dfbe | ||
|
|
a5b98f9ea2 | ||
|
|
baedab2960 | ||
|
|
004e18bbb8 | ||
|
|
72e364afe9 | ||
|
|
86ff2dfcb2 | ||
|
|
69ce73012c | ||
|
|
f3bd1a2e11 | ||
|
|
a819283661 | ||
|
|
8255c1e929 | ||
|
|
eb7990f8dc | ||
|
|
982476b2d0 | ||
|
|
3ec653c3b7 | ||
|
|
5dc5b3795e | ||
|
|
f5f77a80a3 | ||
|
|
1d1099492e | ||
|
|
ccc2564929 | ||
|
|
084574ffd4 | ||
|
|
6a6978c491 | ||
|
|
0399375288 | ||
|
|
394346fbc1 | ||
|
|
0feec109bd | ||
|
|
fdb5c9b536 | ||
|
|
f7ad8aed60 | ||
|
|
4621d7e79f | ||
|
|
b64a49cfb0 | ||
|
|
aaddfbaf40 | ||
|
|
994da5e1ee | ||
|
|
811e282e28 | ||
|
|
892fcf8168 | ||
|
|
032ee9243f | ||
|
|
30cdf34d56 | ||
|
|
fd52caa3fd | ||
|
|
9788712808 | ||
|
|
af9d086f05 | ||
|
|
fc7190868e |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ "ubuntu-latest", "ubuntu-22.04-arm", "windows-latest" ]
|
os: [ "ubuntu-24.04", "ubuntu-22.04-arm", "windows-latest" ]
|
||||||
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
database: [ "h2", "postgresql", "mysql", "mariadb" ]
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
@@ -29,10 +29,10 @@ jobs:
|
|||||||
|
|
||||||
# Setup
|
# Setup
|
||||||
- name: Set up GraalVM
|
- name: Set up GraalVM
|
||||||
uses: graalvm/setup-graalvm@27b360dbb857524eaf0b4c4cef364d38e5b4f173 # v1.6.7
|
uses: graalvm/setup-graalvm@f70bc8032f90a104502824a54bcb94b6f01715bc # v1.6.8
|
||||||
with:
|
with:
|
||||||
java-version: ${{ env.JAVA_VERSION }}
|
java-version: ${{ env.JAVA_VERSION }}
|
||||||
distribution: "graalvm"
|
distribution: "mandrel"
|
||||||
cache: "maven"
|
cache: "maven"
|
||||||
|
|
||||||
- name: Set up NodeJS cache
|
- name: Set up NodeJS cache
|
||||||
@@ -74,7 +74,7 @@ jobs:
|
|||||||
# Upload artifacts
|
# Upload artifacts
|
||||||
- name: Upload cross-platform app
|
- name: Upload cross-platform app
|
||||||
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
if: matrix.os == 'ubuntu-latest' # we only need to upload the cross-platform artifact once per database
|
if: matrix.os == 'ubuntu-24.04' # we only need to upload the cross-platform artifact once per database
|
||||||
with:
|
with:
|
||||||
name: commafeed-${{ matrix.database }}-jvm
|
name: commafeed-${{ matrix.database }}-jvm
|
||||||
path: commafeed-server/target/commafeed-*.zip
|
path: commafeed-server/target/commafeed-*.zip
|
||||||
@@ -87,7 +87,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload pages
|
- name: Upload pages
|
||||||
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||||
if: matrix.os == 'ubuntu-latest' && matrix.database == 'h2' # we only need to upload the pages once
|
if: matrix.os == 'ubuntu-24.04' && matrix.database == 'h2' # we only need to upload the pages once
|
||||||
with:
|
with:
|
||||||
path: target/pages
|
path: target/pages
|
||||||
|
|
||||||
@@ -141,7 +141,7 @@ jobs:
|
|||||||
|
|
||||||
# Docker
|
# Docker
|
||||||
- name: Login to Container Registry
|
- name: Login to Container Registry
|
||||||
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
|
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
|
||||||
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
if: ${{ env.DOCKERHUB_USERNAME != '' }}
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## [7.4.1]
|
||||||
|
|
||||||
|
- Fix entries being marked as read when scrolling back to an entry that was explicitly marked as unread (#2337)
|
||||||
|
- Add a global failsafe timeout to the new HTTP/2 client to prevent it from hanging indefinitely
|
||||||
|
- Add OCI source label to container images (#2335)
|
||||||
|
|
||||||
## [7.4.0]
|
## [7.4.0]
|
||||||
|
|
||||||
- Add support for HTTP/2 when fetching feeds (#2317)
|
- Add support for HTTP/2 when fetching feeds (#2317)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.5.15/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.6.4/schema.json",
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"indentStyle": "space",
|
"indentStyle": "space",
|
||||||
"indentWidth": 4,
|
"indentWidth": 4,
|
||||||
|
|||||||
1073
commafeed-client/package-lock.json
generated
1073
commafeed-client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -17,20 +17,20 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "11.14.0",
|
"@emotion/react": "11.14.0",
|
||||||
"@fontsource/open-sans": "5.3.0",
|
"@fontsource/open-sans": "5.3.0",
|
||||||
"@lingui/core": "6.9.1",
|
"@lingui/core": "6.11.0",
|
||||||
"@lingui/react": "6.9.1",
|
"@lingui/react": "6.11.0",
|
||||||
"@mantine/core": "9.7.2",
|
"@mantine/core": "9.7.4",
|
||||||
"@mantine/dates": "9.7.2",
|
"@mantine/dates": "9.7.4",
|
||||||
"@mantine/form": "9.7.2",
|
"@mantine/form": "9.7.4",
|
||||||
"@mantine/hooks": "9.7.2",
|
"@mantine/hooks": "9.7.4",
|
||||||
"@mantine/modals": "9.7.2",
|
"@mantine/modals": "9.7.4",
|
||||||
"@mantine/notifications": "9.7.2",
|
"@mantine/notifications": "9.7.4",
|
||||||
"@mantine/spotlight": "9.7.2",
|
"@mantine/spotlight": "9.7.4",
|
||||||
"@monaco-editor/react": "4.7.0",
|
"@monaco-editor/react": "4.7.0",
|
||||||
"@react-querybuilder/mantine": "8.25.0",
|
"@react-querybuilder/mantine": "8.26.0",
|
||||||
"@reduxjs/toolkit": "2.13.0",
|
"@reduxjs/toolkit": "2.13.0",
|
||||||
"@rolldown/plugin-babel": "0.2.4",
|
"@rolldown/plugin-babel": "0.3.0",
|
||||||
"axios": "1.20.0",
|
"axios": "1.21.0",
|
||||||
"dayjs": "1.11.23",
|
"dayjs": "1.11.23",
|
||||||
"escape-string-regexp": "5.0.0",
|
"escape-string-regexp": "5.0.0",
|
||||||
"interweave": "13.1.1",
|
"interweave": "13.1.1",
|
||||||
@@ -43,9 +43,9 @@
|
|||||||
"react-draggable": "4.7.2",
|
"react-draggable": "4.7.2",
|
||||||
"react-icons": "5.7.0",
|
"react-icons": "5.7.0",
|
||||||
"react-infinite-scroller": "1.2.6",
|
"react-infinite-scroller": "1.2.6",
|
||||||
"react-querybuilder": "8.25.0",
|
"react-querybuilder": "8.26.0",
|
||||||
"react-redux": "9.3.0",
|
"react-redux": "9.3.1",
|
||||||
"react-router-dom": "7.18.4",
|
"react-router-dom": "7.18.5",
|
||||||
"react-swipeable": "7.0.2",
|
"react-swipeable": "7.0.2",
|
||||||
"style-to-object": "2.0.2",
|
"style-to-object": "2.0.2",
|
||||||
"throttle-debounce": "5.0.2",
|
"throttle-debounce": "5.0.2",
|
||||||
@@ -54,10 +54,9 @@
|
|||||||
"websocket-heartbeat-js": "1.1.3"
|
"websocket-heartbeat-js": "1.1.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/types": "^8.0.6",
|
"@biomejs/biome": "2.6.4",
|
||||||
"@biomejs/biome": "2.5.15",
|
"@lingui/cli": "6.11.0",
|
||||||
"@lingui/cli": "6.9.1",
|
"@lingui/vite-plugin": "6.11.0",
|
||||||
"@lingui/vite-plugin": "6.9.1",
|
|
||||||
"@testing-library/jest-dom": "7.0.1",
|
"@testing-library/jest-dom": "7.0.1",
|
||||||
"@testing-library/react": "16.3.3",
|
"@testing-library/react": "16.3.3",
|
||||||
"@testing-library/user-event": "14.6.8",
|
"@testing-library/user-event": "14.6.8",
|
||||||
@@ -71,7 +70,7 @@
|
|||||||
"babel-plugin-react-compiler": "^1.0.0",
|
"babel-plugin-react-compiler": "^1.0.0",
|
||||||
"jsdom": "30.1.3",
|
"jsdom": "30.1.3",
|
||||||
"typescript": "7.0.2",
|
"typescript": "7.0.2",
|
||||||
"vite": "8.3.4",
|
"vite": "8.3.5",
|
||||||
"vite-plugin-checker": "0.14.5",
|
"vite-plugin-checker": "0.14.5",
|
||||||
"vitest": "5.0.5",
|
"vitest": "5.0.5",
|
||||||
"yaml": "2.9.1"
|
"yaml": "2.9.1"
|
||||||
|
|||||||
@@ -6,16 +6,16 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>7.4.0</version>
|
<version>7.4.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<name>CommaFeed Client</name>
|
<name>CommaFeed Client</name>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- renovate: datasource=node-version depName=node -->
|
<!-- renovate: datasource=node-version depName=node -->
|
||||||
<node.version>v24.22.0</node.version>
|
<node.version>v26.13.0</node.version>
|
||||||
<!-- renovate: datasource=npm depName=npm -->
|
<!-- renovate: datasource=npm depName=npm -->
|
||||||
<npm.version>12.2.0</npm.version>
|
<npm.version>12.2.1</npm.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
@@ -116,6 +116,30 @@ describe("entries", () => {
|
|||||||
expect(client.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
expect(client.entry.mark).toHaveBeenCalledWith({ id: "3", read: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("flags an entry marked as unread by the user until entries are reloaded", async () => {
|
||||||
|
vi.mocked(client.feed.getEntries).mockResolvedValue({
|
||||||
|
data: {
|
||||||
|
entries: [{ id: "3", read: true, markable: true } as Entry],
|
||||||
|
hasMore: false,
|
||||||
|
name: "my-feed",
|
||||||
|
errorCount: 0,
|
||||||
|
feedLink: "https://mysite.com/feed",
|
||||||
|
timestamp: 123,
|
||||||
|
ignoredReadStatus: false,
|
||||||
|
},
|
||||||
|
} as AxiosResponse<Entries>)
|
||||||
|
|
||||||
|
const store = configureStore({ reducer: reducers })
|
||||||
|
await store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: true }))
|
||||||
|
|
||||||
|
store.dispatch(markEntry({ entry: store.getState().entries.entries[0], read: false }))
|
||||||
|
expect(store.getState().entries.entries[0].keepUnread).toBe(true)
|
||||||
|
|
||||||
|
// reloading the entry list resets the flag
|
||||||
|
await store.dispatch(loadEntries({ source: { type: "feed", id: "feed-id" }, clearSearch: false }))
|
||||||
|
expect(store.getState().entries.entries[0].keepUnread).toBeUndefined()
|
||||||
|
})
|
||||||
|
|
||||||
it("marks all entries as read", () => {
|
it("marks all entries as read", () => {
|
||||||
const store = configureStore({
|
const store = configureStore({
|
||||||
reducer: reducers,
|
reducer: reducers,
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ export interface EntrySource {
|
|||||||
id: string
|
id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ExpendableEntry = Entry & { expanded?: boolean }
|
export type ExpendableEntry = Entry & {
|
||||||
|
expanded?: boolean
|
||||||
|
/** set when the user explicitly marks the entry as unread, prevents it from being marked as read again by scrolling in expanded mode until the entry list is reloaded */
|
||||||
|
keepUnread?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
interface EntriesState {
|
interface EntriesState {
|
||||||
/** selected source */
|
/** selected source */
|
||||||
@@ -71,11 +75,13 @@ export const entriesSlice = createSlice({
|
|||||||
builder.addCase(markEntry.pending, (state, action) => {
|
builder.addCase(markEntry.pending, (state, action) => {
|
||||||
for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) {
|
for (const e of state.entries.filter(e => e.id === action.meta.arg.entry.id)) {
|
||||||
e.read = action.meta.arg.read
|
e.read = action.meta.arg.read
|
||||||
|
if (!action.meta.arg.read) e.keepUnread = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
builder.addCase(markMultipleEntries.pending, (state, action) => {
|
builder.addCase(markMultipleEntries.pending, (state, action) => {
|
||||||
for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) {
|
for (const e of state.entries.filter(e => action.meta.arg.entries.some(e2 => e2.id === e.id))) {
|
||||||
e.read = action.meta.arg.read
|
e.read = action.meta.arg.read
|
||||||
|
if (!action.meta.arg.read) e.keepUnread = true
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
builder.addCase(markAllEntries.pending, (state, action) => {
|
builder.addCase(markAllEntries.pending, (state, action) => {
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export const loadMoreEntries = createAppAsyncThunk("entries/loadMore", async (_,
|
|||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
const { source } = state.entries
|
const { source } = state.entries
|
||||||
const offset =
|
const offset =
|
||||||
state.user.settings?.readingMode === "all" || (source.type === "category" && source.id === "starred")
|
state.user.settings?.readingMode === "all" || state.entries.search || (source.type === "category" && source.id === "starred")
|
||||||
? state.entries.entries.length
|
? state.entries.entries.length
|
||||||
: state.entries.entries.filter(e => !e.read).length
|
: state.entries.entries.filter(e => !e.read).length
|
||||||
const endpoint = getEndpoint(state.entries.source.type)
|
const endpoint = getEndpoint(state.entries.source.type)
|
||||||
@@ -137,6 +137,7 @@ export const markAllAsReadWithConfirmationIfRequired = createAppAsyncThunk(
|
|||||||
const state = thunkApi.getState()
|
const state = thunkApi.getState()
|
||||||
const source = state.entries.source
|
const source = state.entries.source
|
||||||
const entriesTimestamp = state.entries.timestamp ?? Date.now()
|
const entriesTimestamp = state.entries.timestamp ?? Date.now()
|
||||||
|
const search = state.entries.search
|
||||||
const markAllAsReadConfirmation = state.user.settings?.markAllAsReadConfirmation
|
const markAllAsReadConfirmation = state.user.settings?.markAllAsReadConfirmation
|
||||||
const markAllAsReadNavigateToNextUnread = state.user.settings?.markAllAsReadNavigateToNextUnread
|
const markAllAsReadNavigateToNextUnread = state.user.settings?.markAllAsReadNavigateToNextUnread
|
||||||
|
|
||||||
@@ -151,6 +152,7 @@ export const markAllAsReadWithConfirmationIfRequired = createAppAsyncThunk(
|
|||||||
read: true,
|
read: true,
|
||||||
olderThan: Date.now(),
|
olderThan: Date.now(),
|
||||||
insertedBefore: entriesTimestamp,
|
insertedBefore: entriesTimestamp,
|
||||||
|
keywords: search,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export function MarkAllAsReadConfirmationDialog() {
|
|||||||
const source = useAppSelector(state => state.entries.source)
|
const source = useAppSelector(state => state.entries.source)
|
||||||
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
const sourceLabel = useAppSelector(state => state.entries.sourceLabel)
|
||||||
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
|
const entriesTimestamp = useAppSelector(state => state.entries.timestamp) ?? Date.now()
|
||||||
|
const search = useAppSelector(state => state.entries.search)
|
||||||
const markAllAsReadNavigateToNextUnread = useAppSelector(state => state.user.settings?.markAllAsReadNavigateToNextUnread)
|
const markAllAsReadNavigateToNextUnread = useAppSelector(state => state.user.settings?.markAllAsReadNavigateToNextUnread)
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
|
||||||
@@ -26,6 +27,7 @@ export function MarkAllAsReadConfirmationDialog() {
|
|||||||
read: true,
|
read: true,
|
||||||
olderThan: Date.now() - threshold * 24 * 60 * 60 * 1000,
|
olderThan: Date.now() - threshold * 24 * 60 * 60 * 1000,
|
||||||
insertedBefore: entriesTimestamp,
|
insertedBefore: entriesTimestamp,
|
||||||
|
keywords: search,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -111,12 +111,13 @@ export function FeedEntries() {
|
|||||||
const el = document.getElementById(Constants.dom.entryId(e))
|
const el = document.getElementById(Constants.dom.entryId(e))
|
||||||
return el && !Constants.layout.isTopVisible(el)
|
return el && !Constants.layout.isTopVisible(el)
|
||||||
})
|
})
|
||||||
if (currentEntry) {
|
if (currentEntry && currentEntry.id !== selectedEntryId) {
|
||||||
dispatch(
|
dispatch(
|
||||||
selectEntry({
|
selectEntry({
|
||||||
entry: currentEntry,
|
entry: currentEntry,
|
||||||
expand: false,
|
expand: false,
|
||||||
markAsRead: !!scrollMarks,
|
// don't mark as read entries that the user explicitly marked as unread
|
||||||
|
markAsRead: !!scrollMarks && !currentEntry.keepUnread,
|
||||||
scrollToEntry: false,
|
scrollToEntry: false,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -124,7 +125,7 @@ export function FeedEntries() {
|
|||||||
})
|
})
|
||||||
window.addEventListener("scroll", listener)
|
window.addEventListener("scroll", listener)
|
||||||
return () => window.removeEventListener("scroll", listener)
|
return () => window.removeEventListener("scroll", listener)
|
||||||
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry])
|
}, [dispatch, entries, viewMode, scrollMarks, scrollingToEntry, selectedEntryId])
|
||||||
|
|
||||||
useMousetrap("r", async () => await dispatch(reloadEntries()))
|
useMousetrap("r", async () => await dispatch(reloadEntries()))
|
||||||
useMousetrap(
|
useMousetrap(
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ export function CategoryDetailsPage() {
|
|||||||
category = query.result && flattenCategoryTree(query.result.data).find(c => c.id === id)
|
category = query.result && flattenCategoryTree(query.result.data).find(c => c.id === id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// a category cannot be moved under itself or one of its own descendants
|
||||||
|
const excludedCategoryIds = category ? flattenCategoryTree(category).map(c => c.id) : [id]
|
||||||
|
|
||||||
const form = useForm<CategoryModificationRequest>()
|
const form = useForm<CategoryModificationRequest>()
|
||||||
const { setValues } = form
|
const { setValues } = form
|
||||||
|
|
||||||
@@ -125,7 +128,7 @@ export function CategoryDetailsPage() {
|
|||||||
label={<Trans>Parent Category</Trans>}
|
label={<Trans>Parent Category</Trans>}
|
||||||
{...form.getInputProps("parentId")}
|
{...form.getInputProps("parentId")}
|
||||||
clearable
|
clearable
|
||||||
withoutCategoryIds={[id]}
|
withoutCategoryIds={excludedCategoryIds}
|
||||||
/>
|
/>
|
||||||
<NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} />
|
<NumberInput label={<Trans>Position</Trans>} {...form.getInputProps("position")} required min={0} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { Trans } from "@lingui/react/macro"
|
|||||||
import { ActionIcon, Box, Center, Divider, Group, Title, useMantineTheme } from "@mantine/core"
|
import { ActionIcon, Box, Center, Divider, Group, Title, useMantineTheme } from "@mantine/core"
|
||||||
import { useViewportSize } from "@mantine/hooks"
|
import { useViewportSize } from "@mantine/hooks"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { TbEdit } from "react-icons/tb"
|
import { TbSettings } from "react-icons/tb"
|
||||||
import { useLocation, useParams } from "react-router-dom"
|
import { useLocation, useParams } from "react-router-dom"
|
||||||
import { Constants } from "@/app/constants"
|
import { Constants } from "@/app/constants"
|
||||||
import type { EntrySourceType } from "@/app/entries/slice"
|
import type { EntrySourceType } from "@/app/entries/slice"
|
||||||
@@ -138,7 +138,7 @@ export function FeedEntriesPage(props: Readonly<FeedEntriesPageProps>) {
|
|||||||
</Title>
|
</Title>
|
||||||
)}
|
)}
|
||||||
<ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}>
|
<ActionIcon onClick={titleClicked} variant="subtle" color={theme.primaryColor}>
|
||||||
<TbEdit size={18} />
|
<TbSettings size={18} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
{showUnreadCount && <UnreadCount unreadCount={unreadCount} showIndicator={hasNewEntries} />}
|
{showUnreadCount && <UnreadCount unreadCount={unreadCount} showIndicator={hasNewEntries} />}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
@@ -6,18 +6,18 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>7.4.0</version>
|
<version>7.4.1</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>commafeed-server</artifactId>
|
<artifactId>commafeed-server</artifactId>
|
||||||
<name>CommaFeed Server</name>
|
<name>CommaFeed Server</name>
|
||||||
<packaging>quarkus</packaging>
|
<packaging>quarkus</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<quarkus.version>3.40.1</quarkus.version>
|
<quarkus.version>3.40.1.1</quarkus.version>
|
||||||
<querydsl.version>7.7</querydsl.version>
|
<querydsl.version>7.7</querydsl.version>
|
||||||
<rome.version>2.1.0</rome.version>
|
<rome.version>2.1.0</rome.version>
|
||||||
<!-- renovate: depName=com.google.googlejavaformat:google-java-format -->
|
<!-- renovate: depName=com.google.googlejavaformat:google-java-format -->
|
||||||
<google-java-format.version>1.36.1</google-java-format.version>
|
<google-java-format.version>2.0.0</google-java-format.version>
|
||||||
|
|
||||||
<build.database>h2</build.database>
|
<build.database>h2</build.database>
|
||||||
</properties>
|
</properties>
|
||||||
@@ -164,7 +164,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-surefire-plugin</artifactId>
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
<version>3.6.0</version>
|
<version>3.6.1</version>
|
||||||
<configuration>
|
<configuration>
|
||||||
<argLine>@{argLine}</argLine>
|
<argLine>@{argLine}</argLine>
|
||||||
<systemPropertyVariables>
|
<systemPropertyVariables>
|
||||||
@@ -176,7 +176,7 @@
|
|||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.apache.maven.plugins</groupId>
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
<artifactId>maven-failsafe-plugin</artifactId>
|
<artifactId>maven-failsafe-plugin</artifactId>
|
||||||
<version>3.6.0</version>
|
<version>3.6.1</version>
|
||||||
<executions>
|
<executions>
|
||||||
<execution>
|
<execution>
|
||||||
<goals>
|
<goals>
|
||||||
@@ -245,7 +245,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.puppycrawl.tools</groupId>
|
<groupId>com.puppycrawl.tools</groupId>
|
||||||
<artifactId>checkstyle</artifactId>
|
<artifactId>checkstyle</artifactId>
|
||||||
<version>14.3.0</version>
|
<version>14.4.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<executions>
|
<executions>
|
||||||
@@ -302,7 +302,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed-client</artifactId>
|
<artifactId>commafeed-client</artifactId>
|
||||||
<version>7.4.0</version>
|
<version>7.4.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- compile-time processors -->
|
<!-- compile-time processors -->
|
||||||
@@ -404,7 +404,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>dev.cel</groupId>
|
<groupId>dev.cel</groupId>
|
||||||
<artifactId>cel</artifactId>
|
<artifactId>cel</artifactId>
|
||||||
<version>0.14.0</version>
|
<version>0.15.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.passay</groupId>
|
<groupId>org.passay</groupId>
|
||||||
@@ -439,7 +439,7 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.ibm.icu</groupId>
|
<groupId>com.ibm.icu</groupId>
|
||||||
<artifactId>icu4j</artifactId>
|
<artifactId>icu4j</artifactId>
|
||||||
<version>78.3</version>
|
<version>79.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>net.sourceforge.cssparser</groupId>
|
<groupId>net.sourceforge.cssparser</groupId>
|
||||||
@@ -481,8 +481,8 @@
|
|||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.mock-server</groupId>
|
<groupId>org.mock-server</groupId>
|
||||||
<artifactId>mockserver-junit-jupiter</artifactId>
|
<artifactId>mockserver-junit-jupiter-no-dependencies</artifactId>
|
||||||
<version>7.6.0</version>
|
<version>9.2.0</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
FROM ibm-semeru-runtimes:open-jdk-25.0.4.15-jre@sha256:630c738228597b4cd31d9823e479384729b03684bac23478b33fdbda13ae31b8
|
FROM ibm-semeru-runtimes:open-jdk-25.0.4.15-jre@sha256:c4f8cd5ee9d607addbf957a5066144c46c853c6a81235819f6952e60237904ca
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/Athou/commafeed"
|
||||||
|
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|
||||||
RUN mkdir -p /commafeed/data
|
RUN mkdir -p /commafeed/data
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
FROM debian:13.7@sha256:913f6706df59a68922d1dd08f78c2476560a8d367897200a6005b00e5f67c2d5
|
FROM debian:13.7@sha256:2dc419c6d7fe3dad0a2f9fd469c34cc90b0f9ba642d8fde837fe3d7d34a0e7f1
|
||||||
|
LABEL org.opencontainers.image.source="https://github.com/Athou/commafeed"
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
|
|
||||||
EXPOSE 8082
|
EXPOSE 8082
|
||||||
|
|||||||
@@ -66,11 +66,10 @@ public class HttpClientFactory {
|
|||||||
String userAgent =
|
String userAgent =
|
||||||
config.httpClient()
|
config.httpClient()
|
||||||
.userAgent()
|
.userAgent()
|
||||||
.orElseGet(
|
.orElseGet(() ->
|
||||||
() ->
|
String.format(
|
||||||
String.format(
|
"CommaFeed/%s (https://github.com/Athou/commafeed)",
|
||||||
"CommaFeed/%s (https://github.com/Athou/commafeed)",
|
version.getVersion()));
|
||||||
version.getVersion()));
|
|
||||||
return newClient(config, connectionManager, userAgent);
|
return newClient(config, connectionManager, userAgent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ import org.apache.hc.core5.http.nio.AsyncRequestProducer;
|
|||||||
import org.apache.hc.core5.http.nio.AsyncResponseConsumer;
|
import org.apache.hc.core5.http.nio.AsyncResponseConsumer;
|
||||||
import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityConsumer;
|
import org.apache.hc.core5.http.nio.entity.AbstractBinAsyncEntityConsumer;
|
||||||
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
|
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
|
||||||
import org.apache.hc.core5.util.Timeout;
|
|
||||||
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
|
import org.jboss.resteasy.reactive.common.headers.CacheControlDelegate;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
@@ -54,6 +53,8 @@ import java.time.InstantSource;
|
|||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
/** Smart HTTP getter: handles gzip, ssl, last modified, and etag headers */
|
/** Smart HTTP getter: handles gzip, ssl, last modified, and etag headers */
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -162,7 +163,6 @@ public class HttpGetter {
|
|||||||
HttpClientContext context = HttpClientContext.create();
|
HttpClientContext context = HttpClientContext.create();
|
||||||
context.setRequestConfig(
|
context.setRequestConfig(
|
||||||
RequestConfig.custom()
|
RequestConfig.custom()
|
||||||
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
|
|
||||||
// causes issues with some feeds
|
// causes issues with some feeds
|
||||||
// see https://github.com/Athou/commafeed/issues/1572
|
// see https://github.com/Athou/commafeed/issues/1572
|
||||||
// and https://issues.apache.org/jira/browse/HTTPCLIENT-2344
|
// and https://issues.apache.org/jira/browse/HTTPCLIENT-2344
|
||||||
@@ -191,7 +191,8 @@ public class HttpGetter {
|
|||||||
client.execute(req, resp, context, callback);
|
client.execute(req, resp, context, callback);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Message<org.apache.hc.core5.http.HttpResponse, byte[]> message = future.get();
|
Message<org.apache.hc.core5.http.HttpResponse, byte[]> message =
|
||||||
|
future.get(config.httpClient().responseTimeout().toSeconds(), TimeUnit.SECONDS);
|
||||||
|
|
||||||
byte[] content = message.getBody();
|
byte[] content = message.getBody();
|
||||||
int code = message.getHead().getCode();
|
int code = message.getHead().getCode();
|
||||||
@@ -245,7 +246,11 @@ public class HttpGetter {
|
|||||||
future.cancel(true);
|
future.cancel(true);
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new RuntimeException("interrupted while fetching " + request.getUrl(), e);
|
throw new RuntimeException("interrupted while fetching " + request.getUrl(), e);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new IOException("timed out while fetching " + request.getUrl(), e);
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
|
future.cancel(true);
|
||||||
throw Lombok.sneakyThrow(e.getCause());
|
throw Lombok.sneakyThrow(e.getCause());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -278,9 +283,8 @@ public class HttpGetter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return CacheBuilder.newBuilder()
|
return CacheBuilder.newBuilder()
|
||||||
.weigher(
|
.weigher((HttpRequest key, HttpResponse value) ->
|
||||||
(HttpRequest key, HttpResponse value) ->
|
value.content() != null ? value.content().length : 0)
|
||||||
value.content() != null ? value.content().length : 0)
|
|
||||||
.maximumWeight(cacheConfig.maximumMemorySize().asLongValue())
|
.maximumWeight(cacheConfig.maximumMemorySize().asLongValue())
|
||||||
.expireAfterWrite(cacheConfig.expiration())
|
.expireAfterWrite(cacheConfig.expiration())
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
import com.commafeed.backend.model.QFeedCategory;
|
import com.commafeed.backend.model.QFeedCategory;
|
||||||
import com.commafeed.backend.model.QUser;
|
import com.commafeed.backend.model.QUser;
|
||||||
@@ -17,8 +18,8 @@ public class FeedCategoryDAO extends GenericDAO<FeedCategory> {
|
|||||||
|
|
||||||
private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
|
private static final QFeedCategory CATEGORY = QFeedCategory.feedCategory;
|
||||||
|
|
||||||
public FeedCategoryDAO(EntityManager entityManager) {
|
public FeedCategoryDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, FeedCategory.class);
|
super(entityManager, FeedCategory.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedCategory> findAll(User user) {
|
public List<FeedCategory> findAll(User user) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.QFeed;
|
import com.commafeed.backend.model.QFeed;
|
||||||
import com.commafeed.backend.model.QFeedSubscription;
|
import com.commafeed.backend.model.QFeedSubscription;
|
||||||
@@ -20,8 +21,8 @@ public class FeedDAO extends GenericDAO<Feed> {
|
|||||||
private static final QFeed FEED = QFeed.feed;
|
private static final QFeed FEED = QFeed.feed;
|
||||||
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
|
private static final QFeedSubscription SUBSCRIPTION = QFeedSubscription.feedSubscription;
|
||||||
|
|
||||||
public FeedDAO(EntityManager entityManager) {
|
public FeedDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, Feed.class);
|
super(entityManager, Feed.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Feed> findByIds(List<Long> id) {
|
public List<Feed> findByIds(List<Long> id) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.FeedEntryContent;
|
import com.commafeed.backend.model.FeedEntryContent;
|
||||||
import com.commafeed.backend.model.QFeedEntry;
|
import com.commafeed.backend.model.QFeedEntry;
|
||||||
import com.commafeed.backend.model.QFeedEntryContent;
|
import com.commafeed.backend.model.QFeedEntryContent;
|
||||||
@@ -15,8 +16,8 @@ public class FeedEntryContentDAO extends GenericDAO<FeedEntryContent> {
|
|||||||
private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
|
private static final QFeedEntryContent CONTENT = QFeedEntryContent.feedEntryContent;
|
||||||
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
||||||
|
|
||||||
public FeedEntryContentDAO(EntityManager entityManager) {
|
public FeedEntryContentDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, FeedEntryContent.class);
|
super(entityManager, FeedEntryContent.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
|
public List<FeedEntryContent> findExisting(String contentHash, String titleHash) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.QFeedEntry;
|
import com.commafeed.backend.model.QFeedEntry;
|
||||||
@@ -23,8 +24,8 @@ public class FeedEntryDAO extends GenericDAO<FeedEntry> {
|
|||||||
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
private static final QFeedEntry ENTRY = QFeedEntry.feedEntry;
|
||||||
private static final int IN_CLAUSE_BATCH_SIZE = 1000;
|
private static final int IN_CLAUSE_BATCH_SIZE = 1000;
|
||||||
|
|
||||||
public FeedEntryDAO(EntityManager entityManager) {
|
public FeedEntryDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, FeedEntry.class);
|
super(entityManager, FeedEntry.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public FeedEntry findExisting(String guidHash, Feed feed) {
|
public FeedEntry findExisting(String guidHash, Feed feed) {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import org.apache.commons.collections4.CollectionUtils;
|
|||||||
|
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -50,7 +51,7 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
EntityManager entityManager,
|
EntityManager entityManager,
|
||||||
FeedEntryTagDAO feedEntryTagDAO,
|
FeedEntryTagDAO feedEntryTagDAO,
|
||||||
CommaFeedConfiguration config) {
|
CommaFeedConfiguration config) {
|
||||||
super(entityManager, FeedEntryStatus.class);
|
super(entityManager, FeedEntryStatus.class, config);
|
||||||
this.feedEntryTagDAO = feedEntryTagDAO;
|
this.feedEntryTagDAO = feedEntryTagDAO;
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
@@ -129,8 +130,6 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
query.limit(limit);
|
query.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(query, config.database().queryTimeout());
|
|
||||||
|
|
||||||
List<FeedEntryStatus> statuses = query.fetch();
|
List<FeedEntryStatus> statuses = query.fetch();
|
||||||
statuses.forEach(s -> s.setMarkable(true));
|
statuses.forEach(s -> s.setMarkable(true));
|
||||||
if (includeContent) {
|
if (includeContent) {
|
||||||
@@ -207,8 +206,6 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
query.limit(limit);
|
query.limit(limit);
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(query, config.database().queryTimeout());
|
|
||||||
|
|
||||||
List<FeedEntryStatus> statuses = new ArrayList<>();
|
List<FeedEntryStatus> statuses = new ArrayList<>();
|
||||||
List<Tuple> tuples = query.fetch();
|
List<Tuple> tuples = query.fetch();
|
||||||
for (Tuple tuple : tuples) {
|
for (Tuple tuple : tuples) {
|
||||||
@@ -238,19 +235,41 @@ public class FeedEntryStatusDAO extends GenericDAO<FeedEntryStatus> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public UnreadCount getUnreadCount(FeedSubscription sub) {
|
public Map<Long, UnreadCount> getUnreadCounts(List<FeedSubscription> subs) {
|
||||||
|
if (subs.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<Long, List<FeedSubscription>> subsByFeedId =
|
||||||
|
subs.stream().collect(Collectors.groupingBy(s -> s.getFeed().getId()));
|
||||||
|
|
||||||
JPAQuery<Tuple> query =
|
JPAQuery<Tuple> query =
|
||||||
query().select(ENTRY.count(), ENTRY.published.max())
|
query().select(ENTRY.feed.id, ENTRY.count(), ENTRY.published.max())
|
||||||
.from(ENTRY)
|
.from(ENTRY)
|
||||||
.leftJoin(ENTRY.statuses, STATUS)
|
.leftJoin(ENTRY.statuses, STATUS)
|
||||||
.on(STATUS.subscription.eq(sub))
|
.on(STATUS.subscription.in(subs))
|
||||||
.where(ENTRY.feed.eq(sub.getFeed()))
|
.where(ENTRY.feed.id.in(subsByFeedId.keySet()))
|
||||||
.where(buildUnreadPredicate());
|
.where(buildUnreadPredicate())
|
||||||
|
.groupBy(ENTRY.feed.id);
|
||||||
|
|
||||||
Tuple tuple = query.fetchOne();
|
Map<Long, UnreadCount> result = new HashMap<>();
|
||||||
Long count = tuple.get(ENTRY.count());
|
for (Tuple tuple : query.fetch()) {
|
||||||
Instant published = tuple.get(ENTRY.published.max());
|
Long feedId = tuple.get(ENTRY.feed.id);
|
||||||
return new UnreadCount(sub.getId(), count == null ? 0 : count, published);
|
Long count = tuple.get(ENTRY.count());
|
||||||
|
Instant published = tuple.get(ENTRY.published.max());
|
||||||
|
for (FeedSubscription sub : subsByFeedId.get(feedId)) {
|
||||||
|
result.put(
|
||||||
|
sub.getId(),
|
||||||
|
new UnreadCount(sub.getId(), count == null ? 0 : count, published));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// feeds with no unread entries are absent from the query result, default them to zero
|
||||||
|
for (FeedSubscription sub : subs) {
|
||||||
|
result.putIfAbsent(sub.getId(), new UnreadCount(sub.getId(), 0, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private BooleanBuilder buildUnreadPredicate() {
|
private BooleanBuilder buildUnreadPredicate() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.FeedEntry;
|
import com.commafeed.backend.model.FeedEntry;
|
||||||
import com.commafeed.backend.model.FeedEntryTag;
|
import com.commafeed.backend.model.FeedEntryTag;
|
||||||
import com.commafeed.backend.model.QFeedEntryTag;
|
import com.commafeed.backend.model.QFeedEntryTag;
|
||||||
@@ -17,8 +18,8 @@ public class FeedEntryTagDAO extends GenericDAO<FeedEntryTag> {
|
|||||||
|
|
||||||
private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
|
private static final QFeedEntryTag TAG = QFeedEntryTag.feedEntryTag;
|
||||||
|
|
||||||
public FeedEntryTagDAO(EntityManager entityManager) {
|
public FeedEntryTagDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, FeedEntryTag.class);
|
super(entityManager, FeedEntryTag.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<String> findByUser(User user) {
|
public List<String> findByUser(User user) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.AbstractModel;
|
import com.commafeed.backend.model.AbstractModel;
|
||||||
import com.commafeed.backend.model.Feed;
|
import com.commafeed.backend.model.Feed;
|
||||||
import com.commafeed.backend.model.FeedCategory;
|
import com.commafeed.backend.model.FeedCategory;
|
||||||
@@ -31,8 +32,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
|||||||
|
|
||||||
private final EntityManager entityManager;
|
private final EntityManager entityManager;
|
||||||
|
|
||||||
public FeedSubscriptionDAO(EntityManager entityManager) {
|
public FeedSubscriptionDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, FeedSubscription.class);
|
super(entityManager, FeedSubscription.class, config);
|
||||||
this.entityManager = entityManager;
|
this.entityManager = entityManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,10 +125,8 @@ public class FeedSubscriptionDAO extends GenericDAO<FeedSubscription> {
|
|||||||
Set<Long> categoryIds =
|
Set<Long> categoryIds =
|
||||||
categories.stream().map(AbstractModel::getId).collect(Collectors.toSet());
|
categories.stream().map(AbstractModel::getId).collect(Collectors.toSet());
|
||||||
return findAll(user).stream()
|
return findAll(user).stream()
|
||||||
.filter(
|
.filter(s ->
|
||||||
s ->
|
s.getCategory() != null && categoryIds.contains(s.getCategory().getId()))
|
||||||
s.getCategory() != null
|
|
||||||
&& categoryIds.contains(s.getCategory().getId()))
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.AbstractModel;
|
import com.commafeed.backend.model.AbstractModel;
|
||||||
import com.querydsl.core.types.EntityPath;
|
import com.querydsl.core.types.EntityPath;
|
||||||
import com.querydsl.jpa.impl.JPADeleteClause;
|
import com.querydsl.jpa.impl.JPADeleteClause;
|
||||||
@@ -21,9 +22,15 @@ public abstract class GenericDAO<T extends AbstractModel> {
|
|||||||
|
|
||||||
private final EntityManager entityManager;
|
private final EntityManager entityManager;
|
||||||
private final Class<T> entityClass;
|
private final Class<T> entityClass;
|
||||||
|
private final CommaFeedConfiguration config;
|
||||||
|
|
||||||
protected JPAQueryFactory query() {
|
protected JPAQueryFactory query() {
|
||||||
return new JPAQueryFactory(entityManager);
|
return new JPAQueryFactory(entityManager) {
|
||||||
|
@Override
|
||||||
|
public JPAQuery<?> query() {
|
||||||
|
return withTimeout(super.query(), config.database().queryTimeout());
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
protected JPAUpdateClause updateQuery(EntityPath<T> entityPath) {
|
protected JPAUpdateClause updateQuery(EntityPath<T> entityPath) {
|
||||||
@@ -57,9 +64,10 @@ public abstract class GenericDAO<T extends AbstractModel> {
|
|||||||
return objects.size();
|
return objects.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setTimeout(JPAQuery<?> query, Duration timeout) {
|
private JPAQuery<?> withTimeout(JPAQuery<?> query, Duration timeout) {
|
||||||
if (!timeout.isZero()) {
|
if (!timeout.isZero()) {
|
||||||
query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis()));
|
query.setHint(SpecHints.HINT_SPEC_QUERY_TIMEOUT, Math.toIntExact(timeout.toMillis()));
|
||||||
}
|
}
|
||||||
|
return query;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.QUser;
|
import com.commafeed.backend.model.QUser;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
|
|
||||||
@@ -11,8 +12,8 @@ public class UserDAO extends GenericDAO<User> {
|
|||||||
|
|
||||||
private static final QUser USER = QUser.user;
|
private static final QUser USER = QUser.user;
|
||||||
|
|
||||||
public UserDAO(EntityManager entityManager) {
|
public UserDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, User.class);
|
super(entityManager, User.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User findByName(String name) {
|
public User findByName(String name) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.QUserRole;
|
import com.commafeed.backend.model.QUserRole;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.model.UserRole;
|
import com.commafeed.backend.model.UserRole;
|
||||||
@@ -17,8 +18,8 @@ public class UserRoleDAO extends GenericDAO<UserRole> {
|
|||||||
|
|
||||||
private static final QUserRole ROLE = QUserRole.userRole;
|
private static final QUserRole ROLE = QUserRole.userRole;
|
||||||
|
|
||||||
public UserRoleDAO(EntityManager entityManager) {
|
public UserRoleDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, UserRole.class);
|
super(entityManager, UserRole.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<UserRole> findAll() {
|
public List<UserRole> findAll() {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.commafeed.backend.dao;
|
package com.commafeed.backend.dao;
|
||||||
|
|
||||||
|
import com.commafeed.CommaFeedConfiguration;
|
||||||
import com.commafeed.backend.model.QUserSettings;
|
import com.commafeed.backend.model.QUserSettings;
|
||||||
import com.commafeed.backend.model.User;
|
import com.commafeed.backend.model.User;
|
||||||
import com.commafeed.backend.model.UserSettings;
|
import com.commafeed.backend.model.UserSettings;
|
||||||
@@ -12,8 +13,8 @@ public class UserSettingsDAO extends GenericDAO<UserSettings> {
|
|||||||
|
|
||||||
private static final QUserSettings SETTINGS = QUserSettings.userSettings;
|
private static final QUserSettings SETTINGS = QUserSettings.userSettings;
|
||||||
|
|
||||||
public UserSettingsDAO(EntityManager entityManager) {
|
public UserSettingsDAO(EntityManager entityManager, CommaFeedConfiguration config) {
|
||||||
super(entityManager, UserSettings.class);
|
super(entityManager, UserSettings.class, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
public UserSettings findByUser(User user) {
|
public UserSettings findByUser(User user) {
|
||||||
|
|||||||
@@ -104,56 +104,52 @@ public class FeedRefreshEngine {
|
|||||||
|
|
||||||
private void startFeedProcessingLoop() {
|
private void startFeedProcessingLoop() {
|
||||||
// take a feed from the queue, process it, rince, repeat
|
// take a feed from the queue, process it, rince, repeat
|
||||||
feedProcessingLoopExecutor.submit(
|
feedProcessingLoopExecutor.submit(() -> {
|
||||||
() -> {
|
while (!feedProcessingLoopExecutor.isShutdown()) {
|
||||||
while (!feedProcessingLoopExecutor.isShutdown()) {
|
try {
|
||||||
try {
|
// take() is blocking until a feed is available from the queue
|
||||||
// take() is blocking until a feed is available from the queue
|
Feed feed = queue.take();
|
||||||
Feed feed = queue.take();
|
|
||||||
|
|
||||||
// send the feed to be processed
|
// send the feed to be processed
|
||||||
log.debug(
|
log.debug("got feed {} from the queue, send it for processing", feed.getId());
|
||||||
"got feed {} from the queue, send it for processing",
|
processFeedAsync(feed);
|
||||||
feed.getId());
|
|
||||||
processFeedAsync(feed);
|
|
||||||
|
|
||||||
// we removed a feed from the queue, try to refill it as it may now be
|
// we removed a feed from the queue, try to refill it as it may now be
|
||||||
// empty
|
// empty
|
||||||
if (queue.isEmpty()) {
|
if (queue.isEmpty()) {
|
||||||
log.debug("took the last feed from the queue, try to refill");
|
log.debug("took the last feed from the queue, try to refill");
|
||||||
refillQueueAsync();
|
refillQueueAsync();
|
||||||
}
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.debug("interrupted while waiting for a feed in the queue");
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
} catch (InterruptedException e) {
|
||||||
|
log.debug("interrupted while waiting for a feed in the queue");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void startRefillLoop() {
|
private void startRefillLoop() {
|
||||||
// refill the queue at regular intervals if it's empty
|
// refill the queue at regular intervals if it's empty
|
||||||
refillLoopExecutor.submit(
|
refillLoopExecutor.submit(() -> {
|
||||||
() -> {
|
while (!refillLoopExecutor.isShutdown()) {
|
||||||
while (!refillLoopExecutor.isShutdown()) {
|
try {
|
||||||
try {
|
if (queue.isEmpty()) {
|
||||||
if (queue.isEmpty()) {
|
log.debug("refilling queue");
|
||||||
log.debug("refilling queue");
|
refillQueueAsync();
|
||||||
refillQueueAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
log.debug("sleeping for 15s");
|
|
||||||
TimeUnit.SECONDS.sleep(15);
|
|
||||||
} catch (InterruptedException e) {
|
|
||||||
log.debug("interrupted while sleeping");
|
|
||||||
Thread.currentThread().interrupt();
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.error(e.getMessage(), e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
log.debug("sleeping for 15s");
|
||||||
|
TimeUnit.SECONDS.sleep(15);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
log.debug("interrupted while sleeping");
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void refreshImmediately(Feed feed) {
|
public void refreshImmediately(Feed feed) {
|
||||||
@@ -184,65 +180,53 @@ public class FeedRefreshEngine {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
refillExecutor)
|
refillExecutor)
|
||||||
.whenComplete(
|
.whenComplete((data, ex) -> {
|
||||||
(data, ex) -> {
|
if (ex != null) {
|
||||||
if (ex != null) {
|
log.error("error while refilling the queue", ex);
|
||||||
log.error("error while refilling the queue", ex);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void processFeedAsync(Feed feed) {
|
private void processFeedAsync(Feed feed) {
|
||||||
CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
|
CompletableFuture.supplyAsync(() -> worker.update(feed), workerExecutor)
|
||||||
.thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
|
.thenApplyAsync(r -> updater.update(r.feed(), r.entries()), databaseUpdaterExecutor)
|
||||||
.thenCompose(
|
.thenCompose(r -> {
|
||||||
r -> {
|
List<CompletableFuture<Void>> futures =
|
||||||
List<CompletableFuture<Void>> futures =
|
r.insertedUnreadEntriesBySubscription().entrySet().stream()
|
||||||
r.insertedUnreadEntriesBySubscription().entrySet().stream()
|
.map(e -> {
|
||||||
.map(
|
FeedSubscription sub = e.getKey();
|
||||||
e -> {
|
List<FeedEntry> entries = e.getValue();
|
||||||
FeedSubscription sub = e.getKey();
|
|
||||||
List<FeedEntry> entries = e.getValue();
|
|
||||||
|
|
||||||
notifier.notifyOverWebsocket(sub, entries);
|
notifier.notifyOverWebsocket(sub, entries);
|
||||||
return CompletableFuture.runAsync(
|
return CompletableFuture.runAsync(
|
||||||
() ->
|
() -> notifier.sendPushNotifications(sub, entries),
|
||||||
notifier
|
notifierExecutor);
|
||||||
.sendPushNotifications(
|
})
|
||||||
sub,
|
.toList();
|
||||||
entries),
|
return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new));
|
||||||
notifierExecutor);
|
})
|
||||||
})
|
.exceptionally(ex -> {
|
||||||
.toList();
|
log.error("error while processing feed {}", feed.getUrl(), ex);
|
||||||
return CompletableFuture.allOf(
|
return null;
|
||||||
futures.toArray(CompletableFuture[]::new));
|
});
|
||||||
})
|
|
||||||
.exceptionally(
|
|
||||||
ex -> {
|
|
||||||
log.error("error while processing feed {}", feed.getUrl(), ex);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Feed> getNextUpdatableFeeds(int max) {
|
private List<Feed> getNextUpdatableFeeds(int max) {
|
||||||
return unitOfWork.call(
|
return unitOfWork.call(() -> {
|
||||||
() -> {
|
Instant lastLoginThreshold =
|
||||||
Instant lastLoginThreshold =
|
config.feedRefresh().userInactivityPeriod().isZero()
|
||||||
config.feedRefresh().userInactivityPeriod().isZero()
|
? null
|
||||||
? null
|
: Instant.now().minus(config.feedRefresh().userInactivityPeriod());
|
||||||
: Instant.now()
|
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
|
||||||
.minus(config.feedRefresh().userInactivityPeriod());
|
if (!feeds.isEmpty()) {
|
||||||
List<Feed> feeds = feedDAO.findNextUpdatable(max, lastLoginThreshold);
|
// update disabledUntil to prevent feeds from being returned again by
|
||||||
if (!feeds.isEmpty()) {
|
// feedDAO.findNextUpdatable()
|
||||||
// update disabledUntil to prevent feeds from being returned again by
|
Instant nextUpdateDate = Instant.now().plus(config.feedRefresh().interval());
|
||||||
// feedDAO.findNextUpdatable()
|
feedDAO.setDisabledUntil(
|
||||||
Instant nextUpdateDate =
|
feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
|
||||||
Instant.now().plus(config.feedRefresh().interval());
|
}
|
||||||
feedDAO.setDisabledUntil(
|
return feeds;
|
||||||
feeds.stream().map(AbstractModel::getId).toList(), nextUpdateDate);
|
});
|
||||||
}
|
|
||||||
return feeds;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private int getBatchSize() {
|
private int getBatchSize() {
|
||||||
@@ -292,19 +276,18 @@ public class FeedRefreshEngine {
|
|||||||
ThreadPoolExecutor pool =
|
ThreadPoolExecutor pool =
|
||||||
new ThreadPoolExecutor(
|
new ThreadPoolExecutor(
|
||||||
threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
|
threads, threads, 0L, TimeUnit.MILLISECONDS, new SynchronousQueue<>());
|
||||||
pool.setRejectedExecutionHandler(
|
pool.setRejectedExecutionHandler((r, e) -> {
|
||||||
(r, e) -> {
|
if (e.isShutdown()) {
|
||||||
if (e.isShutdown()) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
e.getQueue().put(r);
|
e.getQueue().put(r);
|
||||||
} catch (InterruptedException ex) {
|
} catch (InterruptedException ex) {
|
||||||
log.debug("interrupted while waiting for a slot in the queue.", ex);
|
log.debug("interrupted while waiting for a slot in the queue.", ex);
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return pool;
|
return pool;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,24 +91,22 @@ public class FeedRefreshUpdater {
|
|||||||
if (locked1 && locked2) {
|
if (locked1 && locked2) {
|
||||||
processed = true;
|
processed = true;
|
||||||
insertedEntry =
|
insertedEntry =
|
||||||
unitOfWork.call(
|
unitOfWork.call(() -> {
|
||||||
() -> {
|
if (feedEntryService.find(feed, entry) != null) {
|
||||||
if (feedEntryService.find(feed, entry) != null) {
|
// entry already exists, nothing to do
|
||||||
// entry already exists, nothing to do
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
FeedEntry feedEntry = feedEntryService.create(feed, entry);
|
FeedEntry feedEntry = feedEntryService.create(feed, entry);
|
||||||
entryInserted.mark();
|
entryInserted.mark();
|
||||||
for (FeedSubscription sub : subscriptions) {
|
for (FeedSubscription sub : subscriptions) {
|
||||||
boolean unread =
|
boolean unread = feedEntryService.applyFilter(sub, feedEntry);
|
||||||
feedEntryService.applyFilter(sub, feedEntry);
|
if (unread) {
|
||||||
if (unread) {
|
subscriptionsForWhichEntryIsUnread.add(sub);
|
||||||
subscriptionsForWhichEntryIsUnread.add(sub);
|
}
|
||||||
}
|
}
|
||||||
}
|
return feedEntry;
|
||||||
return feedEntry;
|
});
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
log.error("lock timeout for {} - {}", feed.getUrl(), key1);
|
log.error("lock timeout for {} - {}", feed.getUrl(), key1);
|
||||||
}
|
}
|
||||||
@@ -147,14 +145,13 @@ public class FeedRefreshUpdater {
|
|||||||
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
AddEntryResult addEntryResult = addEntry(feed, entry, subscriptions);
|
||||||
processed &= addEntryResult.processed;
|
processed &= addEntryResult.processed;
|
||||||
inserted += addEntryResult.insertedEntry != null ? 1 : 0;
|
inserted += addEntryResult.insertedEntry != null ? 1 : 0;
|
||||||
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(
|
addEntryResult.subscriptionsForWhichEntryIsUnread.forEach(sub -> {
|
||||||
sub -> {
|
if (addEntryResult.insertedEntry != null) {
|
||||||
if (addEntryResult.insertedEntry != null) {
|
insertedUnreadEntriesBySubscription
|
||||||
insertedUnreadEntriesBySubscription
|
.computeIfAbsent(sub, k -> new ArrayList<>())
|
||||||
.computeIfAbsent(sub, k -> new ArrayList<>())
|
.add(addEntryResult.insertedEntry);
|
||||||
.add(addEntryResult.insertedEntry);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (inserted == 0) {
|
if (inserted == 0) {
|
||||||
|
|||||||
@@ -65,20 +65,17 @@ public class OPMLExporter {
|
|||||||
|
|
||||||
for (FeedCategory child :
|
for (FeedCategory child :
|
||||||
categories.stream()
|
categories.stream()
|
||||||
.filter(
|
.filter(c ->
|
||||||
c ->
|
c.getParent() != null && c.getParent().getId().equals(cat.getId()))
|
||||||
c.getParent() != null
|
|
||||||
&& c.getParent().getId().equals(cat.getId()))
|
|
||||||
.toList()) {
|
.toList()) {
|
||||||
outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions));
|
outline.getChildren().add(buildCategoryOutline(child, categories, subscriptions));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (FeedSubscription sub :
|
for (FeedSubscription sub :
|
||||||
subscriptions.stream()
|
subscriptions.stream()
|
||||||
.filter(
|
.filter(s ->
|
||||||
s ->
|
s.getCategory() != null
|
||||||
s.getCategory() != null
|
&& s.getCategory().getId().equals(cat.getId()))
|
||||||
&& s.getCategory().getId().equals(cat.getId()))
|
|
||||||
.toList()) {
|
.toList()) {
|
||||||
outline.getChildren().add(buildSubscriptionOutline(sub));
|
outline.getChildren().add(buildSubscriptionOutline(sub));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ public class FeedEntryFilteringService {
|
|||||||
.addVar("categories", SimpleType.STRING)
|
.addVar("categories", SimpleType.STRING)
|
||||||
.addVar("categoriesLower", SimpleType.STRING)
|
.addVar("categoriesLower", SimpleType.STRING)
|
||||||
.build();
|
.build();
|
||||||
private static final CelRuntime CEL_RUNTIME =
|
private static final CelRuntime CEL_RUNTIME = CelRuntimeFactory.plannerRuntimeBuilder().build();
|
||||||
CelRuntimeFactory.standardCelRuntimeBuilder().build();
|
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newCachedThreadPool();
|
private final ExecutorService executor = Executors.newCachedThreadPool();
|
||||||
private final CommaFeedConfiguration config;
|
private final CommaFeedConfiguration config;
|
||||||
|
|||||||
@@ -148,26 +148,23 @@ public class FeedEntryService {
|
|||||||
List<FeedEntryStatus> statusesToMark =
|
List<FeedEntryStatus> statusesToMark =
|
||||||
statuses.stream()
|
statuses.stream()
|
||||||
.filter(FeedEntryStatus::isMarkable)
|
.filter(FeedEntryStatus::isMarkable)
|
||||||
.filter(
|
.filter(s -> {
|
||||||
s -> {
|
Instant entryDate = s.getEntry().getPublished();
|
||||||
Instant entryDate = s.getEntry().getPublished();
|
return olderThan == null
|
||||||
return olderThan == null
|
|| entryDate == null
|
||||||
|| entryDate == null
|
|| entryDate.isBefore(olderThan);
|
||||||
|| entryDate.isBefore(olderThan);
|
})
|
||||||
})
|
.filter(s -> {
|
||||||
.filter(
|
Instant insertedDate = s.getEntry().getInserted();
|
||||||
s -> {
|
return insertedBefore == null
|
||||||
Instant insertedDate = s.getEntry().getInserted();
|
|| insertedDate == null
|
||||||
return insertedBefore == null
|
|| insertedDate.isBefore(insertedBefore);
|
||||||
|| insertedDate == null
|
})
|
||||||
|| insertedDate.isBefore(insertedBefore);
|
|
||||||
})
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
statusesToMark.forEach(
|
statusesToMark.forEach(s -> {
|
||||||
s -> {
|
s.setRead(true);
|
||||||
s.setRead(true);
|
feedEntryStatusDAO.merge(s);
|
||||||
feedEntryStatusDAO.merge(s);
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import lombok.extern.slf4j.Slf4j;
|
|||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Singleton
|
@Singleton
|
||||||
@@ -47,14 +46,13 @@ public class FeedSubscriptionService {
|
|||||||
// we need to use this hook because the feed needs to have been persisted before being
|
// we need to use this hook because the feed needs to have been persisted before being
|
||||||
// processed
|
// processed
|
||||||
// by the feed engine
|
// by the feed engine
|
||||||
feedSubscriptionDAO.onPostCommitInsert(
|
feedSubscriptionDAO.onPostCommitInsert(sub -> {
|
||||||
sub -> {
|
Feed feed = sub.getFeed();
|
||||||
Feed feed = sub.getFeed();
|
if (feed.getDisabledUntil() == null
|
||||||
if (feed.getDisabledUntil() == null
|
|| feed.getDisabledUntil().isBefore(Instant.now())) {
|
||||||
|| feed.getDisabledUntil().isBefore(Instant.now())) {
|
feedRefreshEngine.refreshImmediately(feed);
|
||||||
feedRefreshEngine.refreshImmediately(feed);
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long subscribe(
|
public long subscribe(
|
||||||
@@ -116,10 +114,7 @@ public class FeedSubscriptionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<Long, UnreadCount> getUnreadCount(User user) {
|
public Map<Long, UnreadCount> getUnreadCount(User user) {
|
||||||
return feedSubscriptionDAO.findAll(user).stream()
|
return feedEntryStatusDAO.getUnreadCounts(feedSubscriptionDAO.findAll(user));
|
||||||
.collect(
|
|
||||||
Collectors.toMap(
|
|
||||||
FeedSubscription::getId, feedEntryStatusDAO::getUnreadCount));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("serial")
|
@SuppressWarnings("serial")
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import org.apache.commons.lang3.StringUtils;
|
|||||||
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
|
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
|
||||||
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
|
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
|
||||||
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
|
import org.apache.hc.client5.http.async.methods.SimpleRequestProducer;
|
||||||
import org.apache.hc.client5.http.config.RequestConfig;
|
|
||||||
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
|
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
|
||||||
import org.apache.hc.core5.http.ContentType;
|
import org.apache.hc.core5.http.ContentType;
|
||||||
import org.apache.hc.core5.http.HttpResponse;
|
import org.apache.hc.core5.http.HttpResponse;
|
||||||
@@ -29,7 +28,6 @@ import org.apache.hc.core5.http.message.BasicNameValuePair;
|
|||||||
import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer;
|
import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer;
|
||||||
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
|
import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
|
||||||
import org.apache.hc.core5.net.WWWFormCodec;
|
import org.apache.hc.core5.net.WWWFormCodec;
|
||||||
import org.apache.hc.core5.util.Timeout;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
@@ -37,6 +35,8 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.Future;
|
import java.util.concurrent.Future;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -105,10 +105,6 @@ public class PushNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/" + topic);
|
SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/" + topic);
|
||||||
request.setRequestConfig(
|
|
||||||
RequestConfig.custom()
|
|
||||||
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
|
|
||||||
.build());
|
|
||||||
request.addHeader("Title", feedTitle);
|
request.addHeader("Title", feedTitle);
|
||||||
request.setBody(entryTitle, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
|
request.setBody(entryTitle, ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
@@ -152,10 +148,6 @@ public class PushNotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/message");
|
SimpleRequestBuilder request = SimpleRequestBuilder.post(serverUrl + "/message");
|
||||||
request.setRequestConfig(
|
|
||||||
RequestConfig.custom()
|
|
||||||
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
|
|
||||||
.build());
|
|
||||||
request.addHeader("X-Gotify-Key", token);
|
request.addHeader("X-Gotify-Key", token);
|
||||||
request.setBody(json.toString(), ContentType.APPLICATION_JSON);
|
request.setBody(json.toString(), ContentType.APPLICATION_JSON);
|
||||||
|
|
||||||
@@ -187,10 +179,6 @@ public class PushNotificationService {
|
|||||||
|
|
||||||
SimpleRequestBuilder request =
|
SimpleRequestBuilder request =
|
||||||
SimpleRequestBuilder.post("https://api.pushover.net/1/messages.json");
|
SimpleRequestBuilder.post("https://api.pushover.net/1/messages.json");
|
||||||
request.setRequestConfig(
|
|
||||||
RequestConfig.custom()
|
|
||||||
.setResponseTimeout(Timeout.of(config.httpClient().responseTimeout()))
|
|
||||||
.build());
|
|
||||||
request.setBody(
|
request.setBody(
|
||||||
WWWFormCodec.format(params, StandardCharsets.UTF_8),
|
WWWFormCodec.format(params, StandardCharsets.UTF_8),
|
||||||
ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8));
|
ContentType.APPLICATION_FORM_URLENCODED.withCharset(StandardCharsets.UTF_8));
|
||||||
@@ -205,7 +193,9 @@ public class PushNotificationService {
|
|||||||
new BasicResponseConsumer<>(new DiscardingEntityConsumer<>()),
|
new BasicResponseConsumer<>(new DiscardingEntityConsumer<>()),
|
||||||
null);
|
null);
|
||||||
try {
|
try {
|
||||||
HttpResponse response = future.get().getHead();
|
HttpResponse response =
|
||||||
|
future.get(config.httpClient().responseTimeout().toSeconds(), TimeUnit.SECONDS)
|
||||||
|
.getHead();
|
||||||
if (response.getCode() >= 400) {
|
if (response.getCode() >= 400) {
|
||||||
throw new PushNotificationException(
|
throw new PushNotificationException(
|
||||||
name + " notification failed with status " + response.getCode());
|
name + " notification failed with status " + response.getCode());
|
||||||
@@ -214,7 +204,11 @@ public class PushNotificationService {
|
|||||||
future.cancel(true);
|
future.cancel(true);
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
throw new IOException("interrupted while sending " + name + " notification", e);
|
throw new IOException("interrupted while sending " + name + " notification", e);
|
||||||
|
} catch (TimeoutException e) {
|
||||||
|
future.cancel(true);
|
||||||
|
throw new IOException("timed out while sending " + name + " notification", e);
|
||||||
} catch (ExecutionException e) {
|
} catch (ExecutionException e) {
|
||||||
|
future.cancel(true);
|
||||||
throw new IOException(e.getCause());
|
throw new IOException(e.getCause());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -79,13 +79,10 @@ public class DatabaseCleaningService {
|
|||||||
} while (entriesDeleted > 0);
|
} while (entriesDeleted > 0);
|
||||||
}
|
}
|
||||||
deleted =
|
deleted =
|
||||||
unitOfWork.call(
|
unitOfWork.call(() ->
|
||||||
() ->
|
feedDAO.delete(
|
||||||
feedDAO.delete(
|
feedDAO.findByIds(
|
||||||
feedDAO.findByIds(
|
feeds.stream().map(AbstractModel::getId).toList())));
|
||||||
feeds.stream()
|
|
||||||
.map(AbstractModel::getId)
|
|
||||||
.toList())));
|
|
||||||
total += deleted;
|
total += deleted;
|
||||||
log.debug("removed {} feeds without subscriptions", total);
|
log.debug("removed {} feeds without subscriptions", total);
|
||||||
} while (deleted != 0);
|
} while (deleted != 0);
|
||||||
@@ -119,10 +116,9 @@ public class DatabaseCleaningService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<FeedCapacity> feeds =
|
List<FeedCapacity> feeds =
|
||||||
unitOfWork.call(
|
unitOfWork.call(() ->
|
||||||
() ->
|
feedEntryDAO.findFeedsExceedingCapacity(
|
||||||
feedEntryDAO.findFeedsExceedingCapacity(
|
maxFeedCapacity, batchSize, keepStarredEntries));
|
||||||
maxFeedCapacity, batchSize, keepStarredEntries));
|
|
||||||
if (feeds.isEmpty()) {
|
if (feeds.isEmpty()) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -139,12 +135,11 @@ public class DatabaseCleaningService {
|
|||||||
|
|
||||||
final long rem = remaining;
|
final long rem = remaining;
|
||||||
deleted =
|
deleted =
|
||||||
unitOfWork.call(
|
unitOfWork.call(() ->
|
||||||
() ->
|
feedEntryDAO.deleteOldEntries(
|
||||||
feedEntryDAO.deleteOldEntries(
|
feed.id(),
|
||||||
feed.id(),
|
Math.min(batchSize, rem),
|
||||||
Math.min(batchSize, rem),
|
keepStarredEntries));
|
||||||
keepStarredEntries));
|
|
||||||
entriesDeletedMeter.mark(deleted);
|
entriesDeletedMeter.mark(deleted);
|
||||||
total += deleted;
|
total += deleted;
|
||||||
remaining -= deleted;
|
remaining -= deleted;
|
||||||
@@ -166,10 +161,9 @@ public class DatabaseCleaningService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleted =
|
deleted =
|
||||||
unitOfWork.call(
|
unitOfWork.call(() ->
|
||||||
() ->
|
feedEntryDAO.deleteEntriesOlderThan(
|
||||||
feedEntryDAO.deleteEntriesOlderThan(
|
olderThan, batchSize, keepStarredEntries));
|
||||||
olderThan, batchSize, keepStarredEntries));
|
|
||||||
entriesDeletedMeter.mark(deleted);
|
entriesDeletedMeter.mark(deleted);
|
||||||
total += deleted;
|
total += deleted;
|
||||||
log.debug("removed {} old entries", total);
|
log.debug("removed {} old entries", total);
|
||||||
|
|||||||
@@ -31,16 +31,15 @@ public class DemoAccountCleanupTask extends ScheduledTask {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log.info("recreating demo user account");
|
log.info("recreating demo user account");
|
||||||
unitOfWork.run(
|
unitOfWork.run(() -> {
|
||||||
() -> {
|
User demoUser = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO);
|
||||||
User demoUser = userDAO.findByName(CommaFeedConstants.USERNAME_DEMO);
|
if (demoUser == null) {
|
||||||
if (demoUser == null) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
userService.unregister(demoUser);
|
userService.unregister(demoUser);
|
||||||
userService.createDemoUser();
|
userService.createDemoUser();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -435,6 +435,19 @@ public class CategoryREST {
|
|||||||
&& !Strings.CS.equals(req.getParentId(), String.valueOf(req.getId()))) {
|
&& !Strings.CS.equals(req.getParentId(), String.valueOf(req.getId()))) {
|
||||||
parent = feedCategoryDAO.findById(user, Long.valueOf(req.getParentId()));
|
parent = feedCategoryDAO.findById(user, Long.valueOf(req.getParentId()));
|
||||||
}
|
}
|
||||||
|
if (parent != null) {
|
||||||
|
Long parentId = parent.getId();
|
||||||
|
List<FeedCategory> descendants =
|
||||||
|
feedCategoryDAO.findAllChildrenCategories(user, category);
|
||||||
|
boolean parentIsDescendant =
|
||||||
|
descendants.stream().anyMatch(c -> c.getId().equals(parentId));
|
||||||
|
if (parentIsDescendant) {
|
||||||
|
return Response.status(Status.BAD_REQUEST)
|
||||||
|
.entity(
|
||||||
|
"<message>cannot set a category's own descendant as its parent</message>")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
category.setParent(parent);
|
category.setParent(parent);
|
||||||
|
|
||||||
if (req.getPosition() != null) {
|
if (req.getPosition() != null) {
|
||||||
|
|||||||
@@ -140,14 +140,9 @@ public class FeverREST {
|
|||||||
Map<String, String> params = new HashMap<>();
|
Map<String, String> params = new HashMap<>();
|
||||||
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
uri.getQueryParameters().forEach((k, v) -> params.put(k, v.getFirst()));
|
||||||
form.getValues()
|
form.getValues()
|
||||||
.forEach(
|
.forEach((k, v) ->
|
||||||
(k, v) ->
|
params.put(
|
||||||
params.put(
|
k, v.stream().map(FormValue::getValue).findFirst().orElse(null)));
|
||||||
k,
|
|
||||||
v.stream()
|
|
||||||
.map(FormValue::getValue)
|
|
||||||
.findFirst()
|
|
||||||
.orElse(null)));
|
|
||||||
return handle(userId, params);
|
return handle(userId, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,46 +241,42 @@ public class FeverREST {
|
|||||||
s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
|
s -> s.getCategory() == null ? 0 : s.getCategory().getId()))
|
||||||
.entrySet()
|
.entrySet()
|
||||||
.stream()
|
.stream()
|
||||||
.map(
|
.map(e -> {
|
||||||
e -> {
|
FeverFeedGroup fg = new FeverFeedGroup();
|
||||||
FeverFeedGroup fg = new FeverFeedGroup();
|
fg.setGroupId(e.getKey());
|
||||||
fg.setGroupId(e.getKey());
|
fg.setFeedIds(e.getValue().stream().map(FeedSubscription::getId).toList());
|
||||||
fg.setFeedIds(
|
return fg;
|
||||||
e.getValue().stream().map(FeedSubscription::getId).toList());
|
})
|
||||||
return fg;
|
|
||||||
})
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FeverGroup> buildGroups(List<FeedCategory> categories) {
|
private List<FeverGroup> buildGroups(List<FeedCategory> categories) {
|
||||||
return categories.stream()
|
return categories.stream()
|
||||||
.map(
|
.map(c -> {
|
||||||
c -> {
|
FeverGroup g = new FeverGroup();
|
||||||
FeverGroup g = new FeverGroup();
|
g.setId(c.getId());
|
||||||
g.setId(c.getId());
|
g.setTitle(c.getName());
|
||||||
g.setTitle(c.getName());
|
return g;
|
||||||
return g;
|
})
|
||||||
})
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<FeverFeed> buildFeeds(List<FeedSubscription> subscriptions) {
|
private List<FeverFeed> buildFeeds(List<FeedSubscription> subscriptions) {
|
||||||
return subscriptions.stream()
|
return subscriptions.stream()
|
||||||
.map(
|
.map(s -> {
|
||||||
s -> {
|
FeverFeed f = new FeverFeed();
|
||||||
FeverFeed f = new FeverFeed();
|
f.setId(s.getId());
|
||||||
f.setId(s.getId());
|
f.setFaviconId(s.getId());
|
||||||
f.setFaviconId(s.getId());
|
f.setTitle(s.getTitle());
|
||||||
f.setTitle(s.getTitle());
|
f.setUrl(s.getFeed().getUrl());
|
||||||
f.setUrl(s.getFeed().getUrl());
|
f.setSiteUrl(s.getFeed().getLink());
|
||||||
f.setSiteUrl(s.getFeed().getLink());
|
f.setSpark(false);
|
||||||
f.setSpark(false);
|
f.setLastUpdatedOnTime(
|
||||||
f.setLastUpdatedOnTime(
|
s.getFeed().getLastUpdated() == null
|
||||||
s.getFeed().getLastUpdated() == null
|
? 0
|
||||||
? 0
|
: s.getFeed().getLastUpdated().getEpochSecond());
|
||||||
: s.getFeed().getLastUpdated().getEpochSecond());
|
return f;
|
||||||
return f;
|
})
|
||||||
})
|
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,19 +358,18 @@ public class FeverREST {
|
|||||||
|
|
||||||
private List<FeverFavicon> buildFavicons(List<FeedSubscription> subscriptions) {
|
private List<FeverFavicon> buildFavicons(List<FeedSubscription> subscriptions) {
|
||||||
return subscriptions.stream()
|
return subscriptions.stream()
|
||||||
.map(
|
.map(s -> {
|
||||||
s -> {
|
Favicon favicon = feedFaviconService.fetchFavicon(s.getFeed());
|
||||||
Favicon favicon = feedFaviconService.fetchFavicon(s.getFeed());
|
|
||||||
|
|
||||||
FeverFavicon f = new FeverFavicon();
|
FeverFavicon f = new FeverFavicon();
|
||||||
f.setId(s.getFeed().getId());
|
f.setId(s.getFeed().getId());
|
||||||
f.setData(
|
f.setData(
|
||||||
String.format(
|
String.format(
|
||||||
"data:%s;base64,%s",
|
"data:%s;base64,%s",
|
||||||
favicon.mediaType(),
|
favicon.mediaType(),
|
||||||
Base64.getEncoder().encodeToString(favicon.icon())));
|
Base64.getEncoder().encodeToString(favicon.icon())));
|
||||||
return f;
|
return f;
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -216,11 +216,10 @@ public class GoogleReaderREST {
|
|||||||
List<GoogleReaderModel.Tag> tags = new ArrayList<>();
|
List<GoogleReaderModel.Tag> tags = new ArrayList<>();
|
||||||
feedCategoryDAO
|
feedCategoryDAO
|
||||||
.findAll(user)
|
.findAll(user)
|
||||||
.forEach(
|
.forEach(c ->
|
||||||
c ->
|
tags.add(
|
||||||
tags.add(
|
new GoogleReaderModel.Tag(
|
||||||
new GoogleReaderModel.Tag(
|
GoogleReaderStreamId.label(c.getName()))));
|
||||||
GoogleReaderStreamId.label(c.getName()))));
|
|
||||||
tags.add(new GoogleReaderModel.Tag(GoogleReaderStreamId.STARRED));
|
tags.add(new GoogleReaderModel.Tag(GoogleReaderStreamId.STARRED));
|
||||||
|
|
||||||
GoogleReaderModel.TagList resp = new GoogleReaderModel.TagList();
|
GoogleReaderModel.TagList resp = new GoogleReaderModel.TagList();
|
||||||
@@ -259,21 +258,19 @@ public class GoogleReaderREST {
|
|||||||
Map<Long, FeedCategory> categoriesById =
|
Map<Long, FeedCategory> categoriesById =
|
||||||
feedCategoryDAO.findAll(user).stream()
|
feedCategoryDAO.findAll(user).stream()
|
||||||
.collect(Collectors.toMap(FeedCategory::getId, c -> c));
|
.collect(Collectors.toMap(FeedCategory::getId, c -> c));
|
||||||
unreadByCategory.forEach(
|
unreadByCategory.forEach((categoryId, count) -> {
|
||||||
(categoryId, count) -> {
|
if (categoryId == 0) {
|
||||||
if (categoryId == 0) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
FeedCategory category = categoriesById.get(categoryId);
|
||||||
FeedCategory category = categoriesById.get(categoryId);
|
if (category == null) {
|
||||||
if (category == null) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
GoogleReaderModel.UnreadCountEntry entry = new GoogleReaderModel.UnreadCountEntry();
|
||||||
GoogleReaderModel.UnreadCountEntry entry =
|
entry.setId(GoogleReaderStreamId.label(category.getName()));
|
||||||
new GoogleReaderModel.UnreadCountEntry();
|
entry.setCount(count);
|
||||||
entry.setId(GoogleReaderStreamId.label(category.getName()));
|
entries.add(entry);
|
||||||
entry.setCount(count);
|
});
|
||||||
entries.add(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
GoogleReaderModel.UnreadCountEntry readingList = new GoogleReaderModel.UnreadCountEntry();
|
GoogleReaderModel.UnreadCountEntry readingList = new GoogleReaderModel.UnreadCountEntry();
|
||||||
readingList.setId(GoogleReaderStreamId.READING_LIST);
|
readingList.setId(GoogleReaderStreamId.READING_LIST);
|
||||||
@@ -334,10 +331,8 @@ public class GoogleReaderREST {
|
|||||||
GoogleReaderModel.ItemRefs resp = new GoogleReaderModel.ItemRefs();
|
GoogleReaderModel.ItemRefs resp = new GoogleReaderModel.ItemRefs();
|
||||||
resp.setItemRefs(
|
resp.setItemRefs(
|
||||||
paged.statuses().stream()
|
paged.statuses().stream()
|
||||||
.map(
|
.map(s ->
|
||||||
s ->
|
new GoogleReaderModel.ItemRef(String.valueOf(s.getEntry().getId())))
|
||||||
new GoogleReaderModel.ItemRef(
|
|
||||||
String.valueOf(s.getEntry().getId())))
|
|
||||||
.toList());
|
.toList());
|
||||||
resp.setContinuation(paged.continuation());
|
resp.setContinuation(paged.continuation());
|
||||||
return resp;
|
return resp;
|
||||||
|
|||||||
@@ -38,20 +38,18 @@ public class DatabaseApiKeyIdentityProvider
|
|||||||
@Override
|
@Override
|
||||||
public Uni<SecurityIdentity> authenticate(
|
public Uni<SecurityIdentity> authenticate(
|
||||||
TokenAuthenticationRequest request, AuthenticationRequestContext context) {
|
TokenAuthenticationRequest request, AuthenticationRequestContext context) {
|
||||||
return context.runBlocking(
|
return context.runBlocking(() -> {
|
||||||
() -> {
|
Optional<User> user =
|
||||||
Optional<User> user =
|
unitOfWork.call(() -> userService.login(request.getToken().getToken()));
|
||||||
unitOfWork.call(() -> userService.login(request.getToken().getToken()));
|
if (user.isEmpty()) {
|
||||||
if (user.isEmpty()) {
|
throw new AuthenticationFailedException("could not find a user with this api key");
|
||||||
throw new AuthenticationFailedException(
|
}
|
||||||
"could not find a user with this api key");
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
||||||
return QuarkusSecurityIdentity.builder()
|
return QuarkusSecurityIdentity.builder()
|
||||||
.setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
|
.setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
|
||||||
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
||||||
.build();
|
.build();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,25 +39,22 @@ public class DatabaseUsernamePasswordIdentityProvider
|
|||||||
@Override
|
@Override
|
||||||
public Uni<SecurityIdentity> authenticate(
|
public Uni<SecurityIdentity> authenticate(
|
||||||
UsernamePasswordAuthenticationRequest request, AuthenticationRequestContext context) {
|
UsernamePasswordAuthenticationRequest request, AuthenticationRequestContext context) {
|
||||||
return context.runBlocking(
|
return context.runBlocking(() -> {
|
||||||
() -> {
|
Optional<User> user =
|
||||||
Optional<User> user =
|
unitOfWork.call(() ->
|
||||||
unitOfWork.call(
|
userService.login(
|
||||||
() ->
|
request.getUsername(),
|
||||||
userService.login(
|
new String(request.getPassword().getPassword())));
|
||||||
request.getUsername(),
|
if (user.isEmpty()) {
|
||||||
new String(
|
throw new CommaFeedApplicationException(
|
||||||
request.getPassword().getPassword())));
|
CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD);
|
||||||
if (user.isEmpty()) {
|
}
|
||||||
throw new CommaFeedApplicationException(
|
|
||||||
CommaFeedExceptionType.WRONG_USERNAME_OR_PASSWORD);
|
|
||||||
}
|
|
||||||
|
|
||||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user.get()));
|
||||||
return QuarkusSecurityIdentity.builder()
|
return QuarkusSecurityIdentity.builder()
|
||||||
.setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
|
.setPrincipal(new QuarkusPrincipal(String.valueOf(user.get().getId())))
|
||||||
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
||||||
.build();
|
.build();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,24 +40,23 @@ public class TrustedIdentityProvider implements IdentityProvider<TrustedAuthenti
|
|||||||
@Override
|
@Override
|
||||||
public Uni<SecurityIdentity> authenticate(
|
public Uni<SecurityIdentity> authenticate(
|
||||||
TrustedAuthenticationRequest request, AuthenticationRequestContext context) {
|
TrustedAuthenticationRequest request, AuthenticationRequestContext context) {
|
||||||
return context.runBlocking(
|
return context.runBlocking(() -> {
|
||||||
() -> {
|
Long userId = Long.valueOf(request.getPrincipal());
|
||||||
Long userId = Long.valueOf(request.getPrincipal());
|
User user = unitOfWork.call(() -> userDAO.findById(userId));
|
||||||
User user = unitOfWork.call(() -> userDAO.findById(userId));
|
if (user == null) {
|
||||||
if (user == null) {
|
throw new AuthenticationFailedException("user not found");
|
||||||
throw new AuthenticationFailedException("user not found");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// execute post login activities manually because we didn't call login() since
|
// execute post login activities manually because we didn't call login() since
|
||||||
// we received
|
// we received
|
||||||
// a trusted authentication request
|
// a trusted authentication request
|
||||||
unitOfWork.run(() -> postLoginActivities.executeFor(user));
|
unitOfWork.run(() -> postLoginActivities.executeFor(user));
|
||||||
|
|
||||||
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user));
|
Set<Role> roles = unitOfWork.call(() -> userService.getRoles(user));
|
||||||
return QuarkusSecurityIdentity.builder()
|
return QuarkusSecurityIdentity.builder()
|
||||||
.setPrincipal(new QuarkusPrincipal(String.valueOf(userId)))
|
.setPrincipal(new QuarkusPrincipal(String.valueOf(userId)))
|
||||||
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
.addRoles(roles.stream().map(Enum::name).collect(Collectors.toSet()))
|
||||||
.build();
|
.build();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,11 +41,10 @@ class NativeImageClassesTest {
|
|||||||
WireFeedGenerator.class)) {
|
WireFeedGenerator.class)) {
|
||||||
Set<Class<?>> moduleClasses =
|
Set<Class<?>> moduleClasses =
|
||||||
new HashSet<>(reflections.get(Scanners.SubTypes.of(clazz).asClass()));
|
new HashSet<>(reflections.get(Scanners.SubTypes.of(clazz).asClass()));
|
||||||
moduleClasses.removeIf(
|
moduleClasses.removeIf(c ->
|
||||||
c ->
|
c.isInterface()
|
||||||
c.isInterface()
|
|| Modifier.isAbstract(c.getModifiers())
|
||||||
|| Modifier.isAbstract(c.getModifiers())
|
|| !Modifier.isPublic(c.getModifiers()));
|
||||||
|| !Modifier.isPublic(c.getModifiers()));
|
|
||||||
moduleClasses.removeAll(classesInAnnotation);
|
moduleClasses.removeAll(classesInAnnotation);
|
||||||
missingClasses.addAll(moduleClasses);
|
missingClasses.addAll(moduleClasses);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ import java.io.ByteArrayOutputStream;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.net.NoRouteToHostException;
|
import java.net.NoRouteToHostException;
|
||||||
import java.net.SocketTimeoutException;
|
|
||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
@@ -241,7 +240,7 @@ class HttpGetterTest {
|
|||||||
.when(HttpRequest.request().withMethod("GET"))
|
.when(HttpRequest.request().withMethod("GET"))
|
||||||
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
|
.respond(HttpResponse.response().withDelay(Delay.milliseconds(1000)));
|
||||||
|
|
||||||
Assertions.assertThrows(SocketTimeoutException.class, () -> getter.get(this.feedUrl));
|
Assertions.assertThrows(IOException.class, () -> getter.get(this.feedUrl));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -318,18 +317,17 @@ class HttpGetterTest {
|
|||||||
|
|
||||||
this.mockServerClient
|
this.mockServerClient
|
||||||
.when(HttpRequest.request().withMethod("GET"))
|
.when(HttpRequest.request().withMethod("GET"))
|
||||||
.respond(
|
.respond(req -> {
|
||||||
req -> {
|
calls.incrementAndGet();
|
||||||
calls.incrementAndGet();
|
|
||||||
|
|
||||||
if (req.containsHeader(HttpHeaders.COOKIE)) {
|
if (req.containsHeader(HttpHeaders.COOKIE)) {
|
||||||
throw new Exception("cookie should not be sent by the client");
|
throw new Exception("cookie should not be sent by the client");
|
||||||
}
|
}
|
||||||
|
|
||||||
return HttpResponse.response()
|
return HttpResponse.response()
|
||||||
.withBody("ok")
|
.withBody("ok")
|
||||||
.withHeader(HttpHeaders.SET_COOKIE, "foo=bar");
|
.withHeader(HttpHeaders.SET_COOKIE, "foo=bar");
|
||||||
});
|
});
|
||||||
|
|
||||||
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
|
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
|
||||||
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl + "?foo=bar"));
|
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl + "?foo=bar"));
|
||||||
@@ -342,11 +340,10 @@ class HttpGetterTest {
|
|||||||
|
|
||||||
this.mockServerClient
|
this.mockServerClient
|
||||||
.when(HttpRequest.request().withMethod("GET"))
|
.when(HttpRequest.request().withMethod("GET"))
|
||||||
.respond(
|
.respond(req -> {
|
||||||
req -> {
|
calls.incrementAndGet();
|
||||||
calls.incrementAndGet();
|
return HttpResponse.response().withBody("ok");
|
||||||
return HttpResponse.response().withBody("ok");
|
});
|
||||||
});
|
|
||||||
|
|
||||||
HttpResult result = getter.get(this.feedUrl);
|
HttpResult result = getter.get(this.feedUrl);
|
||||||
Assertions.assertEquals(result, getter.get(this.feedUrl));
|
Assertions.assertEquals(result, getter.get(this.feedUrl));
|
||||||
@@ -401,17 +398,15 @@ class HttpGetterTest {
|
|||||||
|
|
||||||
this.mockServerClient
|
this.mockServerClient
|
||||||
.when(HttpRequest.request().withMethod("GET"))
|
.when(HttpRequest.request().withMethod("GET"))
|
||||||
.respond(
|
.respond(req -> {
|
||||||
req -> {
|
calls.incrementAndGet();
|
||||||
calls.incrementAndGet();
|
|
||||||
|
|
||||||
if (req.containsHeader(HttpHeaders.UPGRADE)) {
|
if (req.containsHeader(HttpHeaders.UPGRADE)) {
|
||||||
throw new Exception(
|
throw new Exception("upgrade header should not be sent by the client");
|
||||||
"upgrade header should not be sent by the client");
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return HttpResponse.response().withBody("ok");
|
return HttpResponse.response().withBody("ok");
|
||||||
});
|
});
|
||||||
|
|
||||||
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
|
Assertions.assertDoesNotThrow(() -> getter.get(this.feedUrl));
|
||||||
Assertions.assertEquals(1, calls.get());
|
Assertions.assertEquals(1, calls.get());
|
||||||
@@ -439,33 +434,30 @@ class HttpGetterTest {
|
|||||||
HttpGetterTest.this
|
HttpGetterTest.this
|
||||||
.mockServerClient
|
.mockServerClient
|
||||||
.when(HttpRequest.request().withMethod("GET"))
|
.when(HttpRequest.request().withMethod("GET"))
|
||||||
.respond(
|
.respond(req -> {
|
||||||
req -> {
|
try {
|
||||||
try {
|
String acceptEncodingHeader =
|
||||||
String acceptEncodingHeader =
|
req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
|
||||||
req.getFirstHeader(HttpHeaders.ACCEPT_ENCODING);
|
if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) {
|
||||||
if (!ACCEPT_ENCODING.equals(acceptEncodingHeader)) {
|
throw new Exception(
|
||||||
throw new Exception(
|
"Wrong value in the Accept-Encoding header, should be '%s' but was '%s'"
|
||||||
"Wrong value in the Accept-Encoding header, should be '%s' but was '%s'"
|
.formatted(ACCEPT_ENCODING, acceptEncodingHeader));
|
||||||
.formatted(
|
}
|
||||||
ACCEPT_ENCODING,
|
|
||||||
acceptEncodingHeader));
|
|
||||||
}
|
|
||||||
|
|
||||||
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
ByteArrayOutputStream output = new ByteArrayOutputStream();
|
||||||
try (OutputStream compressionOutputStream =
|
try (OutputStream compressionOutputStream =
|
||||||
compressionFunction.apply(output)) {
|
compressionFunction.apply(output)) {
|
||||||
compressionOutputStream.write(body.getBytes());
|
compressionOutputStream.write(body.getBytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
return HttpResponse.response()
|
return HttpResponse.response()
|
||||||
.withBody(output.toByteArray())
|
.withBody(output.toByteArray())
|
||||||
.withHeader(HttpHeaders.CONTENT_ENCODING, encoding);
|
.withHeader(HttpHeaders.CONTENT_ENCODING, encoding);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error in compression callback", e);
|
log.error("Error in compression callback", e);
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
HttpResult result = getter.get(HttpGetterTest.this.feedUrl);
|
HttpResult result = getter.get(HttpGetterTest.this.feedUrl);
|
||||||
Assertions.assertEquals(body, new String(result.content()));
|
Assertions.assertEquals(body, new String(result.content()));
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ class YoutubeFaviconFetcherTest {
|
|||||||
|
|
||||||
byte[] apiResponse =
|
byte[] apiResponse =
|
||||||
"""
|
"""
|
||||||
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}"""
|
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}\
|
||||||
|
"""
|
||||||
.getBytes();
|
.getBytes();
|
||||||
HttpResult apiHttpResult =
|
HttpResult apiHttpResult =
|
||||||
new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
|
new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
|
||||||
@@ -102,7 +103,8 @@ class YoutubeFaviconFetcherTest {
|
|||||||
|
|
||||||
byte[] apiResponse =
|
byte[] apiResponse =
|
||||||
"""
|
"""
|
||||||
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}"""
|
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}\
|
||||||
|
"""
|
||||||
.getBytes();
|
.getBytes();
|
||||||
HttpResult apiHttpResult =
|
HttpResult apiHttpResult =
|
||||||
new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
|
new HttpResult(apiResponse, "application/json", null, null, null, Duration.ZERO);
|
||||||
@@ -136,7 +138,8 @@ class YoutubeFaviconFetcherTest {
|
|||||||
|
|
||||||
byte[] playlistResponse =
|
byte[] playlistResponse =
|
||||||
"""
|
"""
|
||||||
{"items":[{"snippet":{"channelId":"testChannelId"}}]}"""
|
{"items":[{"snippet":{"channelId":"testChannelId"}}]}\
|
||||||
|
"""
|
||||||
.getBytes();
|
.getBytes();
|
||||||
HttpResult playlistHttpResult =
|
HttpResult playlistHttpResult =
|
||||||
new HttpResult(
|
new HttpResult(
|
||||||
@@ -151,7 +154,8 @@ class YoutubeFaviconFetcherTest {
|
|||||||
|
|
||||||
byte[] channelResponse =
|
byte[] channelResponse =
|
||||||
"""
|
"""
|
||||||
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}"""
|
{"items":[{"snippet":{"thumbnails":{"default":{"url":"https://example.com/icon.png"}}}}]}\
|
||||||
|
"""
|
||||||
.getBytes();
|
.getBytes();
|
||||||
HttpResult channelHttpResult =
|
HttpResult channelHttpResult =
|
||||||
new HttpResult(
|
new HttpResult(
|
||||||
|
|||||||
@@ -226,14 +226,16 @@ class XMLCleanerTest {
|
|||||||
void testRemoveMultilineDoctype() {
|
void testRemoveMultilineDoctype() {
|
||||||
String source =
|
String source =
|
||||||
"""
|
"""
|
||||||
<!DOCTYPE
|
<!DOCTYPE
|
||||||
html
|
html
|
||||||
>
|
>
|
||||||
<html><head></head><body></body></html>""";
|
<html><head></head><body></body></html>\
|
||||||
|
""";
|
||||||
Assertions.assertEquals(
|
Assertions.assertEquals(
|
||||||
"""
|
"""
|
||||||
|
|
||||||
<html><head></head><body></body></html>""",
|
<html><head></head><body></body></html>\
|
||||||
|
""",
|
||||||
xmlCleaner.removeDoctypeDeclarations(source));
|
xmlCleaner.removeDoctypeDeclarations(source));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,23 +12,23 @@ class FeedEntryContentCleaningServiceTest {
|
|||||||
void testClean() {
|
void testClean() {
|
||||||
String content =
|
String content =
|
||||||
"""
|
"""
|
||||||
<p>
|
<p>
|
||||||
Some text
|
Some text
|
||||||
<img width="965" height="320" src="https://localhost/an-image.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="alt-desc" decoding="async" sizes="(max-width: 965px) 100vw, 965px" style="width: 100%; opacity: 0">
|
<img width="965" height="320" src="https://localhost/an-image.png" class="attachment-post-thumbnail size-post-thumbnail wp-post-image" alt="alt-desc" decoding="async" sizes="(max-width: 965px) 100vw, 965px" style="width: 100%; opacity: 0">
|
||||||
<iframe src="https://localhost/an-iframe" style="width: 100%; opacity: 0"></iframe>
|
<iframe src="https://localhost/an-iframe" style="width: 100%; opacity: 0"></iframe>
|
||||||
<forbidden-element>aaa</forbidden-element>
|
<forbidden-element>aaa</forbidden-element>
|
||||||
""";
|
""";
|
||||||
String result = feedEntryContentCleaningService.clean(content, "baseUri", false);
|
String result = feedEntryContentCleaningService.clean(content, "baseUri", false);
|
||||||
|
|
||||||
Assertions.assertLinesMatch(
|
Assertions.assertLinesMatch(
|
||||||
"""
|
"""
|
||||||
<p>
|
<p>
|
||||||
Some text
|
Some text
|
||||||
<img width="965" height="320" src="https://localhost/an-image.png" alt="alt-desc" style="width:100%;">
|
<img width="965" height="320" src="https://localhost/an-image.png" alt="alt-desc" style="width:100%;">
|
||||||
<iframe src="https://localhost/an-iframe" style="width:100%;"></iframe>
|
<iframe src="https://localhost/an-iframe" style="width:100%;"></iframe>
|
||||||
aaa
|
aaa
|
||||||
</p>
|
</p>
|
||||||
"""
|
"""
|
||||||
.lines(),
|
.lines(),
|
||||||
result.lines());
|
result.lines());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -98,19 +98,19 @@ class PushNotificationServiceTest {
|
|||||||
.withBody(
|
.withBody(
|
||||||
JsonBody.json(
|
JsonBody.json(
|
||||||
"""
|
"""
|
||||||
{
|
{
|
||||||
"title": "Test Feed",
|
"title": "Test Feed",
|
||||||
"message": "Test Entry",
|
"message": "Test Entry",
|
||||||
"priority": 5,
|
"priority": 5,
|
||||||
"extras": {
|
"extras": {
|
||||||
"client::notification": {
|
"client::notification": {
|
||||||
"click": {
|
"click": {
|
||||||
"url": "http://example.com/entry"
|
"url": "http://example.com/entry"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
""")))
|
""")))
|
||||||
.respond(HttpResponse.response().withStatusCode(200));
|
.respond(HttpResponse.response().withStatusCode(200));
|
||||||
|
|
||||||
Assertions.assertDoesNotThrow(
|
Assertions.assertDoesNotThrow(
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ class InPageReferenceFeedURLProviderTest {
|
|||||||
String url = "http://example.com";
|
String url = "http://example.com";
|
||||||
String html =
|
String html =
|
||||||
"""
|
"""
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link type="application/atom+xml" href="/feed.atom">
|
<link type="application/atom+xml" href="/feed.atom">
|
||||||
<link type="application/rss+xml" href="/feed.rss">
|
<link type="application/rss+xml" href="/feed.rss">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
</html>""";
|
</html>\
|
||||||
|
""";
|
||||||
|
|
||||||
Assertions.assertIterableEquals(
|
Assertions.assertIterableEquals(
|
||||||
List.of("http://example.com/feed.atom", "http://example.com/feed.rss"),
|
List.of("http://example.com/feed.atom", "http://example.com/feed.rss"),
|
||||||
@@ -33,9 +34,10 @@ class InPageReferenceFeedURLProviderTest {
|
|||||||
String url = "http://example.com";
|
String url = "http://example.com";
|
||||||
String html =
|
String html =
|
||||||
"""
|
"""
|
||||||
<?xml version="1.0"?>
|
<?xml version="1.0"?>
|
||||||
<feed></feed>
|
<feed></feed>
|
||||||
</xml>""";
|
</xml>\
|
||||||
|
""";
|
||||||
|
|
||||||
Assertions.assertTrue(provider.get(url, html).isEmpty());
|
Assertions.assertTrue(provider.get(url, html).isEmpty());
|
||||||
}
|
}
|
||||||
@@ -45,13 +47,14 @@ class InPageReferenceFeedURLProviderTest {
|
|||||||
String url = "http://example.com";
|
String url = "http://example.com";
|
||||||
String html =
|
String html =
|
||||||
"""
|
"""
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<link type="text/css" href="/style.css">
|
<link type="text/css" href="/style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
</body>
|
</body>
|
||||||
</html>""";
|
</html>\
|
||||||
|
""";
|
||||||
|
|
||||||
Assertions.assertTrue(provider.get(url, html).isEmpty());
|
Assertions.assertTrue(provider.get(url, html).isEmpty());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,10 +170,9 @@ class GoogleReaderIT extends BaseIT {
|
|||||||
client.get(BASE + "/reader/api/0/unread-count").as(UnreadCounts.class);
|
client.get(BASE + "/reader/api/0/unread-count").as(UnreadCounts.class);
|
||||||
Assertions.assertTrue(
|
Assertions.assertTrue(
|
||||||
counts.getUnreadCounts().stream()
|
counts.getUnreadCounts().stream()
|
||||||
.anyMatch(
|
.anyMatch(c ->
|
||||||
c ->
|
c.getId().equals("user/-/state/com.google/reading-list")
|
||||||
c.getId().equals("user/-/state/com.google/reading-list")
|
&& c.getCount() == 2));
|
||||||
&& c.getCount() == 2));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
<groupId>com.commafeed</groupId>
|
<groupId>com.commafeed</groupId>
|
||||||
<artifactId>commafeed</artifactId>
|
<artifactId>commafeed</artifactId>
|
||||||
<version>7.4.0</version>
|
<version>7.4.1</version>
|
||||||
<name>CommaFeed</name>
|
<name>CommaFeed</name>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user