├── l10n ├── .gitkeep ├── ps.json ├── ur_PK.json ├── ps.js ├── ur_PK.js ├── an.json ├── an.js ├── hy.json ├── hy.js ├── ms_MY.json ├── kn.json ├── ms_MY.js ├── kn.js ├── tk.json ├── tk.js ├── gd.json ├── gd.js ├── ta.json ├── bs.json ├── ta.js ├── bs.js ├── kab.json ├── km.json ├── kab.js ├── km.js ├── sr@latin.json ├── si.json ├── sr@latin.js ├── si.js ├── uz.json ├── uz.js ├── az.json ├── nn_NO.json ├── az.js ├── nn_NO.js ├── cy_GB.json ├── cy_GB.js ├── bn_BD.json ├── bn_BD.js ├── br.json ├── br.js ├── af.json ├── af.js ├── eo.json ├── eo.js ├── mn.json ├── lb.json ├── mn.js ├── lb.js ├── vi.json ├── vi.js ├── mk.json ├── mk.js ├── ia.json └── ia.js ├── .envrc ├── .github ├── styles │ ├── vocab.txt │ └── config │ │ └── vocabularies │ │ └── Nextcloud-News │ │ └── accept.txt ├── ISSUE_TEMPLATE │ ├── config.yml │ └── bug_report.md ├── pull_request_template.md ├── workflows │ ├── changelog-enforcer.yml │ ├── lint-text.yml │ ├── documentation.yml │ ├── lint.yml │ ├── lint-info-xml.yml │ ├── api-php-tests.yml │ └── api-php-static-code-check.yml ├── no-response.yml ├── stale.yml └── dependabot.yml ├── .stylelintignore ├── docker ├── news.config.php ├── Dockerfile ├── docker-compose.yml ├── Makefile └── README.md ├── img ├── app-128.png ├── app-512.png ├── favicon.ico ├── favicon.png ├── favicon-touch.png ├── facebook.svg ├── app.svg ├── rss.svg ├── twitter.svg ├── favicon-touch.svg ├── favicon.svg └── app-dark.svg ├── screenshots ├── 1.png ├── 2.png ├── 3.png ├── 1-small.png ├── 2-small.png └── 3-small.png ├── docs ├── assets │ ├── favicon.png │ └── logo.svg ├── features │ ├── themes.md │ ├── customCSS.md │ └── integration.md ├── README.md ├── index.md └── maintenance.md ├── tests ├── test_helper │ └── feeds │ │ ├── logo.png │ │ ├── favicon.ico │ │ └── Nextcloud.opml ├── command │ ├── helpers │ │ └── settings.bash │ ├── explore.bats │ ├── update.bats │ ├── opml.bats │ └── folders.bats ├── api │ └── helpers │ │ └── settings.bash ├── bootstrap.php ├── updater │ └── helpers │ │ └── settings.bash ├── javascript │ └── unit │ │ ├── components │ │ ├── routes │ │ │ ├── Explore.spec.ts │ │ │ ├── Recent.spec.ts │ │ │ └── Item.spec.ts │ │ └── App.spec.ts │ │ ├── setup.ts │ │ └── services │ │ ├── share.service.spec.ts │ │ └── folder.service.spec.ts └── Unit │ ├── Controller │ └── JSONHttpErrorTest.php │ └── Utility │ └── TimeTest.php ├── vendor-bin └── php-scoper │ └── composer.json ├── stylelint.config.cjs ├── .vale.ini ├── src ├── types │ ├── ExploreSite.ts │ ├── Folder.ts │ ├── nextcloud__router.d.ts │ ├── FeedItem.ts │ ├── ApiRoutes.ts │ ├── Feed.ts │ └── MutationTypes.ts ├── main-admin.js ├── utils │ ├── unreadCache.ts │ ├── dateUtils.ts │ └── itemFilter.ts ├── enums │ └── index.ts ├── main-cron-warning.js ├── README.md ├── components │ └── routes │ │ ├── Recent.vue │ │ ├── Item.vue │ │ ├── All.vue │ │ └── Starred.vue ├── dataservices │ └── share.service.ts ├── store │ └── index.ts └── main.js ├── bin ├── git │ └── hooks │ │ ├── pre-commit │ │ └── README.md └── tools │ ├── check_feeds.sh │ ├── file_from_env.php │ └── generate_authors.php ├── templates ├── part.content.warnings.php ├── admin.php └── index.php ├── _typos.toml ├── shell.nix ├── .tx └── config ├── lib ├── Controller │ ├── Exceptions │ │ └── NotLoggedInException.php │ ├── FaviconController.php │ ├── FaviconApiController.php │ ├── JSONHttpErrorTrait.php │ ├── ApiPayloadTrait.php │ ├── Controller.php │ └── ImportController.php ├── Constants.php ├── Explore │ ├── Exceptions │ │ └── RecommendedSiteNotFoundException.php │ └── RecommendedSites.php ├── Db │ ├── IAPI.php │ ├── ListType.php │ └── EntityJSONSerializer.php ├── Utility │ ├── Time.php │ └── Cache.php ├── Settings │ ├── AdminSection.php │ └── AdminSettings.php ├── Listeners │ ├── AddMissingIndicesListener.php │ └── UserSettingsListener.php ├── Service │ ├── Exceptions │ │ ├── ServiceNotFoundException.php │ │ ├── ServiceConflictException.php │ │ ├── ServiceValidationException.php │ │ └── ServiceException.php │ └── UpdaterService.php ├── Scraper │ └── IScraper.php ├── Migration │ ├── RemoveUnusedJob.php │ ├── Version150302Date20210312231251.php │ ├── Version250200Date20241219085150.php │ └── Version150400Date20210318215425.php ├── Hooks │ └── UserDeleteHook.php ├── Command │ ├── Config │ │ ├── OpmlExport.php │ │ ├── FeedDelete.php │ │ ├── FolderDelete.php │ │ ├── OpmlImport.php │ │ ├── FolderAdd.php │ │ ├── FeedList.php │ │ └── FolderList.php │ └── Updater │ │ └── BeforeUpdate.php ├── Plugin │ └── Client │ │ └── Plugin.php ├── Fetcher │ ├── FaviconDataAccess.php │ └── IFeedFetcher.php └── Cron │ └── UpdaterJob.php ├── eslint.config.mjs ├── .gitmodules ├── .editorconfig ├── phpstan.neon.dist ├── .devcontainer ├── README.md ├── setup.sh └── devcontainer.json ├── tsconfig.json ├── vite.config.ts ├── .mailmap ├── term.kdl ├── .gitignore ├── phpunit.xml ├── mkdocs.yml ├── README.md └── scoper.inc.php /l10n/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.github/styles/vocab.txt: -------------------------------------------------------------------------------- 1 | # test 2 | -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | coverage/ -------------------------------------------------------------------------------- /docker/news.config.php: -------------------------------------------------------------------------------- 1 | 5 |
6 | 7 | -------------------------------------------------------------------------------- /_typos.toml: -------------------------------------------------------------------------------- 1 | [files] 2 | extend-exclude = [ 3 | "l10n/*.js*", 4 | "js-old/**", 5 | "/tests/test_helper/feeds/*", 6 | "tests/Unit/Db/ItemTest.php", 7 | "AUTHORS.md" 8 | ] 9 | -------------------------------------------------------------------------------- /docs/features/themes.md: -------------------------------------------------------------------------------- 1 | # Themes 2 | 3 | Nextcloud News can look different with the following themes: 4 | 5 | * [Nextcloud News Themes](https://github.com/cwmke/nextcloud-news-themes) 6 | -------------------------------------------------------------------------------- /tests/command/helpers/settings.bash: -------------------------------------------------------------------------------- 1 | user=admin 2 | NC_FEED="http://localhost:8090/Nextcloud.rss" 3 | HEISE_FEED="http://localhost:8090/heise.xml" 4 | NO_GUID_FEED="http://localhost:8090/no_guid_feed.xml" 5 | -------------------------------------------------------------------------------- /src/types/Folder.ts: -------------------------------------------------------------------------------- 1 | import type { Feed } from './Feed.ts' 2 | 3 | export type Folder = { 4 | feeds: Feed[] 5 | feedCount: number 6 | updateErrorCount: number 7 | name: string 8 | id: number 9 | opened: boolean 10 | } 11 | -------------------------------------------------------------------------------- /src/types/nextcloud__router.d.ts: -------------------------------------------------------------------------------- 1 | // src/types/nextcloud__router.d.ts 2 | declare module '@nextcloud/router' { 3 | export function generateUrl(route: string): string 4 | export function generateOcsUrl(route: string): string 5 | } 6 | -------------------------------------------------------------------------------- /src/types/FeedItem.ts: -------------------------------------------------------------------------------- 1 | export type FeedItem = { 2 | id: string 3 | title: string 4 | unread: boolean 5 | starred: boolean 6 | feedId: number 7 | guidHash: string 8 | pubDate: number 9 | url: string 10 | keepUnread: boolean 11 | } 12 | -------------------------------------------------------------------------------- /templates/admin.php: -------------------------------------------------------------------------------- 1 | 4 | // SPDX-Licence-Identifier: AGPL-3.0-or-later 5 | \OCP\Util::addScript('news', 'news-admin-settings'); 6 | ?> 7 | 8 |
9 | -------------------------------------------------------------------------------- /tests/api/helpers/settings.bash: -------------------------------------------------------------------------------- 1 | export user=admin 2 | export NC_FEED="http://localhost:8090/Nextcloud.rss" 3 | export HEISE_FEED="http://localhost:8090/heise.xml" 4 | export BASE_URLv1="http://localhost:8080/index.php/apps/news/api/v1-2" 5 | export NC_HOST="http://localhost:8080" -------------------------------------------------------------------------------- /bin/git/hooks/README.md: -------------------------------------------------------------------------------- 1 | # Hooks 2 | 3 | The following hooks are provided: 4 | 5 | * Update checksums before you commit 6 | 7 | # Installation 8 | Link the files in here into your **.git/hooks** directory: 9 | 10 | ln -s -r bin/git/hooks/pre-commit .git/hooks/ 11 | 12 | -------------------------------------------------------------------------------- /img/facebook.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /tests/command/explore.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "helpers/settings" 4 | 5 | TESTSUITE="Explore" 6 | 7 | @test "[$TESTSUITE] Create new" { 8 | curl --fail "$NC_FEED" 9 | 10 | run ./occ news:generate-explore --votes 100 "$NC_FEED" 11 | [ "$status" -eq 0 ] 12 | } -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import {} }: 2 | pkgs.mkShell { 3 | nativeBuildInputs = with pkgs; [ 4 | gnumake 5 | nodejs 6 | php83 7 | php83Packages.composer 8 | zellij # smart terminal workspace 9 | lazygit # git terminal 10 | ]; 11 | } 12 | -------------------------------------------------------------------------------- /src/types/ApiRoutes.ts: -------------------------------------------------------------------------------- 1 | import { generateUrl } from '@nextcloud/router' 2 | 3 | export const API_ROUTES = { 4 | FOLDER: generateUrl('/apps/news/folders'), 5 | FEED: generateUrl('/apps/news/feeds'), 6 | ITEMS: generateUrl('/apps/news/items'), 7 | FAVICON: generateUrl('/apps/news/favicon'), 8 | } 9 | -------------------------------------------------------------------------------- /tests/test_helper/feeds/Nextcloud.opml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Subscriptions 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.tx/config: -------------------------------------------------------------------------------- 1 | [main] 2 | host = https://www.transifex.com 3 | lang_map = sk_SK: sk, th_TH: th, ja_JP: ja, bg_BG: bg, cs_CZ: cs, fi_FI: fi, hu_HU: hu, nb_NO: nb 4 | 5 | [o:nextcloud:p:nextcloud:r:news] 6 | file_filter = translationfiles//news.po 7 | source_file = translationfiles/templates/news.pot 8 | source_lang = en 9 | type = PO 10 | 11 | -------------------------------------------------------------------------------- /bin/tools/check_feeds.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | feeds=$(cat ./lib/Explore/feeds/feeds.en.json | jq -r .[][0].feed) 4 | 5 | for feed in ${feeds} ; do 6 | if [[ $feed == "http://"* ]]; then 7 | echo "Insecure feed $feed" 8 | exit 1; 9 | fi 10 | if ! curl --fail --silent "$feed" > /dev/null; then 11 | echo "Failed to fetch $feed" 12 | exit 1; 13 | fi 14 | done -------------------------------------------------------------------------------- /lib/Controller/Exceptions/NotLoggedInException.php: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-Licence-Identifier: AGPL-3.0-or-later 3 | 4 | import { translate as t } from '@nextcloud/l10n' 5 | import { createApp } from 'vue' 6 | import AdminSettings from './components/AdminSettings.vue' 7 | 8 | const app = createApp(AdminSettings) 9 | 10 | app.config.globalProperties.t = t 11 | 12 | app.mount('#vue-admin-news') 13 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Bernhard Posselt 2012, 2014 10 | */ 11 | 12 | require_once __DIR__ . '/../../../tests/bootstrap.php'; 13 | require_once __DIR__ . '/../vendor/autoload.php'; 14 | -------------------------------------------------------------------------------- /tests/updater/helpers/settings.bash: -------------------------------------------------------------------------------- 1 | export user=admin 2 | export NC_FEED="http://localhost:8090/Nextcloud.rss" 3 | export HEISE_FEED="http://localhost:8090/heise.xml" 4 | export BASE_URLv1="http://localhost:8080/index.php/apps/news/api/v1-2" 5 | export NC_HOST="http://localhost:8080" 6 | export TEST_FEED="http://localhost:8090/test.xml" 7 | export FEED1="http://localhost:8090/feed1.xml" 8 | export FEED2="http://localhost:8090/feed2.xml" -------------------------------------------------------------------------------- /l10n/an.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Escargar", 3 | "Close" : "Zarrar", 4 | "Folder name" : "Nombre de carpeta", 5 | "Username" : "Nombre d'usuario", 6 | "Password" : "Clau", 7 | "New folder" : "Nueva carpeta", 8 | "Share" : "Compartir", 9 | "Settings" : "Configuración", 10 | "Rename" : "Renombrar", 11 | "Delete" : "Borrar" 12 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 13 | } -------------------------------------------------------------------------------- /lib/Constants.php: -------------------------------------------------------------------------------- 1 | 3 | 4 | ## Summary 5 | 6 | 7 | 8 | ## Checklist 9 | 10 | - Code is [properly formatted](https://nextcloud.github.io/news/developer/#coding-style-guidelines) 11 | - [Sign-off message](https://github.com/src-d/guide/blob/master/developer-community/fix-DCO.md) is added to all commits 12 | - Changelog entry added for all important changes. 13 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/_/nextcloud/ 2 | FROM nextcloud:30-apache 3 | #FROM ghcr.io/pbek/nextcloud-docker-pre-apache:latest 4 | #FROM ghcr.io/digital-blueprint/nextcloud-docker-pre-apache:latest 5 | 6 | COPY entrypoint.sh / 7 | 8 | RUN apt-get update && apt-get install -y sqlite3 9 | RUN deluser www-data 10 | RUN useradd -u 1000 -ms /bin/bash www-data 11 | RUN usermod -a -G www-data www-data 12 | RUN mkdir /var/www/deploy 13 | -------------------------------------------------------------------------------- /l10n/an.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Escargar", 5 | "Close" : "Zarrar", 6 | "Folder name" : "Nombre de carpeta", 7 | "Username" : "Nombre d'usuario", 8 | "Password" : "Clau", 9 | "New folder" : "Nueva carpeta", 10 | "Share" : "Compartir", 11 | "Settings" : "Configuración", 12 | "Rename" : "Renombrar", 13 | "Delete" : "Borrar" 14 | }, 15 | "nplurals=2; plural=(n != 1);"); 16 | -------------------------------------------------------------------------------- /src/utils/unreadCache.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * updates unread cache 3 | * 4 | * @param newItems latest unread items from store 5 | * @param unreadCache cache array to update 6 | */ 7 | export function updateUnreadCache(newItems, unreadCache) { 8 | const cachedItemIds = new Set(unreadCache.map((item) => item.id)) 9 | 10 | for (const item of newItems) { 11 | if (!cachedItemIds.has(item.id)) { 12 | unreadCache.push(item) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "tests/test_helper/bats-support"] 2 | path = tests/test_helper/bats-support 3 | url = https://github.com/bats-core/bats-support.git 4 | [submodule "tests/test_helper/bats-assert"] 5 | path = tests/test_helper/bats-assert 6 | url = https://github.com/bats-core/bats-assert.git 7 | [submodule "tests/test_helper/php-feed-generator"] 8 | path = tests/test_helper/php-feed-generator 9 | url = https://github.com/Grotax/php-feed-generator.git 10 | -------------------------------------------------------------------------------- /templates/index.php: -------------------------------------------------------------------------------- 1 | $fileName) { 8 | style($appName, $fileName); 9 | } 10 | foreach (Plugin::getScripts() as $appName => $fileName) { 11 | script($appName, $fileName); 12 | } 13 | 14 | print_unescaped($this->inc('part.content.warnings')) 15 | 16 | ?> 17 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | trim_trailing_whitespace = true 5 | end_of_line = lf 6 | insert_final_newline = true 7 | 8 | [*.{php,html,css}] 9 | indent_style = space 10 | indent_size = 4 11 | charset = utf-8 12 | 13 | [*.{js,ts,vue}] 14 | indent_style = tab 15 | indent_size = 4 16 | charset = utf-8 17 | 18 | [*.bats] 19 | indent_style = space 20 | indent_size = 2 21 | charset = utf-8 22 | 23 | [Makefile] 24 | indent_style = tab 25 | indent_size = 4 26 | 27 | -------------------------------------------------------------------------------- /.github/workflows/changelog-enforcer.yml: -------------------------------------------------------------------------------- 1 | name: "Changelog Enforcer" 2 | on: 3 | pull_request: 4 | types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled] 5 | 6 | jobs: 7 | # Enforces the update of a changelog file on every pull request 8 | changelog: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: dangoslen/changelog-enforcer@v3 12 | with: 13 | changeLogPath: 'CHANGELOG.md' 14 | skipLabels: 'Skip-Changelog' 15 | -------------------------------------------------------------------------------- /tests/command/update.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | # This only works with NC 26 4 | 5 | load "helpers/settings" 6 | load "../test_helper/bats-support/load" 7 | load "../test_helper/bats-assert/load" 8 | 9 | TESTSUITE="Update" 10 | 11 | @test "[$TESTSUITE] Job status" { 12 | run ./occ news:updater:job 13 | 14 | [ "$status" -eq 0 -o "$status" -eq 2 ] 15 | } 16 | 17 | @test "[$TESTSUITE] Job reset" { 18 | run ./occ news:updater:job --reset 19 | 20 | assert_success 21 | } -------------------------------------------------------------------------------- /.github/workflows/lint-text.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | typos: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6.0.1 14 | 15 | - name: typos-action 16 | uses: crate-ci/typos@master 17 | 18 | vale: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v6.0.1 22 | 23 | - uses: errata-ai/vale-action@reviewdog 24 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to the Nextcloud News APP documentation, it contains information for users, administrators and developers. News is an APP for Nextcloud that can be installed from the official [APP Store](https://apps.nextcloud.com/apps/news). 4 | 5 | News offers the user an RSS/Atom feed reader and can be used to subscribe to multiple feeds, which get automatically updated in the background. Additionally, news offers a REST-API that allows clients to synchronize with News. -------------------------------------------------------------------------------- /src/enums/index.ts: -------------------------------------------------------------------------------- 1 | export enum DISPLAY_MODE { 2 | DEFAULT = '0', 3 | COMPACT = '1', 4 | SCREENREADER = '2', 5 | } 6 | 7 | export enum FEED_ORDER { 8 | DEFAULT = 0, 9 | OLDEST = 1, 10 | NEWEST = 2, 11 | } 12 | 13 | export enum FEED_UPDATE_MODE { 14 | UNREAD = 0, 15 | IGNORE = 1, 16 | } 17 | 18 | export enum SPLIT_MODE { 19 | VERTICAL = '0', 20 | HORIZONTAL = '1', 21 | OFF = '2', 22 | } 23 | 24 | export enum ITEM_HEIGHT { 25 | DEFAULT = '111', 26 | COMPACT = '44', 27 | } 28 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | parameters: 2 | inferPrivatePropertyTypeFromConstructor: true 3 | treatPhpDocTypesAsCertain: false 4 | reportUnmatchedIgnoredErrors: false 5 | bootstrapFiles: 6 | - %currentWorkingDirectory%/../../lib/base.php 7 | excludePaths: 8 | - %currentWorkingDirectory%/lib/Migration/*.php 9 | - %currentWorkingDirectory%/lib/Vendor/* 10 | - %currentWorkingDirectory%/lib/AppInfo/Application.php 11 | - %currentWorkingDirectory%/lib/Fetcher/Client/Legacy*.php 12 | -------------------------------------------------------------------------------- /src/types/Feed.ts: -------------------------------------------------------------------------------- 1 | import type { FEED_ORDER, FEED_UPDATE_MODE } from '../enums/index.ts' 2 | 3 | export type Feed = { 4 | folderId?: number 5 | unreadCount: number 6 | url: string 7 | title?: string 8 | autoDiscover?: boolean 9 | faviconLink?: string 10 | id?: number 11 | pinned: boolean 12 | preventUpdate: boolean 13 | ordering: FEED_ORDER 14 | fullTextEnabled: boolean 15 | updateMode: FEED_UPDATE_MODE 16 | updateErrorCount: number 17 | lastUpdateError: string 18 | location: string 19 | } 20 | -------------------------------------------------------------------------------- /l10n/hy.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Բեռնել", 3 | "Close" : "Փակել", 4 | "Folder name" : "Պանակի անուն", 5 | "Username" : "Օգտանուն", 6 | "Password" : "Գաղտնաբառ", 7 | "New folder" : "Նոր պանակ", 8 | "Credentials" : "Credentials", 9 | "Move" : "Տեղափոխել", 10 | "Share" : "Կիսվել", 11 | "Settings" : "Կարգավորումներ", 12 | "Rename" : "Վերանվանել", 13 | "Delete" : "հեռացնել", 14 | "Folder" : "Պանակ", 15 | "Refresh" : "Թարմացնել" 16 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 17 | } -------------------------------------------------------------------------------- /l10n/hy.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Բեռնել", 5 | "Close" : "Փակել", 6 | "Folder name" : "Պանակի անուն", 7 | "Username" : "Օգտանուն", 8 | "Password" : "Գաղտնաբառ", 9 | "New folder" : "Նոր պանակ", 10 | "Credentials" : "Credentials", 11 | "Move" : "Տեղափոխել", 12 | "Share" : "Կիսվել", 13 | "Settings" : "Կարգավորումներ", 14 | "Rename" : "Վերանվանել", 15 | "Delete" : "հեռացնել", 16 | "Folder" : "Պանակ", 17 | "Refresh" : "Թարմացնել" 18 | }, 19 | "nplurals=2; plural=(n != 1);"); 20 | -------------------------------------------------------------------------------- /.devcontainer/README.md: -------------------------------------------------------------------------------- 1 | # DevContainer 2 | 3 | ## Image Used in This Project 4 | 5 | This project uses a Nextcloud Docker image for the development container. The image is defined in the `.devcontainer.json` configuration file. For more details about the image and its configuration, you can refer to the [GitHub repository](https://github.com/juliusknorr/nextcloud-dev). 6 | 7 | ## Visual Studio Code 8 | 9 | For information on how to configure and use Dev Containers, please visit the [official documentation](https://code.visualstudio.com/docs/remote/containers). 10 | -------------------------------------------------------------------------------- /l10n/ms_MY.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Muat turun", 3 | "Close" : "Tutup", 4 | "Web address" : "Alamat sesawang", 5 | "Username" : "Nama pengguna", 6 | "Password" : "Kata laluan", 7 | "Credentials" : "Credentials", 8 | "Move" : "Move", 9 | "Share" : "Kongsi", 10 | "Settings" : "Tetapan", 11 | "Documentation" : "Dokumentasi", 12 | "Rename" : "Namakan", 13 | "Delete" : "Padam", 14 | "by" : "oleh", 15 | "Folder" : "Folder", 16 | "Refresh" : "Refresh" 17 | },"pluralForm" :"nplurals=1; plural=0;" 18 | } -------------------------------------------------------------------------------- /docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | name: nextcloud-news 2 | 3 | services: 4 | app: 5 | container_name: nextcloud-news-app 6 | build: . 7 | ports: 8 | - 8081:80 9 | environment: 10 | - NEXTCLOUD_ADMIN_USER=admin 11 | - NEXTCLOUD_ADMIN_PASSWORD=admin 12 | - SQLITE_DATABASE=mydb 13 | - NEXTCLOUD_TRUSTED_DOMAINS=localhost 127.0.0.1 14 | volumes: 15 | - nextcloud:/var/www/html 16 | - ..:/var/www/html/custom_apps/news 17 | - ./news.config.php:/var/www/html/config/news.config.php 18 | 19 | volumes: 20 | nextcloud: 21 | -------------------------------------------------------------------------------- /l10n/kn.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "ಪ್ರತಿಯನ್ನು ಸ್ಥಳೀಯವಾಗಿ ಉಳಿಸಿಕೊಳ್ಳಿ", 3 | "Close" : "ಮುಚ್ಚು", 4 | "Username" : "ಬಳಕೆಯ ಹೆಸರು", 5 | "Password" : "ಗುಪ್ತಪದ", 6 | "New folder" : "ಹೊಸ ಕೋಶ", 7 | "Credentials" : "ರುಜುವಾತುಗಳು", 8 | "Move" : "Move", 9 | "Share" : "ಹಂಚಿಕೊಳ್ಳಿ", 10 | "Settings" : "ಆಯ್ಕೆ", 11 | "Documentation" : "ದಸ್ತಾವೇಜು", 12 | "Rename" : "ಮರುಹೆಸರಿಸು", 13 | "Delete" : "ಅಳಿಸಿ", 14 | "Folder" : "ಕೋಶ", 15 | "right" : "ಬಲ", 16 | "left" : "ಎಡ" 17 | },"pluralForm" :"nplurals=2; plural=(n > 1);" 18 | } -------------------------------------------------------------------------------- /l10n/ms_MY.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Muat turun", 5 | "Close" : "Tutup", 6 | "Web address" : "Alamat sesawang", 7 | "Username" : "Nama pengguna", 8 | "Password" : "Kata laluan", 9 | "Credentials" : "Credentials", 10 | "Move" : "Move", 11 | "Share" : "Kongsi", 12 | "Settings" : "Tetapan", 13 | "Documentation" : "Dokumentasi", 14 | "Rename" : "Namakan", 15 | "Delete" : "Padam", 16 | "by" : "oleh", 17 | "Folder" : "Folder", 18 | "Refresh" : "Refresh" 19 | }, 20 | "nplurals=1; plural=0;"); 21 | -------------------------------------------------------------------------------- /img/app.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /l10n/kn.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "ಪ್ರತಿಯನ್ನು ಸ್ಥಳೀಯವಾಗಿ ಉಳಿಸಿಕೊಳ್ಳಿ", 5 | "Close" : "ಮುಚ್ಚು", 6 | "Username" : "ಬಳಕೆಯ ಹೆಸರು", 7 | "Password" : "ಗುಪ್ತಪದ", 8 | "New folder" : "ಹೊಸ ಕೋಶ", 9 | "Credentials" : "ರುಜುವಾತುಗಳು", 10 | "Move" : "Move", 11 | "Share" : "ಹಂಚಿಕೊಳ್ಳಿ", 12 | "Settings" : "ಆಯ್ಕೆ", 13 | "Documentation" : "ದಸ್ತಾವೇಜು", 14 | "Rename" : "ಮರುಹೆಸರಿಸು", 15 | "Delete" : "ಅಳಿಸಿ", 16 | "Folder" : "ಕೋಶ", 17 | "right" : "ಬಲ", 18 | "left" : "ಎಡ" 19 | }, 20 | "nplurals=2; plural=(n > 1);"); 21 | -------------------------------------------------------------------------------- /lib/Explore/Exceptions/RecommendedSiteNotFoundException.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Explore\Exceptions; 15 | 16 | use Exception; 17 | 18 | class RecommendedSiteNotFoundException extends Exception 19 | { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main-cron-warning.js: -------------------------------------------------------------------------------- 1 | // Send error to Vuex State for displaying in Vue Application 2 | window.store.commit('SET_ERROR', { 3 | toString: () => t('news', 'Ajax or webcron mode detected! Your feeds will not be updated!'), 4 | links: [ 5 | { 6 | url: 'https://docs.nextcloud.org/server/latest/admin_manual/configuration_server/background_jobs_configuration.html#cron', 7 | text: t('news', 'How to set up the operating system cron'), 8 | }, { 9 | url: 'https://github.com/nextcloud/news-updater', 10 | text: t('news', 'Install and set up a faster parallel updater that uses the News app\'s update API'), 11 | }, 12 | ], 13 | }) 14 | -------------------------------------------------------------------------------- /l10n/tk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "12", 3 | "Close" : "Ýap", 4 | "Folder name" : "Papkanyň ady", 5 | "Username" : "Ulanyjy ady", 6 | "Password" : "Açarsöz", 7 | "New folder" : "Täze papka döretmek", 8 | "Move" : "Göçüriň", 9 | "Share" : "Paýlaş", 10 | "Default" : "Bellenen", 11 | "Settings" : "Sazlamalar", 12 | "Rename" : "Adyny üýtgetmek", 13 | "Delete" : "Pozmak", 14 | "Newest first" : "Täze ilkinji", 15 | "Oldest first" : "Ilki bilen iň köne", 16 | "Not available" : "Elýeterli däl", 17 | "Refresh" : "Täzelemek" 18 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 19 | } -------------------------------------------------------------------------------- /lib/Db/IAPI.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @author Paul Tirk 11 | * @copyright 2012 Alessandro Cosentino 12 | * @copyright 2012-2014 Bernhard Posselt 13 | * @copyright 2020 Paul Tirk 14 | */ 15 | 16 | namespace OCA\News\Db; 17 | 18 | interface IAPI 19 | { 20 | public function toAPI(); 21 | public function toAPI2(bool $reduced = false): array; 22 | } 23 | -------------------------------------------------------------------------------- /l10n/tk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "12", 5 | "Close" : "Ýap", 6 | "Folder name" : "Papkanyň ady", 7 | "Username" : "Ulanyjy ady", 8 | "Password" : "Açarsöz", 9 | "New folder" : "Täze papka döretmek", 10 | "Move" : "Göçüriň", 11 | "Share" : "Paýlaş", 12 | "Default" : "Bellenen", 13 | "Settings" : "Sazlamalar", 14 | "Rename" : "Adyny üýtgetmek", 15 | "Delete" : "Pozmak", 16 | "Newest first" : "Täze ilkinji", 17 | "Oldest first" : "Ilki bilen iň köne", 18 | "Not available" : "Elýeterli däl", 19 | "Refresh" : "Täzelemek" 20 | }, 21 | "nplurals=2; plural=(n != 1);"); 22 | -------------------------------------------------------------------------------- /l10n/gd.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Luchdaich a-nuas", 3 | "Close" : "Dùin", 4 | "Folder name" : "Ainm a’ phasgain", 5 | "Username" : "Ainm-cleachdaiche", 6 | "Password" : "Facal-faire", 7 | "New folder" : "Pasgan ùr", 8 | "Move" : "Gluais", 9 | "Share" : "Co-roinn", 10 | "Default" : "Bunaiteach", 11 | "Settings" : "Roghainnean", 12 | "Rename" : "Thoir ainm ùr air", 13 | "Delete" : "Sguab às", 14 | "Newest first" : "As ùire an toiseach", 15 | "Oldest first" : "As sine an toiseach", 16 | "Refresh" : "Ath-nuadhaich" 17 | },"pluralForm" :"nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;" 18 | } -------------------------------------------------------------------------------- /src/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript Development 2 | JavaScript is built and minified using webpack. 3 | 4 | Therefore you need to install **Node.js 6+ and npm**. 5 | 6 | Then install the local dependencies by running: 7 | 8 | npm install 9 | 10 | ## Tasks 11 | The following tasks are available: 12 | 13 | * **Build the JavaScript**: 14 | 15 | npm run build 16 | 17 | * **Build the JavaScript in Dev Mode**: 18 | 19 | npm run build 20 | 21 | * **Watch for changes and build JavaScript**: 22 | 23 | npm run watch 24 | 25 | * **Run JavaScript unit tests**: 26 | 27 | npm run karma 28 | 29 | * **Watch for changes and run JavaScript unit tests**: 30 | 31 | npm run watch-karma 32 | -------------------------------------------------------------------------------- /bin/tools/file_from_env.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 10 | * @copyright Benjamin Brahmer 2020 11 | */ 12 | 13 | if ($argc < 2) { 14 | echo "This script expects two parameters:\n"; 15 | echo "./file_from_env.php ENV_VAR PATH_TO_FILE\n"; 16 | exit(1); 17 | } 18 | 19 | # Read environment variable 20 | $content = getenv($argv[1]); 21 | 22 | if (!$content){ 23 | echo "Variable was empty\n"; 24 | exit(1); 25 | } 26 | 27 | file_put_contents($argv[2], $content); 28 | 29 | echo "Done...\n"; -------------------------------------------------------------------------------- /l10n/gd.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Luchdaich a-nuas", 5 | "Close" : "Dùin", 6 | "Folder name" : "Ainm a’ phasgain", 7 | "Username" : "Ainm-cleachdaiche", 8 | "Password" : "Facal-faire", 9 | "New folder" : "Pasgan ùr", 10 | "Move" : "Gluais", 11 | "Share" : "Co-roinn", 12 | "Default" : "Bunaiteach", 13 | "Settings" : "Roghainnean", 14 | "Rename" : "Thoir ainm ùr air", 15 | "Delete" : "Sguab às", 16 | "Newest first" : "As ùire an toiseach", 17 | "Oldest first" : "As sine an toiseach", 18 | "Refresh" : "Ath-nuadhaich" 19 | }, 20 | "nplurals=4; plural=(n==1 || n==11) ? 0 : (n==2 || n==12) ? 1 : (n > 2 && n < 20) ? 2 : 3;"); 21 | -------------------------------------------------------------------------------- /l10n/ta.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "பதிவிறக்குக", 3 | "Close" : "மூடுக", 4 | "Web address" : "வலைய முகவரி", 5 | "Folder name" : "கோப்புறை பெயர்", 6 | "Username" : "பயனாளர் பெயர்", 7 | "Password" : "கடவுச்சொல்", 8 | "Credentials" : "சான்று ஆவணங்கள்", 9 | "Move" : "Move", 10 | "Share" : "பகிர்வு", 11 | "Settings" : "அமைப்புகள்", 12 | "Documentation" : "ஆவணமாக்கல்", 13 | "Keyboard shortcuts" : "விசைப்பலகை குறுக்குவழிகள்", 14 | "Rename" : "பெயர்மாற்றம்", 15 | "Delete" : "நீக்குக", 16 | "by" : "மூலம்", 17 | "Title" : "தலைப்பு", 18 | "Folder" : "கோப்புறை", 19 | "Refresh" : "மீள் ஏற்றுக" 20 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 21 | } -------------------------------------------------------------------------------- /tests/javascript/unit/components/routes/Explore.spec.ts: -------------------------------------------------------------------------------- 1 | import axios from '@nextcloud/axios' 2 | import { shallowMount } from '@vue/test-utils' 3 | import { describe, expect, it } from 'vitest' 4 | import Explore from '../../../../../src/components/routes/Explore.vue' 5 | 6 | describe('Explore.vue', () => { 7 | 'use strict' 8 | 9 | it('should initialize without showing AddFeed Component', () => { 10 | axios.get.mockResolvedValue({ data: { } }) 11 | 12 | const wrapper = shallowMount(Explore, { 13 | mocks: { 14 | $store: { 15 | state: { 16 | feeds: [], 17 | folders: [], 18 | }, 19 | }, 20 | }, 21 | }) 22 | 23 | expect(wrapper.vm.$data.showAddFeed).toBeFalsy() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /l10n/bs.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Preuzmite", 3 | "Close" : "Zatvori", 4 | "Username" : "Korisničko ime", 5 | "Password" : "Lozinka", 6 | "New folder" : "Nova fascikla", 7 | "Credentials" : "Vjerodajnice", 8 | "Move" : "Move", 9 | "Share" : "Podjeli", 10 | "Starred" : "Označeno", 11 | "Settings" : "Postavke", 12 | "Documentation" : "Dokumentacija", 13 | "Keyboard shortcuts" : "Tipkovni prečaci", 14 | "Rename" : "Preimenuj", 15 | "Delete" : "Obriši", 16 | "by" : "od strane", 17 | "Title" : "Naslov", 18 | "Folder" : "Fasikla" 19 | },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" 20 | } -------------------------------------------------------------------------------- /l10n/ta.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "பதிவிறக்குக", 5 | "Close" : "மூடுக", 6 | "Web address" : "வலைய முகவரி", 7 | "Folder name" : "கோப்புறை பெயர்", 8 | "Username" : "பயனாளர் பெயர்", 9 | "Password" : "கடவுச்சொல்", 10 | "Credentials" : "சான்று ஆவணங்கள்", 11 | "Move" : "Move", 12 | "Share" : "பகிர்வு", 13 | "Settings" : "அமைப்புகள்", 14 | "Documentation" : "ஆவணமாக்கல்", 15 | "Keyboard shortcuts" : "விசைப்பலகை குறுக்குவழிகள்", 16 | "Rename" : "பெயர்மாற்றம்", 17 | "Delete" : "நீக்குக", 18 | "by" : "மூலம்", 19 | "Title" : "தலைப்பு", 20 | "Folder" : "கோப்புறை", 21 | "Refresh" : "மீள் ஏற்றுக" 22 | }, 23 | "nplurals=2; plural=(n != 1);"); 24 | -------------------------------------------------------------------------------- /lib/Utility/Time.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2016 Bernhard Posselt 10 | */ 11 | 12 | namespace OCA\News\Utility; 13 | 14 | class Time 15 | { 16 | public function getTime(): int 17 | { 18 | return time(); 19 | } 20 | 21 | /** 22 | * @return string the current unix time in milliseconds 23 | */ 24 | public function getMicroTime(): string 25 | { 26 | list($millisecs, $secs) = explode(" ", microtime()); 27 | return $secs . substr($millisecs, 2, 6); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /l10n/bs.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Preuzmite", 5 | "Close" : "Zatvori", 6 | "Username" : "Korisničko ime", 7 | "Password" : "Lozinka", 8 | "New folder" : "Nova fascikla", 9 | "Credentials" : "Vjerodajnice", 10 | "Move" : "Move", 11 | "Share" : "Podjeli", 12 | "Starred" : "Označeno", 13 | "Settings" : "Postavke", 14 | "Documentation" : "Dokumentacija", 15 | "Keyboard shortcuts" : "Tipkovni prečaci", 16 | "Rename" : "Preimenuj", 17 | "Delete" : "Obriši", 18 | "by" : "od strane", 19 | "Title" : "Naslov", 20 | "Folder" : "Fasikla" 21 | }, 22 | "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); 23 | -------------------------------------------------------------------------------- /l10n/kab.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Sider", 3 | "Close" : "Mdel", 4 | "Folder name" : "Isem n ukaram", 5 | "Username" : "Isem n useqdac", 6 | "Password" : "Awal uffir", 7 | "New folder" : "Akaram amaynut", 8 | "Move" : "Senkez", 9 | "Share" : "Bḍu", 10 | "Default" : "Prédéfini(e)", 11 | "Off" : "Ur irmid ara", 12 | "Settings" : "Iɣewwaṛen", 13 | "Keyboard shortcuts" : "Inegzumen n unasiw", 14 | "Rename" : "Beddel isem", 15 | "Delete" : "Kkes", 16 | "Newest first" : "Imaynuten d imezwura", 17 | "Oldest first" : "Iqbuṛen d imezwura", 18 | "Title" : "Azwel", 19 | "Folder" : "Akaram", 20 | "Refresh" : "Sismeḍ" 21 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 22 | } -------------------------------------------------------------------------------- /.devcontainer/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors 4 | # SPDX-License-Identifier: CC0-1.0 5 | 6 | # better prompt 7 | echo 'export PS1="\[\e[32m\]root@devcontainer\[\e[0m\]:\[\e[34m\]\W\[\e[0m\]# "' >> ~/.bashrc 8 | 9 | # Map NEXTCLOUD_VERSION to SERVER_BRANCH for the bootstrap script 10 | if [ -n "$NEXTCLOUD_VERSION" ]; then 11 | export SERVER_BRANCH="stable${NEXTCLOUD_VERSION}" 12 | echo "Setting SERVER_BRANCH to: $SERVER_BRANCH" 13 | fi 14 | 15 | # Show what we're using 16 | echo "Environment variables:" 17 | env | grep -E "(NEXTCLOUD|SERVER_BRANCH)" | sort 18 | 19 | ( 20 | cd /tmp && /usr/local/bin/bootstrap.sh apache2ctl start 21 | ) 22 | 23 | make composer 24 | make npm -------------------------------------------------------------------------------- /img/rss.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /l10n/km.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "អត្ថបទ​ដែល​មិន​ទាន់​អាន", 3 | "Download" : "ទាញយក", 4 | "Close" : "បិទ", 5 | "Folder name" : "ឈ្មោះ​ថត", 6 | "Username" : "ឈ្មោះ​អ្នកប្រើ", 7 | "Password" : "ពាក្យសម្ងាត់", 8 | "New folder" : "ថត​ថ្មី", 9 | "Credentials" : "Credentials", 10 | "Move" : "Move", 11 | "Share" : "ចែក​រំលែក", 12 | "All articles" : "អត្ថបទ​ទាំង​អស់", 13 | "Starred" : "បាន​ដាក់​ផ្កាយ", 14 | "Settings" : "ការកំណត់", 15 | "Documentation" : "កម្រង​ឯកសារ", 16 | "Rename" : "ប្ដូរ​ឈ្មោះ", 17 | "Delete" : "លុប", 18 | "by" : "ដោយ", 19 | "Folder" : "ថត", 20 | "right" : "ខាង​ស្ដាំ", 21 | "left" : "ខាង​ឆ្វេង" 22 | },"pluralForm" :"nplurals=1; plural=0;" 23 | } -------------------------------------------------------------------------------- /.github/no-response.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-no-response - https://github.com/probot/no-response 2 | 3 | # Number of days of inactivity before an Issue is closed for lack of response 4 | daysUntilClose: 14 5 | # Label requiring a response 6 | responseRequiredLabel: needs info 7 | # Comment to post when closing an Issue for lack of response. Set to `false` to disable 8 | closeComment: > 9 | This issue has been automatically closed because there has been no response 10 | to our request for more information from the original author. With only the 11 | information that is currently in the issue, we don't have enough information 12 | to take action. Please reach out if you have or find the answers we need so 13 | that we can investigate further. 14 | -------------------------------------------------------------------------------- /lib/Db/ListType.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Db; 15 | 16 | /** 17 | * Enum FeedType 18 | * 19 | * @package OCA\News\Db 20 | */ 21 | class ListType 22 | { 23 | const FEED = 0; 24 | const FOLDER = 1; 25 | const STARRED = 2; 26 | const ALL_ITEMS = 3; 27 | const SHARED = 4; 28 | const EXPLORE = 5; 29 | const UNREAD = 6; 30 | } 31 | -------------------------------------------------------------------------------- /l10n/kab.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Sider", 5 | "Close" : "Mdel", 6 | "Folder name" : "Isem n ukaram", 7 | "Username" : "Isem n useqdac", 8 | "Password" : "Awal uffir", 9 | "New folder" : "Akaram amaynut", 10 | "Move" : "Senkez", 11 | "Share" : "Bḍu", 12 | "Default" : "Prédéfini(e)", 13 | "Off" : "Ur irmid ara", 14 | "Settings" : "Iɣewwaṛen", 15 | "Keyboard shortcuts" : "Inegzumen n unasiw", 16 | "Rename" : "Beddel isem", 17 | "Delete" : "Kkes", 18 | "Newest first" : "Imaynuten d imezwura", 19 | "Oldest first" : "Iqbuṛen d imezwura", 20 | "Title" : "Azwel", 21 | "Folder" : "Akaram", 22 | "Refresh" : "Sismeḍ" 23 | }, 24 | "nplurals=2; plural=(n != 1);"); 25 | -------------------------------------------------------------------------------- /l10n/km.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "អត្ថបទ​ដែល​មិន​ទាន់​អាន", 5 | "Download" : "ទាញយក", 6 | "Close" : "បិទ", 7 | "Folder name" : "ឈ្មោះ​ថត", 8 | "Username" : "ឈ្មោះ​អ្នកប្រើ", 9 | "Password" : "ពាក្យសម្ងាត់", 10 | "New folder" : "ថត​ថ្មី", 11 | "Credentials" : "Credentials", 12 | "Move" : "Move", 13 | "Share" : "ចែក​រំលែក", 14 | "All articles" : "អត្ថបទ​ទាំង​អស់", 15 | "Starred" : "បាន​ដាក់​ផ្កាយ", 16 | "Settings" : "ការកំណត់", 17 | "Documentation" : "កម្រង​ឯកសារ", 18 | "Rename" : "ប្ដូរ​ឈ្មោះ", 19 | "Delete" : "លុប", 20 | "by" : "ដោយ", 21 | "Folder" : "ថត", 22 | "right" : "ខាង​ស្ដាំ", 23 | "left" : "ខាង​ឆ្វេង" 24 | }, 25 | "nplurals=1; plural=0;"); 26 | -------------------------------------------------------------------------------- /l10n/sr@latin.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Preuzmi", 3 | "Close" : "Zatvori", 4 | "Folder name" : "Naziv fascikle", 5 | "Username" : "Username", 6 | "Password" : "Password", 7 | "New folder" : "Nova fascikla", 8 | "Credentials" : "Credentials", 9 | "Move" : "Premesti", 10 | "Share" : "Podeli", 11 | "Starred" : "Ozvezdano", 12 | "Settings" : "Postavke", 13 | "Documentation" : "Dokumentacija", 14 | "Rename" : "Rename", 15 | "Delete" : "Delete", 16 | "Rename Feed" : "Preimenuj dovod", 17 | "Newest first" : "Prvo novije", 18 | "Oldest first" : "prvo starije", 19 | "Refresh" : "Osveži" 20 | },"pluralForm" :"nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);" 21 | } -------------------------------------------------------------------------------- /tests/javascript/unit/setup.ts: -------------------------------------------------------------------------------- 1 | import { config } from '@vue/test-utils' 2 | import { vi } from 'vitest' 3 | 4 | global.appName = 'news' 5 | global.appVersion = '1.0.0' 6 | 7 | global.OC = { 8 | getLanguage() { 9 | return 'en-US' 10 | }, 11 | } 12 | 13 | // Mock nextcloud translate functions 14 | config.global.mocks.$t = function(_app: any, string: any) { 15 | return string 16 | } 17 | config.global.mocks.t = config.global.mocks.$t 18 | global.t = config.global.mocks.$t 19 | 20 | config.global.mocks.$n = function(app: any, singular: any) { 21 | return singular 22 | } 23 | config.global.mocks.n = config.global.mocks.$n 24 | 25 | // Mock nextcloud helpers 26 | vi.mock('@nextcloud/axios') 27 | vi.mock('@nextcloud/capabilities', () => ({ 28 | getCapabilities: vi.fn(() => ({})), 29 | })) 30 | -------------------------------------------------------------------------------- /docs/features/customCSS.md: -------------------------------------------------------------------------------- 1 | # Custom CSS 2 | 3 | Sometimes you want to add additional CSS for a feed to improve the rendering. This can very easily be done by adding a CSS class to **css/custom.css** following the following naming convention: 4 | 5 | * Take the URL from the \ attribute (e.g.: \ \) 6 | * Extract the Domain from the URL (e.g.: ) 7 | * Strip the leading **www.** (e.g.: google.de) 8 | * Replace all . with - (e.g.: google-de) 9 | * Prepend **custom-** (e.g.: custom-google-de) 10 | 11 | Each class rule should be prefixed with **#app-content** and should only affect the article body. An example rule would be: 12 | 13 | ```css 14 | #app-content .custom-google-de .body { 15 | /* Custom CSS rules here */ 16 | } 17 | ``` 18 | -------------------------------------------------------------------------------- /l10n/si.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "News" : "පුවත්", 3 | "Download" : "බාගන්න", 4 | "Close" : "වසන්න", 5 | "Web address" : "වියමන ලිපිනය", 6 | "Folder name" : "බහාලුමේ නම", 7 | "Username" : "පරිශීලක නාමය", 8 | "Password" : "මුර පදය", 9 | "New folder" : "නව බහාලුම", 10 | "Folder exists already!" : "බහාලුම දැනටමත් පවතී!", 11 | "Update interval" : "යාවත්කාලීන කාල පරතරය", 12 | "Share" : "බෙදාගන්න", 13 | "Settings" : "සැකසුම්", 14 | "Rename" : "නැවත නම් කරන්න", 15 | "Open website" : "වියමන අඩවිය විවෘත කරන්න", 16 | "Folder" : "බහාලුම", 17 | "right" : "දකුණ", 18 | "left" : "වම", 19 | "Load previous folder" : "පෙර බහාලුම පූරණය කරන්න", 20 | "Load next folder" : "ඊළඟ බහාලුම පූරණය කරන්න" 21 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 22 | } -------------------------------------------------------------------------------- /l10n/sr@latin.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Preuzmi", 5 | "Close" : "Zatvori", 6 | "Folder name" : "Naziv fascikle", 7 | "Username" : "Username", 8 | "Password" : "Password", 9 | "New folder" : "Nova fascikla", 10 | "Credentials" : "Credentials", 11 | "Move" : "Premesti", 12 | "Share" : "Podeli", 13 | "Starred" : "Ozvezdano", 14 | "Settings" : "Postavke", 15 | "Documentation" : "Dokumentacija", 16 | "Rename" : "Rename", 17 | "Delete" : "Delete", 18 | "Rename Feed" : "Preimenuj dovod", 19 | "Newest first" : "Prvo novije", 20 | "Oldest first" : "prvo starije", 21 | "Refresh" : "Osveži" 22 | }, 23 | "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"); 24 | -------------------------------------------------------------------------------- /.github/workflows/documentation.yml: -------------------------------------------------------------------------------- 1 | name: Publish docs via GitHub Pages 2 | on: 3 | push: 4 | branches: 5 | - master 6 | paths: 7 | - 'docs/**' 8 | 9 | jobs: 10 | build: 11 | name: Deploy docs 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout master 15 | uses: actions/checkout@v6.0.1 16 | 17 | - name: Deploy docs 18 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 19 | # Or use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to build without the mkdocs-material theme 20 | env: 21 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 22 | #CUSTOM_DOMAIN: optionaldomain.com 23 | #CONFIG_FILE: folder/mkdocs.yml 24 | #EXTRA_PACKAGES: build-base 25 | # GITHUB_DOMAIN: github.myenterprise.com -------------------------------------------------------------------------------- /l10n/si.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "News" : "පුවත්", 5 | "Download" : "බාගන්න", 6 | "Close" : "වසන්න", 7 | "Web address" : "වියමන ලිපිනය", 8 | "Folder name" : "බහාලුමේ නම", 9 | "Username" : "පරිශීලක නාමය", 10 | "Password" : "මුර පදය", 11 | "New folder" : "නව බහාලුම", 12 | "Folder exists already!" : "බහාලුම දැනටමත් පවතී!", 13 | "Update interval" : "යාවත්කාලීන කාල පරතරය", 14 | "Share" : "බෙදාගන්න", 15 | "Settings" : "සැකසුම්", 16 | "Rename" : "නැවත නම් කරන්න", 17 | "Open website" : "වියමන අඩවිය විවෘත කරන්න", 18 | "Folder" : "බහාලුම", 19 | "right" : "දකුණ", 20 | "left" : "වම", 21 | "Load previous folder" : "පෙර බහාලුම පූරණය කරන්න", 22 | "Load next folder" : "ඊළඟ බහාලුම පූරණය කරන්න" 23 | }, 24 | "nplurals=2; plural=(n != 1);"); 25 | -------------------------------------------------------------------------------- /l10n/uz.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Yuklab olish", 3 | "Close" : "Yopish", 4 | "Folder name" : "Folder name", 5 | "Username" : "Username", 6 | "Password" : "Password", 7 | "New folder" : "New folder", 8 | "Credentials" : "Hisob ma'lumotlari", 9 | "Subscribe" : "Obuna boʻlish", 10 | "Move" : "Move", 11 | "Share" : "Ulashish", 12 | "Default" : "Standart", 13 | "Off" : "Oʻchirilgan", 14 | "Settings" : "Sozlamalar", 15 | "Documentation" : "Hujjatlar", 16 | "Keyboard shortcuts" : "Klaviatura yorliqlari", 17 | "Rename" : "Nomini o'zgartirish", 18 | "Delete" : "O'chirish", 19 | "Title" : "Sarlavha", 20 | "Not available" : "Mavjud emas", 21 | "right" : "right", 22 | "left" : "left", 23 | "Refresh" : "Refresh" 24 | },"pluralForm" :"nplurals=1; plural=0;" 25 | } -------------------------------------------------------------------------------- /src/components/routes/Recent.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 30 | -------------------------------------------------------------------------------- /.github/stale.yml: -------------------------------------------------------------------------------- 1 | # Configuration for probot-stale - https://github.com/probot/stale 2 | 3 | # Number of days of inactivity before an Issue or Pull Request becomes stale 4 | daysUntilStale: 21 5 | # Number of days of inactivity before a stale Issue or Pull Request is closed 6 | daysUntilClose: 7 7 | # Issues or Pull Requests with these labels will never be considered stale 8 | exemptLabels: 9 | - 0. Needs triage 10 | - 1. to develop 11 | - 2. developing 12 | - 3. to review 13 | - 4. to release 14 | - help wanted 15 | - in progress 16 | # Label to use when marking as stale 17 | staleLabel: stale 18 | # Comment to post when marking as stale. Set to `false` to disable 19 | markComment: > 20 | This issue has been automatically marked as stale because it has not had 21 | recent activity. It will be closed if no further activity occurs. 22 | -------------------------------------------------------------------------------- /l10n/uz.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Yuklab olish", 5 | "Close" : "Yopish", 6 | "Folder name" : "Folder name", 7 | "Username" : "Username", 8 | "Password" : "Password", 9 | "New folder" : "New folder", 10 | "Credentials" : "Hisob ma'lumotlari", 11 | "Subscribe" : "Obuna boʻlish", 12 | "Move" : "Move", 13 | "Share" : "Ulashish", 14 | "Default" : "Standart", 15 | "Off" : "Oʻchirilgan", 16 | "Settings" : "Sozlamalar", 17 | "Documentation" : "Hujjatlar", 18 | "Keyboard shortcuts" : "Klaviatura yorliqlari", 19 | "Rename" : "Nomini o'zgartirish", 20 | "Delete" : "O'chirish", 21 | "Title" : "Sarlavha", 22 | "Not available" : "Mavjud emas", 23 | "right" : "right", 24 | "left" : "left", 25 | "Refresh" : "Refresh" 26 | }, 27 | "nplurals=1; plural=0;"); 28 | -------------------------------------------------------------------------------- /l10n/az.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "Oxunulmamış məqalə", 3 | "Download" : "Yüklə", 4 | "Close" : "Bağla", 5 | "Folder name" : "Qovluq adı", 6 | "Username" : "İstifadəçi adı", 7 | "Password" : "Şifrə", 8 | "New folder" : "Yeni qovluq", 9 | "Credentials" : "Səlahiyyətlər", 10 | "Subscribe" : "Abunə", 11 | "Last job ran {relativeTime}." : "Son işin icra vaxtı {relativeTime}.", 12 | "Move" : "Move", 13 | "Share" : "Paylaş", 14 | "All articles" : "Bütün məqalələr", 15 | "Starred" : "Ulduzlu", 16 | "Settings" : "Quraşdırmalar", 17 | "Documentation" : "Sənədlər", 18 | "Rename" : "Adı dəyiş", 19 | "Delete" : "Sil", 20 | "by" : "onunla", 21 | "Folder" : "Qovluq", 22 | "right" : "Sağ", 23 | "left" : "Sol", 24 | "Refresh" : "Yenilə" 25 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 26 | } -------------------------------------------------------------------------------- /l10n/nn_NO.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "News" : "Nyhende", 3 | "Download" : "Last ned", 4 | "Close" : "Lukk", 5 | "Web address" : "Nettadresse", 6 | "Folder name" : "Mappenamn", 7 | "Username" : "Brukarnamn", 8 | "Password" : "Passord", 9 | "New folder" : "Ny mappe", 10 | "Credentials" : "Credentials", 11 | "Show all articles" : "Vis alle artiklar", 12 | "Move" : "Move", 13 | "Share" : "Del", 14 | "Settings" : "Innstillingar", 15 | "Documentation" : "Dokumentasjon", 16 | "Keyboard shortcuts" : "Tastatursnarvegar", 17 | "Rename" : "Endra namn", 18 | "Delete" : "Ta bort", 19 | "Newest first" : "Nyaste fyrst", 20 | "Oldest first" : "Eldste fyrst", 21 | "by" : "av", 22 | "Download video" : "Last ned video", 23 | "Folder" : "Mappe", 24 | "Refresh" : "Oppfrisk" 25 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 26 | } -------------------------------------------------------------------------------- /src/dataservices/share.service.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosResponse } from '@nextcloud/axios' 2 | 3 | import axios from '@nextcloud/axios' 4 | import { generateOcsUrl } from '@nextcloud/router' 5 | 6 | export class ShareService { 7 | /** 8 | * Retrieves all of users matching the search term 9 | * 10 | * @param query {String} search string 11 | * @return Folders contained in data.folders property 12 | */ 13 | static fetchUsers(query: string): Promise { 14 | return axios.get(generateOcsUrl(`apps/files_sharing/api/v1/sharees?search=${query}&itemType=news_item&perPage=5/`)) 15 | } 16 | 17 | static async share(id: number, users: string[]): Promise { 18 | const promises = [] 19 | for (const shareName of users) { 20 | promises.push(axios.post(`items/${id}/share/${shareName}`)) 21 | } 22 | 23 | await Promise.all(promises) 24 | 25 | return true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /l10n/az.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "Oxunulmamış məqalə", 5 | "Download" : "Yüklə", 6 | "Close" : "Bağla", 7 | "Folder name" : "Qovluq adı", 8 | "Username" : "İstifadəçi adı", 9 | "Password" : "Şifrə", 10 | "New folder" : "Yeni qovluq", 11 | "Credentials" : "Səlahiyyətlər", 12 | "Subscribe" : "Abunə", 13 | "Last job ran {relativeTime}." : "Son işin icra vaxtı {relativeTime}.", 14 | "Move" : "Move", 15 | "Share" : "Paylaş", 16 | "All articles" : "Bütün məqalələr", 17 | "Starred" : "Ulduzlu", 18 | "Settings" : "Quraşdırmalar", 19 | "Documentation" : "Sənədlər", 20 | "Rename" : "Adı dəyiş", 21 | "Delete" : "Sil", 22 | "by" : "onunla", 23 | "Folder" : "Qovluq", 24 | "right" : "Sağ", 25 | "left" : "Sol", 26 | "Refresh" : "Yenilə" 27 | }, 28 | "nplurals=2; plural=(n != 1);"); 29 | -------------------------------------------------------------------------------- /img/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /l10n/nn_NO.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "News" : "Nyhende", 5 | "Download" : "Last ned", 6 | "Close" : "Lukk", 7 | "Web address" : "Nettadresse", 8 | "Folder name" : "Mappenamn", 9 | "Username" : "Brukarnamn", 10 | "Password" : "Passord", 11 | "New folder" : "Ny mappe", 12 | "Credentials" : "Credentials", 13 | "Show all articles" : "Vis alle artiklar", 14 | "Move" : "Move", 15 | "Share" : "Del", 16 | "Settings" : "Innstillingar", 17 | "Documentation" : "Dokumentasjon", 18 | "Keyboard shortcuts" : "Tastatursnarvegar", 19 | "Rename" : "Endra namn", 20 | "Delete" : "Ta bort", 21 | "Newest first" : "Nyaste fyrst", 22 | "Oldest first" : "Eldste fyrst", 23 | "by" : "av", 24 | "Download video" : "Last ned video", 25 | "Folder" : "Mappe", 26 | "Refresh" : "Oppfrisk" 27 | }, 28 | "nplurals=2; plural=(n != 1);"); 29 | -------------------------------------------------------------------------------- /lib/Settings/AdminSection.php: -------------------------------------------------------------------------------- 1 | url = $url; 17 | $this->l = $l; 18 | } 19 | 20 | /** 21 | * @return string 22 | */ 23 | public function getID() 24 | { 25 | return 'news'; 26 | } 27 | 28 | public function getName() 29 | { 30 | return $this->l->t('News'); 31 | } 32 | 33 | public function getPriority() 34 | { 35 | return 10; 36 | } 37 | 38 | /** 39 | * @return string 40 | */ 41 | public function getIcon() 42 | { 43 | return $this->url->imagePath('news', 'app-dark.svg'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "image": "ghcr.io/juliusknorr/nextcloud-dev-php82:latest", 3 | "remoteUser": "root", 4 | "updateRemoteUserUID": true, 5 | "forwardPorts": [80], 6 | "containerEnv": { 7 | "NEXTCLOUD_VERSION": "31", 8 | "NEXTCLOUD_AUTOINSTALL_APPS": "news", 9 | "XDEBUG_MODE": "debug" 10 | }, 11 | "customizations": { 12 | "vscode": { 13 | "extensions": [ 14 | "felixfbecker.php-intellisense", 15 | "octref.vetur" 16 | ], 17 | "settings": { 18 | "php.suggest.basic": false, 19 | "git.alwaysSignOff": true 20 | } 21 | } 22 | }, 23 | "workspaceMount": "source=${localWorkspaceFolder},target=/var/www/html/apps-extra/news,type=bind", 24 | "workspaceFolder": "/var/www/html/apps-extra/news", 25 | "overrideCommand": true, 26 | "postAttachCommand": "bash ./.devcontainer/setup.sh", 27 | "portsAttributes": { 28 | "80": { 29 | "label": "Webserver" 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /lib/Listeners/AddMissingIndicesListener.php: -------------------------------------------------------------------------------- 1 | 10 | * @copyright 2023 Benjamin Brahmer 11 | */ 12 | 13 | namespace OCA\News\Listeners; 14 | 15 | use OCP\EventDispatcher\Event; 16 | use OCP\EventDispatcher\IEventListener; 17 | use OCP\DB\Events\AddMissingIndicesEvent; 18 | 19 | /** 20 | * @template-implements IEventListener 21 | */ 22 | class AddMissingIndicesListener implements IEventListener 23 | { 24 | public function handle(Event $event): void 25 | { 26 | if (!$event instanceof AddMissingIndicesEvent) { 27 | return; 28 | } 29 | 30 | $event->addMissingIndex('news_feeds', 'news_feeds_deleted_at_index', ['deleted_at']); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /lib/Db/EntityJSONSerializer.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Db; 15 | 16 | trait EntityJSONSerializer 17 | { 18 | 19 | /** 20 | * Serialize object properties. 21 | * 22 | * @param array $properties Serializable properties 23 | * 24 | * @return array 25 | */ 26 | public function serializeFields(array $properties): array 27 | { 28 | $result = []; 29 | foreach ($properties as $property) { 30 | $result[$property] = $this->$property; //@phpstan-ignore-line 31 | } 32 | return $result; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/Listeners/UserSettingsListener.php: -------------------------------------------------------------------------------- 1 | */ 16 | class UserSettingsListener implements IEventListener 17 | { 18 | 19 | public function handle(Event $event): void 20 | { 21 | if (!($event instanceof BeforePreferenceSetEvent || $event instanceof BeforePreferenceDeletedEvent)) { 22 | return; 23 | } 24 | 25 | if ($event->getAppId() !== 'news') { 26 | return; 27 | } 28 | 29 | $event->setValid(true); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /l10n/cy_GB.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Llwytho i lawr", 3 | "Close" : "Cau", 4 | "Web address" : "Cyfeiriad gwe", 5 | "Username" : "Enw defnyddiwr", 6 | "Password" : "Cyfrinair", 7 | "New folder" : "Ffolder newydd", 8 | "Credentials" : "Credentials", 9 | "Subscribe" : "Tanysgrifio", 10 | "Move" : "Symud", 11 | "Share" : "Rhannu", 12 | "Mark read" : "Marcio wedi'i ddarllen", 13 | "Settings" : "Gosodiadau", 14 | "Documentation" : "Dogfennaeth", 15 | "Keyboard shortcuts" : "Llwybrau byr bysellfwrdd", 16 | "Rename" : "Ailenwi", 17 | "Delete" : "Dileu", 18 | "Newest first" : "Diweddaraf gyntaf", 19 | "Oldest first" : "Hynaf gyntaf", 20 | "Mark unread" : "Nodi heb ei ddarllen", 21 | "by" : "gan", 22 | "Folder" : "Plygell", 23 | "Not available" : "Ddim ar gael", 24 | "Refresh" : "Adnewyddu" 25 | },"pluralForm" :"nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;" 26 | } -------------------------------------------------------------------------------- /lib/Service/Exceptions/ServiceNotFoundException.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Service\Exceptions; 15 | 16 | use Exception; 17 | use OCP\AppFramework\Db\IMapperException; 18 | 19 | /** 20 | * Class ServiceNotFoundException 21 | * 22 | * @package OCA\News\Service\Exceptions 23 | */ 24 | class ServiceNotFoundException extends ServiceException 25 | { 26 | /** 27 | * @inheritDoc 28 | */ 29 | public static function from(IMapperException $exception): ServiceException 30 | { 31 | return new self($exception->getMessage(), $exception->getCode(), $exception); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lib/Service/Exceptions/ServiceConflictException.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Service\Exceptions; 15 | 16 | use Exception; 17 | use OCP\AppFramework\Db\IMapperException; 18 | 19 | /** 20 | * Class ServiceConflictException 21 | * 22 | * @package OCA\News\Service\Exceptions 23 | */ 24 | class ServiceConflictException extends ServiceException 25 | { 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public static function from(IMapperException $exception): ServiceException 31 | { 32 | return new self($exception->getMessage(), $exception->getCode(), $exception); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /l10n/cy_GB.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Llwytho i lawr", 5 | "Close" : "Cau", 6 | "Web address" : "Cyfeiriad gwe", 7 | "Username" : "Enw defnyddiwr", 8 | "Password" : "Cyfrinair", 9 | "New folder" : "Ffolder newydd", 10 | "Credentials" : "Credentials", 11 | "Subscribe" : "Tanysgrifio", 12 | "Move" : "Symud", 13 | "Share" : "Rhannu", 14 | "Mark read" : "Marcio wedi'i ddarllen", 15 | "Settings" : "Gosodiadau", 16 | "Documentation" : "Dogfennaeth", 17 | "Keyboard shortcuts" : "Llwybrau byr bysellfwrdd", 18 | "Rename" : "Ailenwi", 19 | "Delete" : "Dileu", 20 | "Newest first" : "Diweddaraf gyntaf", 21 | "Oldest first" : "Hynaf gyntaf", 22 | "Mark unread" : "Nodi heb ei ddarllen", 23 | "by" : "gan", 24 | "Folder" : "Plygell", 25 | "Not available" : "Ddim ar gael", 26 | "Refresh" : "Adnewyddu" 27 | }, 28 | "nplurals=4; plural=(n==1) ? 0 : (n==2) ? 1 : (n != 8 && n != 11) ? 2 : 3;"); 29 | -------------------------------------------------------------------------------- /lib/Service/Exceptions/ServiceValidationException.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Service\Exceptions; 15 | 16 | use Exception; 17 | use OCP\AppFramework\Db\IMapperException; 18 | 19 | /** 20 | * Class ServiceValidationException 21 | * 22 | * @package OCA\News\Service\Exceptions 23 | */ 24 | class ServiceValidationException extends ServiceException 25 | { 26 | 27 | /** 28 | * @inheritDoc 29 | */ 30 | public static function from(IMapperException $exception): ServiceException 31 | { 32 | return new self($exception->getMessage(), $exception->getCode(), $exception); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/utils/dateUtils.ts: -------------------------------------------------------------------------------- 1 | import { formatRelativeTime } from '@nextcloud/l10n' 2 | import moment from '@nextcloud/moment' 3 | 4 | /** 5 | * Returns locale formatted date string 6 | * 7 | * @param epoch date value in epoch format 8 | * @return locale formatted date string 9 | */ 10 | export function formatDate(epoch: number) { 11 | return moment.unix(epoch).format('L, LTS') // e.g. "04/20/2025 18:12:21" 12 | } 13 | 14 | /** 15 | * Returns locale relative date string 16 | * 17 | * @param epoch date value in epoch format 18 | * @return locale relative date string 19 | */ 20 | export function formatDateRelative(epoch: number) { 21 | return epoch ? formatRelativeTime(epoch * 1000) : '' // e.g. "one hour ago" 22 | } 23 | 24 | /** 25 | * Returns ISO date string 26 | * 27 | * @param epoch date value in epoch format 28 | * @return ISO date string 29 | */ 30 | export function formatDateISO(epoch: number) { 31 | return moment.unix(epoch).toISOString() // ISO 8601 e.g. "2025-04-20T18:12:21Z" 32 | } 33 | -------------------------------------------------------------------------------- /l10n/bn_BD.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "অপঠিত লেখা", 3 | "Download" : "ডাউনলোড", 4 | "Close" : "বন্ধ", 5 | "Web address" : "ওয়েব ঠিকানা", 6 | "Folder name" : "ফোলডারের নাম", 7 | "Username" : "ব্যবহারকারী", 8 | "Password" : "কূটশব্দ", 9 | "Feed exists already!" : "ফিড পূর্বে থেকেই বিদ্যমান", 10 | "New folder" : "নব ফােলডার", 11 | "Folder exists already!" : "ফোল্ডার ইতোমধ্যে বিদ্যমান!", 12 | "Credentials" : "Credentials", 13 | "Subscribe" : "গ্রাহক হোন", 14 | "Move" : "Move", 15 | "Share" : "ভাগাভাগি কর", 16 | "All articles" : "সব লেখা", 17 | "Starred" : "তারা চিহ্নিত", 18 | "Settings" : "নিয়ামকসমূহ", 19 | "Documentation" : "নথিবদ্ধকরণ", 20 | "Keyboard shortcuts" : "কী-বোর্ড শর্টকাট", 21 | "Rename" : "পূনঃনামকরণ", 22 | "Delete" : "মুছে", 23 | "by" : "কর্তৃক", 24 | "Folder" : "ফোল্ডার", 25 | "right" : "ডান", 26 | "left" : "বাম", 27 | "Refresh" : "নবোদ্যম" 28 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 29 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /** 2 | * Initially copied from typescript+vue2 project generated by @vue/cli-plugin v5.0.0 3 | */ 4 | { 5 | "extends": "@vue/tsconfig", 6 | "compilerOptions": { 7 | "target": "esnext", 8 | "module": "esnext", 9 | "strict": true, 10 | "moduleResolution": "node", 11 | "experimentalDecorators": true, 12 | "skipLibCheck": true, 13 | "esModuleInterop": true, 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "useDefineForClassFields": true, 17 | "sourceMap": true, 18 | "baseUrl": ".", 19 | "types": [ 20 | "vitest", 21 | "node" 22 | ], 23 | "paths": { 24 | "@/*": [ 25 | "src/*" 26 | ] 27 | }, 28 | "lib": [ 29 | "esnext", 30 | "dom", 31 | "dom.iterable", 32 | "scripthost" 33 | ] 34 | }, 35 | "include": [ 36 | "src/**/*.ts", 37 | "src/**/*.vue", 38 | "tests/**/*.ts" 39 | ], 40 | "exclude": [ 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | /*! 2 | * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors 3 | * SPDX-License-Identifier: CC0-1.0 4 | */ 5 | 6 | import { createAppConfig } from '@nextcloud/vite-config' 7 | import { resolve } from 'node:path' 8 | 9 | export default createAppConfig({ 10 | main: 'src/main.js', 11 | 'admin-settings': 'src/main-admin.js', 12 | 'cron-warning': 'src/main-cron-warning.js', 13 | }, { 14 | inlineCSS: { relativeCSSInjection: true }, 15 | config: { 16 | build: { 17 | cssCodeSplit: true, 18 | }, 19 | test: { 20 | coverage: { 21 | include: ['src/**/*.ts', 'src/**/*.vue'], 22 | provider: 'istanbul', 23 | reporter: ['lcov', 'text'], 24 | }, 25 | environment: 'jsdom', 26 | setupFiles: resolve(__dirname, './tests/javascript/unit/setup.js'), 27 | server: { 28 | deps: { 29 | inline: [ 30 | // Fix unresolvable .css extension for ssr 31 | /@nextcloud\/vue/, 32 | /@nextcloud\/dialogs/, 33 | ], 34 | }, 35 | }, 36 | }, 37 | } 38 | }) 39 | 40 | -------------------------------------------------------------------------------- /docker/Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: bash check-code show-log test build 2 | 3 | # Try to use "docker compose" and fall back to "docker-compose" if not available 4 | DOCKER_COMPOSE := $(shell docker compose > /dev/null && echo docker compose || echo docker-compose) 5 | 6 | build: 7 | $(DOCKER_COMPOSE) build 8 | 9 | ls-db: 10 | $(DOCKER_COMPOSE) run --rm app su -c "ls -hal data/mydb.db*" www-data 11 | 12 | fetch-db: 13 | $(DOCKER_COMPOSE) run --rm app su -c "cp data/mydb.db apps/news" www-data 14 | 15 | push-db: 16 | $(DOCKER_COMPOSE) run --rm app su -c "cp apps/news/mydb.db* data" www-data 17 | 18 | bash: 19 | $(DOCKER_COMPOSE) run --rm app su -c "bash" www-data 20 | 21 | cron: 22 | $(DOCKER_COMPOSE) run --rm app su -c "php cron.php" www-data 23 | 24 | check-code: 25 | $(DOCKER_COMPOSE) run --rm app su -c "./occ app:check-code news" www-data 26 | 27 | show-log: 28 | $(DOCKER_COMPOSE) run --rm app tail -f /var/www/html/data/nextcloud.log 29 | 30 | #test: 31 | # $(DOCKER_COMPOSE) run --rm app su -c "cd custom_apps/news && make test" www-data 32 | -------------------------------------------------------------------------------- /l10n/bn_BD.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "অপঠিত লেখা", 5 | "Download" : "ডাউনলোড", 6 | "Close" : "বন্ধ", 7 | "Web address" : "ওয়েব ঠিকানা", 8 | "Folder name" : "ফোলডারের নাম", 9 | "Username" : "ব্যবহারকারী", 10 | "Password" : "কূটশব্দ", 11 | "Feed exists already!" : "ফিড পূর্বে থেকেই বিদ্যমান", 12 | "New folder" : "নব ফােলডার", 13 | "Folder exists already!" : "ফোল্ডার ইতোমধ্যে বিদ্যমান!", 14 | "Credentials" : "Credentials", 15 | "Subscribe" : "গ্রাহক হোন", 16 | "Move" : "Move", 17 | "Share" : "ভাগাভাগি কর", 18 | "All articles" : "সব লেখা", 19 | "Starred" : "তারা চিহ্নিত", 20 | "Settings" : "নিয়ামকসমূহ", 21 | "Documentation" : "নথিবদ্ধকরণ", 22 | "Keyboard shortcuts" : "কী-বোর্ড শর্টকাট", 23 | "Rename" : "পূনঃনামকরণ", 24 | "Delete" : "মুছে", 25 | "by" : "কর্তৃক", 26 | "Folder" : "ফোল্ডার", 27 | "right" : "ডান", 28 | "left" : "বাম", 29 | "Refresh" : "নবোদ্যম" 30 | }, 31 | "nplurals=2; plural=(n != 1);"); 32 | -------------------------------------------------------------------------------- /lib/Scraper/IScraper.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2019 Gioele Falcetti 10 | */ 11 | 12 | namespace OCA\News\Scraper; 13 | 14 | interface IScraper 15 | { 16 | /** 17 | * Scrape feed url 18 | * 19 | * @param string $url 20 | * 21 | * @return bool False if failed 22 | * 23 | */ 24 | public function scrape(string $url): bool; 25 | 26 | /** 27 | * Get the scraped content 28 | * 29 | * @return string|null 30 | * 31 | */ 32 | public function getContent(): ?string; 33 | 34 | /** 35 | * Get the RTL (right-to-left) information 36 | * 37 | * @param bool $default Return this value if the scraper is unable to determine it 38 | * 39 | * @return bool 40 | * 41 | */ 42 | public function getRTL(bool $default = false): bool; 43 | } 44 | -------------------------------------------------------------------------------- /tests/Unit/Controller/JSONHttpErrorTest.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Tests\Unit\Controller; 15 | 16 | use OCA\News\Controller\JSONHttpErrorTrait; 17 | use PHPUnit\Framework\TestCase; 18 | 19 | class JSONHttpErrorTest extends TestCase 20 | { 21 | 22 | 23 | public function testError() 24 | { 25 | $ex = new \Exception('hi'); 26 | $test = new DummyTraitingClass(); 27 | $result = $test->error($ex, 3); 28 | 29 | $this->assertEquals(['message' => 'hi'], $result->getData()); 30 | $this->assertEquals(3, $result->getStatus()); 31 | } 32 | } 33 | 34 | 35 | class DummyTraitingClass 36 | { 37 | use JSONHttpErrorTrait; 38 | } 39 | -------------------------------------------------------------------------------- /.mailmap: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | David-Development 7 | 8 | Alessandro Cosentino Alessandro 9 | Jenkins for ownCloud Jenkins for ownCloud 10 | Thomas Müller Thomas Mueller 11 | bastei bastei 12 | Konrad Graefe kgraefe 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/command/opml.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | setup(){ 4 | load "../test_helper/bats-support/load" 5 | load "../test_helper/bats-assert/load" 6 | load "helpers/settings" 7 | } 8 | 9 | TESTSUITE="OPML" 10 | 11 | teardown() { 12 | ID_LIST=($(./occ news:feed:list 'admin' | grep -Po '"id": \K([0-9]+)' | tr '\n' ' ')) 13 | for ID in $ID_LIST; do 14 | ./occ news:feed:delete "$user" "$ID" 15 | done 16 | } 17 | 18 | @test "[$TESTSUITE] Import" { 19 | run ./occ news:opml:import "$user" apps/news/tests/test_helper/feeds/Nextcloud.opml 20 | assert_success 21 | 22 | run ./occ news:feed:list "$user" 23 | assert_success 24 | 25 | if ! echo "$output" | grep "title.*Nextcloud"; then 26 | assert_output --partial "Feed not imported" 27 | fi 28 | } 29 | 30 | @test "[$TESTSUITE] Export" { 31 | run ./occ news:feed:add "$user" "$NC_FEED" --title "Something-${BATS_SUITE_TEST_NUMBER}" 32 | assert_success 33 | 34 | run ./occ news:opml:export "$user" 35 | assert_success 36 | 37 | if ! echo "$output" | grep "Something-${BATS_SUITE_TEST_NUMBER}"; then 38 | assert_output --partial "Feed not exported" 39 | fi 40 | } 41 | -------------------------------------------------------------------------------- /term.kdl: -------------------------------------------------------------------------------- 1 | // https://zellij.dev/documentation/creating-a-layout 2 | layout { 3 | tab name="main" focus=true { 4 | pane size=1 borderless=true { 5 | plugin location="zellij:tab-bar" 6 | } 7 | pane split_direction="vertical" size="60%" { 8 | pane { 9 | command "lazygit" 10 | focus true 11 | } 12 | pane cwd="docker" command="docker" { 13 | args "compose" "up" 14 | start_suspended false 15 | } 16 | } 17 | pane split_direction="vertical" size="40%" { 18 | pane size="25%" { 19 | command "npm" 20 | args "install" 21 | } 22 | pane size="25%" { 23 | command "composer" 24 | args "install" 25 | } 26 | pane size="50%" { 27 | command "npm" 28 | args "run" "watch" 29 | } 30 | } 31 | pane size=1 borderless=true { 32 | plugin location="zellij:status-bar" 33 | } 34 | } 35 | tab name="term" { 36 | pane size=1 borderless=true { 37 | plugin location="zellij:tab-bar" 38 | } 39 | pane cwd="docker" 40 | pane size=1 borderless=true { 41 | plugin location="zellij:status-bar" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | eslint: 11 | runs-on: ubuntu-latest 12 | 13 | strategy: 14 | matrix: 15 | node-version: [16.x] 16 | 17 | name: eslint node${{ matrix.node-versions }} 18 | steps: 19 | - uses: actions/checkout@v6.0.1 20 | 21 | - name: Set up node ${{ matrix.node-versions }} 22 | uses: actions/setup-node@v6.1.0 23 | with: 24 | node-version: ${{ matrix.node-versions }} 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Lint 30 | run: npm run lint 31 | 32 | stylelint: 33 | runs-on: ubuntu-latest 34 | 35 | strategy: 36 | matrix: 37 | node-version: [16.x] 38 | 39 | name: stylelint node${{ matrix.node-versions }} 40 | steps: 41 | - uses: actions/checkout@v6.0.1 42 | 43 | - name: Set up node ${{ matrix.node-versions }} 44 | uses: actions/setup-node@v6.1.0 45 | with: 46 | node-version: ${{ matrix.node-versions }} 47 | 48 | - name: Install dependencies 49 | run: npm ci 50 | 51 | - name: Lint 52 | run: npm run stylelint 53 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { AppInfoState } from './app.ts' 2 | import type { FeedState } from './feed.ts' 3 | import type { FolderState } from './folder.ts' 4 | import type { ItemState } from './item.ts' 5 | 6 | import { APPLICATION_MUTATION_TYPES, FEED_ITEM_MUTATION_TYPES, FEED_MUTATION_TYPES, FOLDER_MUTATION_TYPES } from '../types/MutationTypes.ts' 7 | import app, { APPLICATION_ACTION_TYPES } from './app.ts' 8 | import feeds, { FEED_ACTION_TYPES } from './feed.ts' 9 | import folders, { FOLDER_ACTION_TYPES } from './folder.ts' 10 | import items, { FEED_ITEM_ACTION_TYPES } from './item.ts' 11 | 12 | export const MUTATIONS = { 13 | ...APPLICATION_MUTATION_TYPES, 14 | ...FEED_MUTATION_TYPES, 15 | ...FOLDER_MUTATION_TYPES, 16 | ...FEED_ITEM_MUTATION_TYPES, 17 | } 18 | 19 | export const ACTIONS = { 20 | ...APPLICATION_ACTION_TYPES, 21 | ...FEED_ACTION_TYPES, 22 | ...FOLDER_ACTION_TYPES, 23 | ...FEED_ITEM_ACTION_TYPES, 24 | } 25 | 26 | export type AppState = FolderState & FeedState & ItemState & AppInfoState 27 | 28 | type Func = (name: string, value: unknown) => void 29 | export type ActionParams = { commit: Func, dispatch: Func, state: T } 30 | 31 | export default { 32 | modules: { 33 | feeds, 34 | folders, 35 | items, 36 | app, 37 | }, 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/lint-info-xml.yml: -------------------------------------------------------------------------------- 1 | # This workflow is provided via the organization template repository 2 | # 3 | # https://github.com/nextcloud/.github 4 | # https://docs.github.com/en/actions/learn-github-actions/sharing-workflows-with-your-organization 5 | # 6 | # SPDX-FileCopyrightText: 2021-2024 Nextcloud GmbH and Nextcloud contributors 7 | # SPDX-License-Identifier: MIT 8 | 9 | name: Lint info.xml 10 | 11 | on: pull_request 12 | 13 | permissions: 14 | contents: read 15 | 16 | concurrency: 17 | group: lint-info-xml-${{ github.head_ref || github.run_id }} 18 | cancel-in-progress: true 19 | 20 | jobs: 21 | xml-linters: 22 | runs-on: ubuntu-latest-low 23 | 24 | name: info.xml lint 25 | steps: 26 | - name: Checkout 27 | uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 28 | with: 29 | persist-credentials: false 30 | 31 | - name: Download schema 32 | run: wget https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd 33 | 34 | - name: Lint info.xml 35 | uses: ChristophWurst/xmllint-action@36f2a302f84f8c83fceea0b9c59e1eb4a616d3c1 # v1.2 36 | with: 37 | xml-file: ./appinfo/info.xml 38 | xml-schema-file: ./info.xsd 39 | -------------------------------------------------------------------------------- /l10n/br.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Download" : "Pellgargañ", 3 | "Close" : "Serriñ", 4 | "Web address" : "Chom-lec'h web", 5 | "No folder" : "Teuliad ebet", 6 | "Folder name" : "Anv teuliad", 7 | "Username" : "anv implijer", 8 | "Password" : "Ger-tremen", 9 | "New folder" : "Teuliad nevez", 10 | "Credentials" : "Kretaatoù", 11 | "Move" : "Diplasañ", 12 | "Share" : "Rannan", 13 | "All articles" : "Pep pennad", 14 | "Settings" : "Arventennoù", 15 | "Documentation" : "Diellvadur", 16 | "Rename" : "Adenvel", 17 | "Delete" : "Dilemel", 18 | "Newest first" : "An hini nevesañ da gentañ", 19 | "Oldest first" : "An hini kozhoñ da gentañ", 20 | "by" : "gant", 21 | "Play audio" : "Loc'hañ an audio", 22 | "Download video" : "Pellkargañ ar video", 23 | "Open website" : "Digor al lec'hienn-web", 24 | "Title" : "Titl", 25 | "Folder" : "Teuliad", 26 | "Refresh" : "Freskaat" 27 | },"pluralForm" :"nplurals=5; plural=((n%10 == 1) && (n%100 != 11) && (n%100 !=71) && (n%100 !=91) ? 0 :(n%10 == 2) && (n%100 != 12) && (n%100 !=72) && (n%100 !=92) ? 1 :(n%10 ==3 || n%10==4 || n%10==9) && (n%100 < 10 || n% 100 > 19) && (n%100 < 70 || n%100 > 79) && (n%100 < 90 || n%100 > 99) ? 2 :(n != 0 && n % 1000000 == 0) ? 3 : 4);" 28 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #specific to news app 2 | composer.phar 3 | node_modules/ 4 | vendor/ 5 | *.log 6 | /build/ 7 | js/ 8 | .rvm 9 | *.clover 10 | .phpunit.result.cache 11 | site/ 12 | coverage 13 | 14 | #scoped dependencies 15 | lib/Vendor/* 16 | 17 | #bats 18 | tests/api/helpers/settings-override.bash 19 | tests/test_helper/feeds/test.xml 20 | tests/test_helper/feeds/feed1.xml 21 | tests/test_helper/feeds/feed2.xml 22 | 23 | #bats 24 | tests/api/helpers/settings-override.bash 25 | tests/test_helper/feeds/test.xml 26 | tests/test_helper/feeds/feed1.xml 27 | tests/test_helper/feeds/feed2.xml 28 | 29 | # python 30 | PKG-INFO 31 | *pyc 32 | *~ 33 | __pycache__ 34 | bin/updater/dist/ 35 | bin/updater/build 36 | 37 | # just sane ignores 38 | .*.sw[po] 39 | *.bak 40 | *.BAK 41 | *~ 42 | *.orig 43 | *.class 44 | .cvsignore 45 | Thumbs.db 46 | *.py[co] 47 | _darcs/* 48 | CVS/* 49 | .svn/* 50 | RCS/* 51 | 52 | # kdevelop 53 | .kdev 54 | *.kdev4 55 | 56 | # Lokalize 57 | *lokalize* 58 | 59 | # eclipse 60 | .project 61 | .settings 62 | 63 | # netbeans 64 | nbproject 65 | 66 | # vscode 67 | .vscode 68 | 69 | # phpStorm 70 | .idea 71 | 72 | # geany 73 | *.geany 74 | 75 | # Cloud9IDE 76 | .settings.xml 77 | 78 | # vim ex mode 79 | .vimrc 80 | 81 | # Mac OS 82 | .DS_Store 83 | *.iml 84 | 85 | # VS Code 86 | .vscode -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import axios from '@nextcloud/axios' 2 | import { createApp } from 'vue' 3 | import { createStore } from 'vuex' 4 | import App from './App.vue' 5 | import router from './routes/index.ts' 6 | import mainStore, { MUTATIONS } from './store/index.ts' 7 | 8 | const store = createStore(mainStore) 9 | 10 | /** 11 | * Handles errors returned during application runtime 12 | * 13 | * @param {Error} error Error thrown 14 | * @return {Promise} Error promise 15 | */ 16 | function handleErrors(error) { 17 | store.commit(MUTATIONS.SET_ERROR, error) 18 | return Promise.reject(error) 19 | } 20 | 21 | /** 22 | * onSuccessCallback is intentionally undefined (triggers on 2xx responses) 23 | * Any status codes that falls outside the range of 2xx cause this function to trigger 24 | */ 25 | axios.interceptors.response.use(undefined, handleErrors) 26 | 27 | const app = createApp(App) 28 | 29 | app.use(store) 30 | app.use(router) 31 | 32 | app.config.globalProperties.t = t 33 | app.config.globalProperties.n = n 34 | app.config.globalProperties.OC = OC 35 | app.config.globalProperties.OCA = OCA 36 | 37 | app.config.errorHandler = handleErrors 38 | 39 | app.mount('#content') 40 | 41 | // Make store accessible for setting cron warning (also for plugins in the future) 42 | window.store = store 43 | -------------------------------------------------------------------------------- /l10n/br.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Download" : "Pellgargañ", 5 | "Close" : "Serriñ", 6 | "Web address" : "Chom-lec'h web", 7 | "No folder" : "Teuliad ebet", 8 | "Folder name" : "Anv teuliad", 9 | "Username" : "anv implijer", 10 | "Password" : "Ger-tremen", 11 | "New folder" : "Teuliad nevez", 12 | "Credentials" : "Kretaatoù", 13 | "Move" : "Diplasañ", 14 | "Share" : "Rannan", 15 | "All articles" : "Pep pennad", 16 | "Settings" : "Arventennoù", 17 | "Documentation" : "Diellvadur", 18 | "Rename" : "Adenvel", 19 | "Delete" : "Dilemel", 20 | "Newest first" : "An hini nevesañ da gentañ", 21 | "Oldest first" : "An hini kozhoñ da gentañ", 22 | "by" : "gant", 23 | "Play audio" : "Loc'hañ an audio", 24 | "Download video" : "Pellkargañ ar video", 25 | "Open website" : "Digor al lec'hienn-web", 26 | "Title" : "Titl", 27 | "Folder" : "Teuliad", 28 | "Refresh" : "Freskaat" 29 | }, 30 | "nplurals=5; plural=((n%10 == 1) && (n%100 != 11) && (n%100 !=71) && (n%100 !=91) ? 0 :(n%10 == 2) && (n%100 != 12) && (n%100 !=72) && (n%100 !=92) ? 1 :(n%10 ==3 || n%10==4 || n%10==9) && (n%100 < 10 || n% 100 > 19) && (n%100 < 70 || n%100 > 79) && (n%100 < 90 || n%100 > 99) ? 2 :(n != 0 && n % 1000000 == 0) ? 3 : 4);"); 31 | -------------------------------------------------------------------------------- /lib/Migration/RemoveUnusedJob.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 26 | $this->joblist = $jobList; 27 | } 28 | 29 | /** 30 | * Returns the step's name 31 | */ 32 | public function getName() 33 | { 34 | return 'Remove the unused News update job'; 35 | } 36 | 37 | /** 38 | * @param IOutput $output 39 | */ 40 | public function run(IOutput $output) 41 | { 42 | if ($this->joblist->has("OCA\News\Cron\Updater", null)) { 43 | $output->info("Job exists, attempting to remove"); 44 | $this->joblist->remove("OCA\News\Cron\Updater"); 45 | $output->info("Job removed"); 46 | } else { 47 | $output->info("Job does not exist, all good"); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /l10n/af.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "News" : "Nuus", 3 | "Shared with me" : "Gedeel met my", 4 | "Download" : "Laai Af", 5 | "Close" : "Sluit", 6 | "Web address" : "Webadres", 7 | "No folder" : "Geen vouer", 8 | "Folder name" : "Vouernaam", 9 | "Username" : "Gebruikersnaam", 10 | "Password" : "Wagwoord", 11 | "New folder" : "Nuwe vouer", 12 | "Folder exists already!" : "Vouers bestaan reeds!", 13 | "Subscribe" : "Teken in", 14 | "Move" : "Skuif", 15 | "Share" : "Deel", 16 | "All articles" : "Alle artikels", 17 | "Explore" : "Ontdek", 18 | "Settings" : "Instellings", 19 | "Documentation" : "Dokumentasie", 20 | "Keyboard shortcuts" : "Sneltoetse", 21 | "Rename" : "Hernoem", 22 | "Delete" : "Skrap", 23 | "Newest first" : "Nuutste eerste", 24 | "Oldest first" : "Oudste eerste", 25 | "by" : "deur", 26 | "Play audio" : "Speel oudio af", 27 | "Download audio" : "Laai oudio af", 28 | "Download video" : "Laai video af", 29 | "Folder" : "Vouer", 30 | "Keyboard shortcut" : "Snelsleutel", 31 | "Jump to next article" : "Spring na volgende artikel", 32 | "Jump to previous article" : "Spring na vorige artikel", 33 | "right" : "regs", 34 | "left" : "links", 35 | "Refresh" : "Verfris" 36 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 37 | } -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | ./tests/Unit 7 | 8 | 9 | 10 | 11 | 12 | ./lib/ 13 | 14 | 15 | ./lib/AppInfo/Application.php 16 | ./lib/Controller/JSONHttpErrorTrait.php 17 | ./lib/*/Exceptions 18 | ./lib/Migration 19 | ./lib/Vendor 20 | ./lib/Db/FeedType.php 21 | ./lib/Db/IAPI.php 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/types/MutationTypes.ts: -------------------------------------------------------------------------------- 1 | export const FEED_MUTATION_TYPES = { 2 | ADD_FEED: 'ADD_FEED', 3 | SET_FEEDS: 'SET_FEEDS', 4 | UPDATE_FEED: 'UPDATE_FEED', 5 | MOVE_FEED: 'MOVE_FEED', 6 | 7 | SET_NEWEST_ITEM_ID: 'SET_NEWEST_ITEM_ID', 8 | SET_FEED_ALL_READ: 'SET_FEED_ALL_READ', 9 | MODIFY_FEED_UNREAD_COUNT: 'MODIFY_FEED_UNREAD_COUNT', 10 | 11 | FEED_DELETE: 'FEED_DELETE', 12 | } 13 | 14 | export const FOLDER_MUTATION_TYPES = { 15 | SET_FOLDERS: 'SET_FOLDERS', 16 | DELETE_FOLDER: 'DELETE_FOLDER', 17 | UPDATE_FOLDER: 'UPDATE_FOLDER', 18 | REMOVE_FOLDER_FEED: 'REMOVE_FOLDER_FEED', 19 | 20 | MODIFY_FOLDER_UNREAD_COUNT: 'MODIFY_FOLDER_UNREAD_COUNT', 21 | } 22 | 23 | export const FEED_ITEM_MUTATION_TYPES = { 24 | SET_ITEMS: 'SET_ITEMS', 25 | UPDATE_ITEM: 'UPDATE_ITEM', 26 | 27 | SET_SELECTED_ITEM: 'SET_SELECTED_ITEM', 28 | SET_PLAYING_ITEM: 'SET_PLAYING_ITEM', 29 | 30 | SET_STARRED_COUNT: 'SET_STARRED_COUNT', 31 | SET_UNREAD_COUNT: 'SET_UNREAD_COUNT', 32 | MODIFY_UNREAD_COUNT: 'MODIFY_UNREAD_COUNT', 33 | 34 | SET_FETCHING: 'SET_FETCHING', 35 | SET_ALL_LOADED: 'SET_ALL_LOADED', 36 | SET_LAST_ITEM_LOADED: 'SET_LAST_ITEM_LOADED', 37 | SET_NEWEST_ITEM_ID: 'SET_NEWEST_ITEM_ID', 38 | RESET_ITEM_STATES: 'RESET_ITEM_STATES', 39 | } 40 | 41 | export const APPLICATION_MUTATION_TYPES = { 42 | SET_ERROR: 'SET_ERROR', 43 | SET_LOADING: 'SET_LOADING', 44 | } 45 | -------------------------------------------------------------------------------- /l10n/af.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "News" : "Nuus", 5 | "Shared with me" : "Gedeel met my", 6 | "Download" : "Laai Af", 7 | "Close" : "Sluit", 8 | "Web address" : "Webadres", 9 | "No folder" : "Geen vouer", 10 | "Folder name" : "Vouernaam", 11 | "Username" : "Gebruikersnaam", 12 | "Password" : "Wagwoord", 13 | "New folder" : "Nuwe vouer", 14 | "Folder exists already!" : "Vouers bestaan reeds!", 15 | "Subscribe" : "Teken in", 16 | "Move" : "Skuif", 17 | "Share" : "Deel", 18 | "All articles" : "Alle artikels", 19 | "Explore" : "Ontdek", 20 | "Settings" : "Instellings", 21 | "Documentation" : "Dokumentasie", 22 | "Keyboard shortcuts" : "Sneltoetse", 23 | "Rename" : "Hernoem", 24 | "Delete" : "Skrap", 25 | "Newest first" : "Nuutste eerste", 26 | "Oldest first" : "Oudste eerste", 27 | "by" : "deur", 28 | "Play audio" : "Speel oudio af", 29 | "Download audio" : "Laai oudio af", 30 | "Download video" : "Laai video af", 31 | "Folder" : "Vouer", 32 | "Keyboard shortcut" : "Snelsleutel", 33 | "Jump to next article" : "Spring na volgende artikel", 34 | "Jump to previous article" : "Spring na vorige artikel", 35 | "right" : "regs", 36 | "left" : "links", 37 | "Refresh" : "Verfris" 38 | }, 39 | "nplurals=2; plural=(n != 1);"); 40 | -------------------------------------------------------------------------------- /lib/Service/Exceptions/ServiceException.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Service\Exceptions; 15 | 16 | use Exception; 17 | use OCP\AppFramework\Db\IMapperException; 18 | 19 | /** 20 | * Class ServiceException 21 | * 22 | * @package OCA\News\Service\Exceptions 23 | */ 24 | abstract class ServiceException extends Exception 25 | { 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @param string $msg the error message 31 | * @param int $code 32 | * @param Exception|null $previous 33 | */ 34 | final public function __construct(string $msg, int $code = 0, ?Exception $previous = null) 35 | { 36 | parent::__construct($msg, $code, $previous); 37 | } 38 | 39 | /** 40 | * Create exception from Mapper exception. 41 | * 42 | * @param IMapperException $exception Existing exception 43 | * 44 | * @return static 45 | */ 46 | abstract public static function from(IMapperException $exception): ServiceException; 47 | } 48 | -------------------------------------------------------------------------------- /tests/javascript/unit/services/share.service.spec.ts: -------------------------------------------------------------------------------- 1 | import axios from '@nextcloud/axios' 2 | import { beforeEach, describe, expect, it } from 'vitest' 3 | import { ShareService } from './../../../../src/dataservices/share.service' 4 | 5 | describe('share.service.ts', () => { 6 | 'use strict' 7 | 8 | beforeEach(() => { 9 | axios.get.mockReset() 10 | axios.post.mockReset() 11 | }) 12 | 13 | describe('fetchUsers', () => { 14 | it('should call GET to retrieve users', async () => { 15 | axios.get.mockResolvedValue({ data: { feeds: [] } }) 16 | 17 | await ShareService.fetchUsers('abc') 18 | 19 | expect(axios.get).toBeCalled() 20 | const args = axios.get.mock.calls[0] 21 | 22 | expect(args[0]).toContain('search=abc') 23 | }) 24 | }) 25 | 26 | describe('share', () => { 27 | it('should call POST for each user passed', async () => { 28 | await ShareService.share(123, ['share-user']) 29 | 30 | expect(axios.post).toBeCalledTimes(1) 31 | let args = axios.post.mock.calls[0] 32 | 33 | expect(args[0]).toContain('123/share/share-user') 34 | 35 | await ShareService.share(345, ['share-user', 'share2']) 36 | 37 | expect(axios.post).toBeCalledTimes(3) 38 | 39 | args = axios.post.mock.calls[1] 40 | expect(args[0]).toContain('345/share/share-user') 41 | args = axios.post.mock.calls[2] 42 | expect(args[0]).toContain('345/share/share2') 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /lib/Hooks/UserDeleteHook.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Hooks; 15 | 16 | use OCA\News\AppInfo\Application; 17 | use OCA\News\Service\FeedServiceV2; 18 | use OCA\News\Service\FolderServiceV2; 19 | use OCA\News\Service\ItemServiceV2; 20 | use OCP\EventDispatcher\Event; 21 | use OCP\EventDispatcher\IEventListener; 22 | use OCP\User\Events\BeforeUserDeletedEvent; 23 | 24 | class UserDeleteHook implements IEventListener 25 | { 26 | 27 | /** 28 | * Handle user deletion 29 | * 30 | * @param BeforeUserDeletedEvent $event 31 | */ 32 | public function handle(Event $event): void 33 | { 34 | $userId = $event->getUser()->getUID(); 35 | 36 | $app = new Application(); 37 | $container = $app->getContainer(); 38 | 39 | // order is important! 40 | $container->get(ItemServiceV2::class)->deleteUser($userId); 41 | $container->get(FeedServiceV2::class)->deleteUser($userId); 42 | $container->get(FolderServiceV2::class)->deleteUser($userId); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/javascript/unit/components/routes/Recent.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Store } from 'vuex' 2 | 3 | import { shallowMount } from '@vue/test-utils' 4 | import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' 5 | import Vuex from 'vuex' 6 | import ContentTemplate from '../../../../../src/components/ContentTemplate.vue' 7 | import Recent from '../../../../../src/components/routes/Recent.vue' 8 | 9 | describe('Recent.vue', () => { 10 | 'use strict' 11 | let wrapper: any 12 | 13 | const mockItems = [ 14 | { 15 | id: 1, 16 | }, { 17 | id: 2, 18 | }, { 19 | id: 3, 20 | }, { 21 | id: 4, 22 | }, 23 | ] 24 | 25 | const mockItemIds = [2, 3] 26 | 27 | let store: Store 28 | beforeAll(() => { 29 | store = new Vuex.Store({ 30 | state: { 31 | items: { 32 | allItems: mockItems, 33 | recentItemIds: mockItemIds, 34 | }, 35 | }, 36 | getters: { 37 | allItems: () => mockItems, 38 | recentItemIds: () => mockItemIds, 39 | }, 40 | }) 41 | 42 | store.dispatch = vi.fn() 43 | store.commit = vi.fn() 44 | 45 | wrapper = shallowMount(Recent, { 46 | global: { 47 | plugins: [store], 48 | }, 49 | }) 50 | }) 51 | 52 | beforeEach(() => { 53 | vi.clearAllMocks() 54 | }) 55 | 56 | it('should get recent items from state', () => { 57 | expect((wrapper.findComponent(ContentTemplate)).props().items.length).toEqual(2) 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /l10n/eo.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "Nelegitaj artikoloj", 3 | "News" : "Novaĵo", 4 | "Download" : "Elŝuti", 5 | "Close" : "Fermi", 6 | "Web address" : "TTT-adreso", 7 | "No folder" : "Neniu dosierujo", 8 | "Folder name" : "Dosierujnomo", 9 | "Username" : "Uzantonomo", 10 | "Password" : "Pasvorto", 11 | "New folder" : "Nova dosierujo", 12 | "Folder exists already!" : "Dosierujo jam ekzistas!", 13 | "Credentials" : "Aŭtentigiloj", 14 | "Subscribe" : "Aboni", 15 | "Show all articles" : "Montri ĉiujn artikolojn", 16 | "Move" : "Movi", 17 | "Share" : "Kunhavigi", 18 | "Share with" : "Kunhavigi kun", 19 | "Mark read" : "Marki kiel legita", 20 | "All articles" : "Ĉiuj artikoloj", 21 | "Starred" : "Markita", 22 | "Settings" : "Agordo", 23 | "Documentation" : "Dokumentaro", 24 | "Keyboard shortcuts" : "Fulmoklavoj", 25 | "Rename" : "Alinomigi", 26 | "Delete" : "Forigi", 27 | "Newest first" : "Pli novaj unue", 28 | "Oldest first" : "Malpli novaj unue", 29 | "Mark unread" : "Marki kiel nelegita", 30 | "Close details" : "Fermi detalojn", 31 | "by" : "de", 32 | "Open website" : "Malfermi TTT-ejon", 33 | "Folder" : "Dosierujo", 34 | "Not available" : "Ne disponeble", 35 | "right" : "dekstro", 36 | "left" : "maldekstro", 37 | "Refresh" : "Refreŝigi" 38 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 39 | } -------------------------------------------------------------------------------- /l10n/eo.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "Nelegitaj artikoloj", 5 | "News" : "Novaĵo", 6 | "Download" : "Elŝuti", 7 | "Close" : "Fermi", 8 | "Web address" : "TTT-adreso", 9 | "No folder" : "Neniu dosierujo", 10 | "Folder name" : "Dosierujnomo", 11 | "Username" : "Uzantonomo", 12 | "Password" : "Pasvorto", 13 | "New folder" : "Nova dosierujo", 14 | "Folder exists already!" : "Dosierujo jam ekzistas!", 15 | "Credentials" : "Aŭtentigiloj", 16 | "Subscribe" : "Aboni", 17 | "Show all articles" : "Montri ĉiujn artikolojn", 18 | "Move" : "Movi", 19 | "Share" : "Kunhavigi", 20 | "Share with" : "Kunhavigi kun", 21 | "Mark read" : "Marki kiel legita", 22 | "All articles" : "Ĉiuj artikoloj", 23 | "Starred" : "Markita", 24 | "Settings" : "Agordo", 25 | "Documentation" : "Dokumentaro", 26 | "Keyboard shortcuts" : "Fulmoklavoj", 27 | "Rename" : "Alinomigi", 28 | "Delete" : "Forigi", 29 | "Newest first" : "Pli novaj unue", 30 | "Oldest first" : "Malpli novaj unue", 31 | "Mark unread" : "Marki kiel nelegita", 32 | "Close details" : "Fermi detalojn", 33 | "by" : "de", 34 | "Open website" : "Malfermi TTT-ejon", 35 | "Folder" : "Dosierujo", 36 | "Not available" : "Ne disponeble", 37 | "right" : "dekstro", 38 | "left" : "maldekstro", 39 | "Refresh" : "Refreŝigi" 40 | }, 41 | "nplurals=2; plural=(n != 1);"); 42 | -------------------------------------------------------------------------------- /lib/Command/Config/OpmlExport.php: -------------------------------------------------------------------------------- 1 | opmlService = $opmlService; 23 | } 24 | 25 | /** 26 | * Configure command 27 | * 28 | * @return void 29 | */ 30 | protected function configure() 31 | { 32 | $this->setName('news:opml:export') 33 | ->setDescription('Print OPML file') 34 | ->addArgument('user-id', InputArgument::REQUIRED, 'User data to export'); 35 | } 36 | 37 | /** 38 | * Execute command 39 | * 40 | * @param InputInterface $input 41 | * @param OutputInterface $output 42 | * 43 | * @return int 44 | */ 45 | protected function execute(InputInterface $input, OutputInterface $output): int 46 | { 47 | $user = $input->getArgument('user-id'); 48 | 49 | $output->write($this->opmlService->export($user)); 50 | return 0; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/components/routes/Item.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 64 | -------------------------------------------------------------------------------- /bin/tools/generate_authors.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | 10 | * @copyright Bernhard Posselt 2016 11 | */ 12 | 13 | $cmd = 'git --no-pager shortlog -nse HEAD'; 14 | exec($cmd, $contributors); 15 | 16 | // extract data from git output into an array 17 | $regex = '/^\s*(?P\d+)\s*(?P.*\w)\s*<(?P[^\s]+)>$/'; 18 | $contributors = array_map(function ($contributor) use ($regex) { 19 | $result = []; 20 | preg_match($regex, $contributor, $result); 21 | return $result; 22 | }, $contributors); 23 | 24 | // filter out bots 25 | $contributors = array_filter($contributors, function ($contributor) { 26 | if (empty($contributor['name']) || empty($contributor['email'])) { 27 | return false; 28 | } 29 | if (strpos($contributor['email'], 'bot') || strpos($contributor['name'], 'bot')) { 30 | return false; 31 | } 32 | return true; 33 | }); 34 | 35 | // turn tuples into markdown 36 | $markdownLines = array_map(function ($contrib) { 37 | return '* [' . $contrib['name'] . '](mailto:' . $contrib['email'] . ')'; 38 | }, $contributors); 39 | 40 | // add headline 41 | array_unshift($markdownLines, '# Authors'); 42 | 43 | $markdown = implode("\n", $markdownLines); 44 | file_put_contents('AUTHORS.md', $markdown); 45 | -------------------------------------------------------------------------------- /lib/Command/Updater/BeforeUpdate.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Bernhard Posselt 2016 10 | */ 11 | 12 | namespace OCA\News\Command\Updater; 13 | 14 | use OCA\News\Service\UpdaterService; 15 | use Symfony\Component\Console\Command\Command; 16 | use Symfony\Component\Console\Input\InputInterface; 17 | use Symfony\Component\Console\Output\OutputInterface; 18 | 19 | class BeforeUpdate extends Command 20 | { 21 | /** 22 | * @var UpdaterService Updater 23 | */ 24 | private $updaterService; 25 | 26 | public function __construct(UpdaterService $updater) 27 | { 28 | parent::__construct(); 29 | $this->updaterService = $updater; 30 | } 31 | 32 | /** 33 | * @return void 34 | */ 35 | protected function configure() 36 | { 37 | $this->setName('news:updater:before-update') 38 | ->setDescription( 39 | 'This is used to clean up the database. It ' . 40 | 'deletes folders and feeds that are marked for ' . 41 | 'deletion' 42 | ); 43 | } 44 | 45 | protected function execute(InputInterface $input, OutputInterface $output): int 46 | { 47 | $this->updaterService->beforeUpdate(); 48 | 49 | return 0; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/maintenance.md: -------------------------------------------------------------------------------- 1 | # Maintenance 2 | 3 | ## Release 4 | Releases are created automatically by GitHub Actions. A release is triggered via a GitHub Release. 5 | The GitHub Action will then start a build based on the git tag. A release can only be approved by [@Grotax](https://github.com/Grotax) or [@SMillerDev](https://github.com/SMillerDev). An admin of the Nextcloud organization can always overwrite these settings. The private key is stored as environmental secret in GitHub. The owner of the private key is [@Grotax](https://github.com/Grotax). 6 | 7 | ## Support 8 | ### PHP 9 | While the app should try to support all PHP versions that Nextcloud currently supports, 10 | the real focus when deciding to cut a PHP version should be on maintenance burden. 11 | Users are nice, but devs should be a priority in decisions that are likely to impact them significantly. 12 | 13 | ### Issues 14 | - Bug reports without test cases (feed URL and action is enough) can be closed with or without comment. 15 | 16 | - Feature requests without thoughtful commentary or pull request can be closed with or without comment, 17 | unless a developer is interested to support such a feature. 18 | 19 | - Issues without activity in the last 30 days can be closed with or without comment. 20 | If this is a bug you care about that isn't getting attention, fix it. 21 | If you're good enough to understand the bug, you're good enough to fix it. 22 | 23 | 24 | _Largely inspired by https://gist.github.com/ryanflorence/124070e7c4b3839d4573_ 25 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | # Maintain dependencies for npm 9 | - package-ecosystem: "npm" 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | labels: 14 | - "dependencies" 15 | - "Skip-Changelog" 16 | versioning-strategy: increase 17 | groups: 18 | vue: 19 | patterns: 20 | - "vue" 21 | - "vuex" 22 | - "vue-router" 23 | - "@vue/*" 24 | - "vue-material-design-icons" 25 | vite: 26 | patterns: 27 | - "vite" 28 | - "vitest" 29 | - "@vitest/*" 30 | types: 31 | patterns: 32 | - "@types/*" 33 | 34 | # Maintain dependencies for Composer 35 | - package-ecosystem: "composer" 36 | directory: "/" 37 | schedule: 38 | interval: "daily" 39 | labels: 40 | - "dependencies" 41 | - "Skip-Changelog" 42 | versioning-strategy: increase 43 | 44 | # Maintain dependencies for GitHub Actions 45 | - package-ecosystem: "github-actions" 46 | directory: "/" 47 | schedule: 48 | interval: "daily" 49 | labels: 50 | - "dependencies" 51 | - "Skip-Changelog" 52 | -------------------------------------------------------------------------------- /lib/Utility/Cache.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2023 Benjamin Brahmer 10 | */ 11 | namespace OCA\News\Utility; 12 | 13 | use OCP\ITempManager; 14 | use OCP\IConfig; 15 | 16 | class Cache 17 | { 18 | 19 | 20 | /** 21 | * @var ITempManager 22 | */ 23 | private $ITempManager; 24 | 25 | /** 26 | * @var IConfig 27 | */ 28 | private $IConfig; 29 | 30 | 31 | public function __construct( 32 | ITempManager $ITempManager, 33 | IConfig $IConfig 34 | ) { 35 | $this->ITempManager = $ITempManager; 36 | $this->IConfig = $IConfig; 37 | } 38 | 39 | /** 40 | * Get a news app cache directory 41 | * 42 | * @param String $name for the sub-directory, is created if not existing 43 | * 44 | * @return String $directory The path for the cache 45 | */ 46 | public function getCache(String $name): String 47 | { 48 | $baseDir = $this->ITempManager->getTempBaseDir(); 49 | $instanceID = $this->IConfig->getSystemValue('instanceid'); 50 | 51 | $directory = join(DIRECTORY_SEPARATOR, [$baseDir, "news-" . $instanceID, 'cache', $name]); 52 | 53 | if (!is_dir($directory)) { 54 | mkdir($directory, 0770, true); 55 | } 56 | 57 | return $directory; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /l10n/mn.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "News" : "Мэдээ", 3 | "Download" : "Татах", 4 | "Close" : "Хаах", 5 | "Web address" : "Вэб хаяг", 6 | "No folder" : "Хавтас алга", 7 | "Folder name" : "Хавтасны нэр", 8 | "Username" : "Хэрэглэгчийн нэр", 9 | "Password" : "Нууц үг", 10 | "New folder" : "Шинэ хавтас", 11 | "Credentials" : "Итгэмжлэл", 12 | "Subscribe" : "Захиалга", 13 | "Move" : "Зөөх", 14 | "Share" : "Түгээх", 15 | "Explore" : "Хайх", 16 | "Settings" : "Тохиргоо", 17 | "Rename" : "Нэрлэнэ үү", 18 | "Delete" : "Устгах", 19 | "Newest first" : "Шинийг урд нь", 20 | "Oldest first" : "Хуучныг урд нь", 21 | "Toggle star article" : "Өгүүллийг соль", 22 | "Play audio" : "Аудио тоглуулах", 23 | "Download audio" : "Аудиог татаж авах", 24 | "Download video" : "Видеог татаж авах", 25 | "Folder" : "Хавтас", 26 | "Keyboard shortcut" : "Keyboard товчлуур", 27 | "Jump to next article" : "Дараагийн өгүүллийг үзээрэй", 28 | "Jump to previous article" : "Өмнөх нийтлэл рүү очих", 29 | "right" : "баруун", 30 | "left" : "зүүн", 31 | "Refresh" : "Сэргээх", 32 | "Load next feed" : "Дараагийн тэжээлийг ачаална уу", 33 | "Load previous feed" : "Өмнөх тэжээлийг ачаална уу", 34 | "Load previous folder" : "Өмнөх хавтсыг ачаална уу", 35 | "Load next folder" : "Дараагийн хавтасыг ачаална уу", 36 | "How to set up the operating system cron" : "Үйлдлийн системийн cron-г хэрхэн тохируулах талаар" 37 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 38 | } -------------------------------------------------------------------------------- /l10n/lb.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "Ongeliesen Artikelen", 3 | "Download" : "Download", 4 | "Close" : "Zoumaachen", 5 | "Web address" : "Link", 6 | "No folder" : "Keen Dossier", 7 | "Folder name" : "Dossiers Numm:", 8 | "Username" : "Benotzernumm", 9 | "Password" : "Passwuert", 10 | "New folder" : "Neien Dossier", 11 | "Folder exists already!" : "Dësen Dossier gëtt et schonns!", 12 | "Credentials" : "Credentials", 13 | "Subscribe" : "Umellen", 14 | "Show all articles" : "All d'Artikelen uweisen", 15 | "Move" : "Verschieben", 16 | "Share" : "Deelen", 17 | "All articles" : "All Artikelen", 18 | "Explore" : "Erfuerschen", 19 | "Settings" : "Astellungen", 20 | "Documentation" : "Dokumentatioun", 21 | "Rename" : "Ëm-benennen", 22 | "Delete" : "Läschen", 23 | "by" : "vun", 24 | "Play audio" : "Toun ofspillen", 25 | "Download audio" : "Toun eroflueden", 26 | "Download video" : "Video eroflueden", 27 | "Open website" : "Oppe Websäit", 28 | "Folder" : "Dossier", 29 | "Jump to next article" : "Op den nächsten Artikel sprangen", 30 | "Jump to previous article" : "Op den Artikel vu virdru sprangen", 31 | "right" : "riets", 32 | "left" : "lénks", 33 | "Open article in new tab" : "Den Artikel an engem neien Tab opmaachen", 34 | "Refresh" : "Opfrëschen", 35 | "Load previous folder" : "Den Dossier vu virdrun lueden", 36 | "Load next folder" : "Den nächsten Dossier lueden" 37 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 38 | } -------------------------------------------------------------------------------- /l10n/mn.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "News" : "Мэдээ", 5 | "Download" : "Татах", 6 | "Close" : "Хаах", 7 | "Web address" : "Вэб хаяг", 8 | "No folder" : "Хавтас алга", 9 | "Folder name" : "Хавтасны нэр", 10 | "Username" : "Хэрэглэгчийн нэр", 11 | "Password" : "Нууц үг", 12 | "New folder" : "Шинэ хавтас", 13 | "Credentials" : "Итгэмжлэл", 14 | "Subscribe" : "Захиалга", 15 | "Move" : "Зөөх", 16 | "Share" : "Түгээх", 17 | "Explore" : "Хайх", 18 | "Settings" : "Тохиргоо", 19 | "Rename" : "Нэрлэнэ үү", 20 | "Delete" : "Устгах", 21 | "Newest first" : "Шинийг урд нь", 22 | "Oldest first" : "Хуучныг урд нь", 23 | "Toggle star article" : "Өгүүллийг соль", 24 | "Play audio" : "Аудио тоглуулах", 25 | "Download audio" : "Аудиог татаж авах", 26 | "Download video" : "Видеог татаж авах", 27 | "Folder" : "Хавтас", 28 | "Keyboard shortcut" : "Keyboard товчлуур", 29 | "Jump to next article" : "Дараагийн өгүүллийг үзээрэй", 30 | "Jump to previous article" : "Өмнөх нийтлэл рүү очих", 31 | "right" : "баруун", 32 | "left" : "зүүн", 33 | "Refresh" : "Сэргээх", 34 | "Load next feed" : "Дараагийн тэжээлийг ачаална уу", 35 | "Load previous feed" : "Өмнөх тэжээлийг ачаална уу", 36 | "Load previous folder" : "Өмнөх хавтсыг ачаална уу", 37 | "Load next folder" : "Дараагийн хавтасыг ачаална уу", 38 | "How to set up the operating system cron" : "Үйлдлийн системийн cron-г хэрхэн тохируулах талаар" 39 | }, 40 | "nplurals=2; plural=(n != 1);"); 41 | -------------------------------------------------------------------------------- /tests/Unit/Utility/TimeTest.php: -------------------------------------------------------------------------------- 1 | 4 | * 5 | * @license AGPL-3.0 6 | * 7 | * This code is free software: you can redistribute it and/or modify 8 | * it under the terms of the GNU Affero General Public License, version 3, 9 | * as published by the Free Software Foundation. 10 | * 11 | * This program is distributed in the hope that it will be useful, 12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | * GNU Affero General Public License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License, version 3, 17 | * along with this program. If not, see 18 | * 19 | */ 20 | 21 | namespace OCA\News\Tests\Unit\Utility; 22 | 23 | use OCA\News\Utility\Time; 24 | use PHPUnit\Framework\TestCase; 25 | 26 | class TimeTest extends TestCase 27 | { 28 | /** 29 | * Test if the correct type is returned 30 | */ 31 | public function testTime(): void 32 | { 33 | $cur = time(); 34 | 35 | $time = new Time(); 36 | $result = $time->getTime(); 37 | $this->assertIsInt($result); 38 | $this->assertTrue($result >= $cur); 39 | } 40 | 41 | 42 | public function testMicroTime(): void 43 | { 44 | $cur = microtime(true) * 1000000; 45 | 46 | $time = new Time(); 47 | $result = (float) $time->getMicroTime(); 48 | $this->assertTrue($result >= $cur); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /l10n/lb.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "Ongeliesen Artikelen", 5 | "Download" : "Download", 6 | "Close" : "Zoumaachen", 7 | "Web address" : "Link", 8 | "No folder" : "Keen Dossier", 9 | "Folder name" : "Dossiers Numm:", 10 | "Username" : "Benotzernumm", 11 | "Password" : "Passwuert", 12 | "New folder" : "Neien Dossier", 13 | "Folder exists already!" : "Dësen Dossier gëtt et schonns!", 14 | "Credentials" : "Credentials", 15 | "Subscribe" : "Umellen", 16 | "Show all articles" : "All d'Artikelen uweisen", 17 | "Move" : "Verschieben", 18 | "Share" : "Deelen", 19 | "All articles" : "All Artikelen", 20 | "Explore" : "Erfuerschen", 21 | "Settings" : "Astellungen", 22 | "Documentation" : "Dokumentatioun", 23 | "Rename" : "Ëm-benennen", 24 | "Delete" : "Läschen", 25 | "by" : "vun", 26 | "Play audio" : "Toun ofspillen", 27 | "Download audio" : "Toun eroflueden", 28 | "Download video" : "Video eroflueden", 29 | "Open website" : "Oppe Websäit", 30 | "Folder" : "Dossier", 31 | "Jump to next article" : "Op den nächsten Artikel sprangen", 32 | "Jump to previous article" : "Op den Artikel vu virdru sprangen", 33 | "right" : "riets", 34 | "left" : "lénks", 35 | "Open article in new tab" : "Den Artikel an engem neien Tab opmaachen", 36 | "Refresh" : "Opfrëschen", 37 | "Load previous folder" : "Den Dossier vu virdrun lueden", 38 | "Load next folder" : "Den nächsten Dossier lueden" 39 | }, 40 | "nplurals=2; plural=(n != 1);"); 41 | -------------------------------------------------------------------------------- /l10n/vi.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "Bài chưa đọc", 3 | "Download" : "Tải về", 4 | "Close" : "Đóng", 5 | "Web address" : "Địa chỉ Web", 6 | "No folder" : "Không có thư mục nào", 7 | "Folder name" : "Tên thư mục", 8 | "Username" : "Tên đăng nhập", 9 | "Password" : "Mật khẩu", 10 | "Feed exists already!" : "Nguồn đã tồn tại", 11 | "New folder" : "Tạo thư mục", 12 | "Folder exists already!" : "Thư mục đã tồn tại!", 13 | "Credentials" : "Giấy chứng nhận", 14 | "Subscribe" : "Theo dõi", 15 | "Last job ran {relativeTime}." : "Công việc cuối cùng đã chạy {relativeTime}.", 16 | "Move" : "Dịch chuyển", 17 | "Share" : "Chia sẻ", 18 | "Off" : "Tắt", 19 | "Mark read" : "Đánh dấu là đã đọc", 20 | "All articles" : "Mọi bài viết", 21 | "Starred" : "Starred", 22 | "Settings" : "Cài đặt", 23 | "Documentation" : "Tài liệu hướng dẫn", 24 | "Keyboard shortcuts" : "Phím tắt", 25 | "Rename" : "Sửa tên", 26 | "Delete" : "Xóa", 27 | "Rename Feed" : "Đổi tên nguồn dữ liệu", 28 | "Newest first" : "Mới trước", 29 | "Oldest first" : "Cũ trước", 30 | "Mark unread" : "Đánh dấu là chưa đọc", 31 | "by" : "bởi", 32 | "Open website" : "Mở trang web", 33 | "Title" : "Tên", 34 | "Folder" : "Thư mục", 35 | "Not available" : "Không khả dụng", 36 | "Keyboard shortcut" : "Phím tắt", 37 | "Action" : "Hành động", 38 | "right" : "phải", 39 | "left" : "trái", 40 | "Refresh" : "Làm mới", 41 | "Shift" : "Shift" 42 | },"pluralForm" :"nplurals=1; plural=0;" 43 | } -------------------------------------------------------------------------------- /lib/Explore/RecommendedSites.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Explore; 15 | 16 | use OCA\News\Explore\Exceptions\RecommendedSiteNotFoundException; 17 | 18 | class RecommendedSites 19 | { 20 | 21 | private $directory; 22 | 23 | /** 24 | * @param string $exploreDir the absolute path to where the recommendation 25 | * config files lie without a trailing slash 26 | */ 27 | public function __construct(string $exploreDir) 28 | { 29 | $this->directory = $exploreDir; 30 | } 31 | 32 | 33 | /** 34 | * @param string $languageCode 35 | * 36 | * @return array 37 | * 38 | * @throws RecommendedSiteNotFoundException 39 | */ 40 | public function forLanguage(string $languageCode): array 41 | { 42 | $file = $this->directory . '/feeds.' . $languageCode . '.json'; 43 | 44 | if (file_exists($file)) { 45 | return json_decode(file_get_contents($file), true); 46 | } else { 47 | $msg = 'No recommended sites found for language code ' . 48 | $languageCode; 49 | throw new RecommendedSiteNotFoundException($msg); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us fix an issue you are having 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | # IMPORTANT 11 | 12 | Read and tick the following checkbox after you have created the issue or place an x inside the brackets ;) 13 | 14 | * [ ] I have read the [CONTRIBUTING.md](https://github.com/nextcloud/news/blob/master/CONTRIBUTING.md) and followed the provided tips 15 | * [ ] I accept that the issue will be closed without comment if I do not check [here](https://github.com/nextcloud/news/blob/master/CONTRIBUTING.md#hints-for-reporting-bugs) 16 | * [ ] I accept that the issue will be closed without comment if I do not fill out all items in the issue template. 17 | 18 | 19 | ## Explain the Problem 20 | What problem did you encounter? 21 | 22 | ## Steps to Reproduce 23 | Explain what you did to encounter the issue 24 | 1. Click 25 | 2. Drag 26 | 3. ?? 27 | 4. Profit! 28 | 29 | ## System Information 30 | * News app version: 31 | * Nextcloud version: 32 | * Cron type: (system cron/python updater/...) 33 | * PHP version: 34 | * Database and version: 35 | * Browser and version: 36 | * OS and version: 37 | 38 |
39 | Contents of nextcloud/data/nextcloud.log 40 | 41 | ```json 42 | Paste output here 43 | ``` 44 |
45 | 46 |
47 | Contents of Browser Error Console 48 | Read http://ggnome.com/wiki/Using_The_Browser_Error_Console if you are unsure what to put here 49 | 50 | ``` 51 | Paste output here 52 | ``` 53 |
54 | -------------------------------------------------------------------------------- /l10n/vi.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "Bài chưa đọc", 5 | "Download" : "Tải về", 6 | "Close" : "Đóng", 7 | "Web address" : "Địa chỉ Web", 8 | "No folder" : "Không có thư mục nào", 9 | "Folder name" : "Tên thư mục", 10 | "Username" : "Tên đăng nhập", 11 | "Password" : "Mật khẩu", 12 | "Feed exists already!" : "Nguồn đã tồn tại", 13 | "New folder" : "Tạo thư mục", 14 | "Folder exists already!" : "Thư mục đã tồn tại!", 15 | "Credentials" : "Giấy chứng nhận", 16 | "Subscribe" : "Theo dõi", 17 | "Last job ran {relativeTime}." : "Công việc cuối cùng đã chạy {relativeTime}.", 18 | "Move" : "Dịch chuyển", 19 | "Share" : "Chia sẻ", 20 | "Off" : "Tắt", 21 | "Mark read" : "Đánh dấu là đã đọc", 22 | "All articles" : "Mọi bài viết", 23 | "Starred" : "Starred", 24 | "Settings" : "Cài đặt", 25 | "Documentation" : "Tài liệu hướng dẫn", 26 | "Keyboard shortcuts" : "Phím tắt", 27 | "Rename" : "Sửa tên", 28 | "Delete" : "Xóa", 29 | "Rename Feed" : "Đổi tên nguồn dữ liệu", 30 | "Newest first" : "Mới trước", 31 | "Oldest first" : "Cũ trước", 32 | "Mark unread" : "Đánh dấu là chưa đọc", 33 | "by" : "bởi", 34 | "Open website" : "Mở trang web", 35 | "Title" : "Tên", 36 | "Folder" : "Thư mục", 37 | "Not available" : "Không khả dụng", 38 | "Keyboard shortcut" : "Phím tắt", 39 | "Action" : "Hành động", 40 | "right" : "phải", 41 | "left" : "trái", 42 | "Refresh" : "Làm mới", 43 | "Shift" : "Shift" 44 | }, 45 | "nplurals=1; plural=0;"); 46 | -------------------------------------------------------------------------------- /lib/Plugin/Client/Plugin.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Plugin\Client; 15 | 16 | /** 17 | * TODO: remove this? it seems old.. there is no global list of plugins and seems plugins register themselves? 18 | * 19 | * We actually really want to avoid this global list of plugins. A way would be 20 | * for News plugin apps to register themselves in a special database table 21 | * and the News app would just pull out the scripts that should be attached 22 | * but atm there is no really good way since there is no uninstall hook which 23 | * would remove the plugin from the apps so fk it :) 24 | */ 25 | class Plugin 26 | { 27 | 28 | private static $scripts = []; 29 | private static $styles = []; 30 | 31 | public static function registerStyle($appName, $styleName): void 32 | { 33 | self::$styles[$appName] = $styleName; 34 | } 35 | 36 | public static function registerScript($appName, $scriptName): void 37 | { 38 | self::$scripts[$appName] = $scriptName; 39 | } 40 | 41 | public static function getStyles() 42 | { 43 | return self::$styles; 44 | } 45 | 46 | public static function getScripts() 47 | { 48 | return self::$scripts; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/Command/Config/FeedDelete.php: -------------------------------------------------------------------------------- 1 | feedService = $feedService; 23 | } 24 | 25 | /** 26 | * Configure command 27 | * 28 | * @return void 29 | */ 30 | protected function configure() 31 | { 32 | $this->setName('news:feed:delete') 33 | ->setDescription('Remove a feed') 34 | ->addArgument('user-id', InputArgument::REQUIRED, 'User to remove the feed from') 35 | ->addArgument('feed-id', InputArgument::REQUIRED, 'Feed ID', null); 36 | } 37 | 38 | /** 39 | * Execute command 40 | * 41 | * @param InputInterface $input 42 | * @param OutputInterface $output 43 | * 44 | * @return int 45 | */ 46 | protected function execute(InputInterface $input, OutputInterface $output): int 47 | { 48 | $user = $input->getArgument('user-id'); 49 | $id = (int) $input->getArgument('feed-id'); 50 | 51 | $this->feedService->delete($user, $id); 52 | 53 | return 0; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /lib/Migration/Version150302Date20210312231251.php: -------------------------------------------------------------------------------- 1 | hasTable('news_items')) { 36 | $table = $schema->getTable('news_items'); 37 | $table->addColumn('categories_json', 'json', [ 38 | 'notnull' => false 39 | ]); 40 | } 41 | 42 | return $schema; 43 | } 44 | 45 | /** 46 | * @param IOutput $output 47 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 48 | * @param array $options 49 | */ 50 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/Controller/FaviconController.php: -------------------------------------------------------------------------------- 1 | appData->getFileContent(Constants::LOGO_INFO_DIR, 'img_'.$feedUrlHash); 41 | if ($logo_hash) { 42 | $feed_logo = $this->appData->getFileContent(Constants::LOGO_IMAGE_DIR, $logo_hash); 43 | } 44 | if (!$feed_logo) { 45 | $feed_logo = file_get_contents(__DIR__ . '/../../img/rss.svg'); 46 | } 47 | $finfo = new \finfo(FILEINFO_MIME_TYPE); 48 | $mime = $finfo->buffer($feed_logo); 49 | return new DataDownloadResponse($feed_logo, $feedUrlHash, $mime); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /docs/features/integration.md: -------------------------------------------------------------------------------- 1 | # Integrations 2 | 3 | ## Is There An Subscription URL To Easily Subscribe To Feeds? 4 | 5 | By appending `/index.php/apps/news?subscribe_to=SOME_RSS_URL` to your NextCloud base path URL, you can launch the News app with a pre-filled URL, e.g.: 6 | 7 | Ex. 8 | 9 | https://yourdomain.com/nextcloud/index.php/apps/news?subscribe_to=https://github.com/nextcloud/news/releases 10 | 11 | ### Known Working Integrations 12 | 13 | #### Chrome / Edge 14 | 15 | 1. Install [RSS Subscription Extension (by Google)](https://chrome.google.com/webstore/detail/rss-subscription-extensio/nlbjncdgjeocebhnmkbbbdekmmmcbfjd) extension 16 | 1. Open the extension's options menu 17 | 1. Click `Add..` 18 | 1. In the *Description* field, enter a description for the RSS reader entry. 'NextCloud News' is a reasonable name. 19 | 1. Enter `https:///index.php/apps/news?subscribe_to=%s` replacing <NEXTCLOUD_BASE_PATH> with the base URL path to your NextCloud instance. 20 | * Domain based example: 21 | * Domain+subpath based example: 22 | 23 | #### Firefox 24 | 25 | 1. Install Firefox Add-on Extension [Awesome RSS](https://addons.mozilla.org/en-US/firefox/addon/awesome-rss/) 26 | 1. Open the `Preferences` for the extension 27 | 1. In the 'Subscribe using' section, select the `NextCloud` radio button 28 | 1. In the field link field, enter the base NextCloud URL. 29 | * Domain based example: 30 | * Domain+subpath based example: 31 | -------------------------------------------------------------------------------- /l10n/mk.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "Непрочитани статии", 3 | "News" : "Новости", 4 | "Download" : "Преземи", 5 | "Close" : "Затвори", 6 | "Folder name" : "Име на папка", 7 | "Username" : "Корисничко име", 8 | "Password" : "Лозинка", 9 | "Feed exists already!" : "Каналот веќе постои!", 10 | "New folder" : "Нова папка", 11 | "Credentials" : "Акредитиви", 12 | "Subscribe" : "Претплата", 13 | "Last job ran {relativeTime}." : "Последната процедура се изврши пред {relativeTime}.", 14 | "Move" : "Премести", 15 | "Share" : "Сподели", 16 | "Default" : "Предефиниран", 17 | "Vertical" : "Вертикално", 18 | "Horizontal" : "Хоризонтално", 19 | "Mark read" : "Означи како прочитано", 20 | "All articles" : "Сите статии", 21 | "Starred" : "Со ѕвезда", 22 | "Explore" : "Истражувај", 23 | "Settings" : "Параметри", 24 | "Documentation" : "Документација", 25 | "Keyboard shortcuts" : "Кратенки преку тастатура", 26 | "Rename" : "Преименувај", 27 | "Delete" : "Избриши", 28 | "Newest first" : "Новите прво", 29 | "Oldest first" : "Старите прво", 30 | "Mark unread" : "Означи како непрочитано", 31 | "by" : "од", 32 | "Open website" : "Отвори вебсајт", 33 | "Title" : "Наслов", 34 | "Folder" : "Папка", 35 | "Not available" : "Недостапно", 36 | "Keyboard shortcut" : "Кратенка преку тастатура", 37 | "Action" : "Акција", 38 | "right" : "десно", 39 | "left" : "лево", 40 | "Refresh" : "Освежи", 41 | "Load next feed" : "Вчитај следен канал", 42 | "Shift" : "Shift" 43 | },"pluralForm" :"nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;" 44 | } -------------------------------------------------------------------------------- /lib/Migration/Version250200Date20241219085150.php: -------------------------------------------------------------------------------- 1 | getTable('news_feeds'); 40 | if (!$table->hasColumn('next_update_time')) { 41 | $table->addColumn('next_update_time', 'bigint', [ 42 | 'notnull' => false 43 | ]); 44 | } 45 | 46 | return $schema; 47 | } 48 | 49 | /** 50 | * @param IOutput $output 51 | * @param Closure(): ISchemaWrapper $schemaClosure 52 | * @param array $options 53 | */ 54 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/Fetcher/FaviconDataAccess.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2024 Ben Vidulich 10 | */ 11 | 12 | namespace OCA\News\Fetcher; 13 | 14 | use OCA\News\Vendor\Favicon\DataAccess; 15 | 16 | use OCA\News\Config\FetcherConfig; 17 | 18 | /** 19 | * Modified version of DataAccess with a configurable user agent header. 20 | */ 21 | class FaviconDataAccess extends DataAccess 22 | { 23 | /** 24 | * @var FetcherConfig 25 | */ 26 | private $fetcherConfig; 27 | 28 | public function __construct( 29 | FetcherConfig $fetcherConfig, 30 | ) { 31 | $this->fetcherConfig = $fetcherConfig; 32 | } 33 | 34 | public function retrieveUrl($url) 35 | { 36 | $this->setContext(); 37 | return @file_get_contents($url); 38 | } 39 | 40 | public function retrieveHeader($url) 41 | { 42 | $this->setContext(); 43 | $headers = @get_headers($url, 1); 44 | return is_array($headers) ? array_change_key_case($headers) : []; 45 | } 46 | 47 | private function setContext() 48 | { 49 | stream_context_set_default( 50 | [ 51 | 'http' => [ 52 | 'method' => 'GET', 53 | 'follow_location' => 0, 54 | 'max_redirects' => 1, 55 | 'timeout' => 10, 56 | 'header' => 'User-Agent: ' . $this->fetcherConfig->getUserAgent(), 57 | ] 58 | ] 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/javascript/unit/components/App.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils' 2 | import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' 3 | import App from '../../../../src/App.vue' 4 | import { MUTATIONS } from '../../../../src/store/index.ts' 5 | 6 | describe('FeedItemDisplay.vue', () => { 7 | 'use strict' 8 | let wrapper: any 9 | 10 | const dispatchStub = vi.fn() 11 | const commitStub = vi.fn() 12 | beforeAll(() => { 13 | wrapper = shallowMount(App, { 14 | global: { 15 | mocks: { 16 | $store: { 17 | state: { 18 | items: { 19 | playingItem: undefined, 20 | }, 21 | app: { 22 | error: undefined, 23 | }, 24 | }, 25 | dispatch: dispatchStub, 26 | commit: commitStub, 27 | }, 28 | }, 29 | stubs: { 30 | RouterView: true, 31 | }, 32 | }, 33 | }) 34 | }) 35 | 36 | beforeEach(() => { 37 | dispatchStub.mockReset() 38 | commitStub.mockReset() 39 | }) 40 | 41 | it('should send SET_PLAYING_ITEM with undefined to stop playback', () => { 42 | wrapper.vm.stopPlaying() 43 | 44 | expect(commitStub).toBeCalledWith(MUTATIONS.SET_PLAYING_ITEM, undefined) 45 | }) 46 | 47 | it('should stop all video elements in page when playing video', () => { 48 | const pauseStub = vi.fn() 49 | document.getElementsByTagName = vi.fn().mockReturnValue([{ pause: pauseStub }]) 50 | 51 | wrapper.vm.stopVideo() 52 | 53 | expect(pauseStub).toBeCalled() 54 | }) 55 | 56 | it('should remove app state error with commit and undefined', () => { 57 | wrapper.vm.removeError() 58 | 59 | expect(commitStub).toBeCalledWith(MUTATIONS.SET_ERROR, undefined) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /l10n/mk.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "Непрочитани статии", 5 | "News" : "Новости", 6 | "Download" : "Преземи", 7 | "Close" : "Затвори", 8 | "Folder name" : "Име на папка", 9 | "Username" : "Корисничко име", 10 | "Password" : "Лозинка", 11 | "Feed exists already!" : "Каналот веќе постои!", 12 | "New folder" : "Нова папка", 13 | "Credentials" : "Акредитиви", 14 | "Subscribe" : "Претплата", 15 | "Last job ran {relativeTime}." : "Последната процедура се изврши пред {relativeTime}.", 16 | "Move" : "Премести", 17 | "Share" : "Сподели", 18 | "Default" : "Предефиниран", 19 | "Vertical" : "Вертикално", 20 | "Horizontal" : "Хоризонтално", 21 | "Mark read" : "Означи како прочитано", 22 | "All articles" : "Сите статии", 23 | "Starred" : "Со ѕвезда", 24 | "Explore" : "Истражувај", 25 | "Settings" : "Параметри", 26 | "Documentation" : "Документација", 27 | "Keyboard shortcuts" : "Кратенки преку тастатура", 28 | "Rename" : "Преименувај", 29 | "Delete" : "Избриши", 30 | "Newest first" : "Новите прво", 31 | "Oldest first" : "Старите прво", 32 | "Mark unread" : "Означи како непрочитано", 33 | "by" : "од", 34 | "Open website" : "Отвори вебсајт", 35 | "Title" : "Наслов", 36 | "Folder" : "Папка", 37 | "Not available" : "Недостапно", 38 | "Keyboard shortcut" : "Кратенка преку тастатура", 39 | "Action" : "Акција", 40 | "right" : "десно", 41 | "left" : "лево", 42 | "Refresh" : "Освежи", 43 | "Load next feed" : "Вчитај следен канал", 44 | "Shift" : "Shift" 45 | }, 46 | "nplurals=2; plural=(n % 10 == 1 && n % 100 != 11) ? 0 : 1;"); 47 | -------------------------------------------------------------------------------- /lib/Controller/FaviconApiController.php: -------------------------------------------------------------------------------- 1 | appData->getFileContent(Constants::LOGO_INFO_DIR, 'img_'.$feedUrlHash); 43 | if ($logo_hash) { 44 | $feed_logo = $this->appData->getFileContent(Constants::LOGO_IMAGE_DIR, $logo_hash); 45 | } 46 | if (!$feed_logo) { 47 | $feed_logo = file_get_contents(__DIR__ . '/../../img/rss.svg'); 48 | } 49 | $finfo = new \finfo(FILEINFO_MIME_TYPE); 50 | $mime = $finfo->buffer($feed_logo); 51 | return new DataDownloadResponse($feed_logo, $feedUrlHash, $mime); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/routes/All.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 62 | -------------------------------------------------------------------------------- /lib/Controller/JSONHttpErrorTrait.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright Bernhard Posselt 2014 10 | */ 11 | 12 | namespace OCA\News\Controller; 13 | 14 | use \OCP\AppFramework\Http\JSONResponse; 15 | 16 | trait JSONHttpErrorTrait 17 | { 18 | /** 19 | * @param \Exception $exception The exception to report 20 | * @param int $code The http error code 21 | * @return JSONResponse 22 | */ 23 | public function error(\Exception $exception, int $code) 24 | { 25 | return new JSONResponse(['message' => $exception->getMessage()], $code); 26 | } 27 | 28 | /** 29 | * @param \Exception $exception 30 | * @param int $code 31 | * @return \OCP\AppFramework\Http\JSONResponse 32 | */ 33 | public function errorResponseWithExceptionV2(\Exception $exception, int $code): JSONResponse 34 | { 35 | return $this->errorResponseV2( 36 | $exception->getMessage(), 37 | $exception->getCode(), 38 | $code 39 | ); 40 | } 41 | 42 | /** 43 | * @param string $message 44 | * @param int $code 45 | * @param int $httpStatusCode 46 | * @return \OCP\AppFramework\Http\JSONResponse 47 | */ 48 | public function errorResponseV2(string $message, int $code, int $httpStatusCode): JSONResponse 49 | { 50 | return new JSONResponse([ 51 | 'error' => [ 52 | 'code' => $code, 53 | 'message' => $message, 54 | ] 55 | ], $httpStatusCode); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /lib/Migration/Version150400Date20210318215425.php: -------------------------------------------------------------------------------- 1 | hasTable('news_items')) { 36 | $table = $schema->getTable('news_items'); 37 | $table->addColumn('shared_by', 'string', [ 38 | 'notnull' => false, 39 | 'length' => 64 40 | ]); 41 | } 42 | 43 | return $schema; 44 | } 45 | 46 | /** 47 | * @param IOutput $output 48 | * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` 49 | * @param array $options 50 | */ 51 | public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Nextcloud Development Environment 2 | 3 | ## Installation / Running 4 | 5 | ```bash 6 | make build 7 | docker compose up 8 | ``` 9 | 10 | Afterward you should be able to open (admin/admin) to 11 | log in to your Nextcloud instance. 12 | 13 | Press Ctrl+C to stop the container. 14 | 15 | ## Check nextcloud.log 16 | 17 | For debugging, you can show the `nextcloud.log`: 18 | 19 | ```bash 20 | make show-log 21 | ``` 22 | 23 | There also is a [logging web interface](http://localhost:8081/index.php/settings/admin/logging). 24 | 25 | ## Create shell in docker container 26 | 27 | To check if the container is still running: 28 | ```bash 29 | docker ps 30 | ``` 31 | It should show something like this: 32 | ```bash 33 | CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 34 | a1b2c3d4e5f6 nextcloud-news-app "docker-entrypoint.…" 2 hours ago Up 2 hours 0.0.0.0:8081->80/tcp nextcloud-news-app 35 | ``` 36 | 37 | To open a shell as www-data which you need to run occ commands run: 38 | ``` bash 39 | docker exec -u www-data -it nextcloud-news-app /bin/bash 40 | ``` 41 | 42 | To open a shell as root run 43 | ``` bash 44 | docker exec -it nextcloud-news-app /bin/bash 45 | ``` 46 | 47 | To exit press Ctrl+D 48 | 49 | ### Inside the shell 50 | 51 | Use sqlite3 to open the db 52 | 53 | ```bash 54 | sqlite3 data/mydb.db 55 | ``` 56 | 57 | More on the sqlite3 cli: https://www.sqlite.org/cli.html 58 | 59 | ## Tip 60 | 61 | In case something is broken try to reset the container: 62 | 63 | ```bash 64 | docker compose build; docker compose down; docker volume rm nextcloud-news_nextcloud 65 | ``` 66 | -------------------------------------------------------------------------------- /lib/Settings/AdminSettings.php: -------------------------------------------------------------------------------- 1 | initialState->provideInitialState($setting, $this->config->getValueString( 26 | Application::NAME, 27 | $setting, 28 | (string) Application::DEFAULT_SETTINGS[$setting] 29 | )); 30 | } 31 | 32 | if ($this->service->isCronProperlyConfigured()) { 33 | $lastUpdate = $this->service->getUpdateTime(); 34 | } else { 35 | $lastUpdate = 0; 36 | } 37 | 38 | $this->initialState->provideInitialState("lastCron", $lastUpdate); 39 | 40 | $lastLogoPurge = $this->config->getValueInt( 41 | Application::NAME, 42 | 'lastLogoPurge', 43 | 0 44 | ); 45 | $this->initialState->provideInitialState("lastLogoPurge", $lastLogoPurge); 46 | 47 | return new TemplateResponse(Application::NAME, 'admin', []); 48 | } 49 | 50 | public function getSection() 51 | { 52 | return 'news'; 53 | } 54 | 55 | public function getPriority() 56 | { 57 | return 40; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/Cron/UpdaterJob.php: -------------------------------------------------------------------------------- 1 | 9 | * @copyright 2012-2014 Bernhard Posselt 10 | */ 11 | 12 | namespace OCA\News\Cron; 13 | 14 | use OCP\BackgroundJob\TimedJob; 15 | use OCP\AppFramework\Utility\ITimeFactory; 16 | 17 | use OCA\News\AppInfo\Application; 18 | use OCA\News\Service\StatusService; 19 | use OCA\News\Service\UpdaterService; 20 | use OCP\IAppConfig; 21 | 22 | class UpdaterJob extends TimedJob 23 | { 24 | 25 | public function __construct( 26 | ITimeFactory $time, 27 | private IAppConfig $config, 28 | private StatusService $statusService, 29 | private UpdaterService $updaterService 30 | ) { 31 | parent::__construct($time); 32 | 33 | $interval = $this->config->getValueString( 34 | Application::NAME, 35 | 'updateInterval', 36 | Application::DEFAULT_SETTINGS['updateInterval'] 37 | ); 38 | 39 | $this->setInterval($interval); 40 | } 41 | 42 | /** 43 | * @return void 44 | */ 45 | protected function run($argument): void 46 | { 47 | $uses_cron = $this->config->getValueBool( 48 | Application::NAME, 49 | 'useCronUpdates', 50 | Application::DEFAULT_SETTINGS['useCronUpdates'] 51 | ); 52 | 53 | if (!$uses_cron || !$this->statusService->isCronProperlyConfigured()) { 54 | return; 55 | } 56 | 57 | $this->updaterService->beforeUpdate(); 58 | $this->updaterService->update(); 59 | $this->updaterService->afterUpdate(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tests/javascript/unit/services/folder.service.spec.ts: -------------------------------------------------------------------------------- 1 | import axios from '@nextcloud/axios' 2 | import { beforeEach, describe, expect, it } from 'vitest' 3 | import { FolderService } from './../../../../src/dataservices/folder.service' 4 | 5 | describe('folder.service.ts', () => { 6 | 'use strict' 7 | 8 | beforeEach(() => { 9 | axios.get.mockReset() 10 | axios.post.mockReset() 11 | axios.delete.mockReset() 12 | }) 13 | 14 | describe('fetchAllFolders', () => { 15 | it('should call GET to retrieve all folders', async () => { 16 | axios.get.mockResolvedValue({ data: { feeds: [] } }) 17 | 18 | await FolderService.fetchAllFolders() 19 | 20 | expect(axios.get).toBeCalled() 21 | }) 22 | }) 23 | 24 | describe('createFolder', () => { 25 | it('should call POST with folderName param', async () => { 26 | await FolderService.createFolder({ name: 'abc' }) 27 | 28 | expect(axios.post).toBeCalled() 29 | const args = axios.post.mock.calls[0] 30 | 31 | expect(args[1].folderName).toEqual('abc') 32 | }) 33 | }) 34 | 35 | describe('renameFolder', () => { 36 | it('should call POST with item id in URL and folderName param', async () => { 37 | await FolderService.renameFolder({ id: 123, name: 'abc' }) 38 | 39 | expect(axios.post).toBeCalled() 40 | const args = axios.post.mock.calls[0] 41 | 42 | expect(args[0]).toContain('123/rename') 43 | expect(args[1].folderName).toEqual('abc') 44 | }) 45 | }) 46 | 47 | describe('deleteFolder', () => { 48 | it('should call POST with item id in URL and read param', async () => { 49 | await FolderService.deleteFolder({ id: 123 }) 50 | 51 | expect(axios.delete).toBeCalled() 52 | const args = axios.delete.mock.calls[0] 53 | 54 | expect(args[0]).toContain('123') 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /lib/Controller/ApiPayloadTrait.php: -------------------------------------------------------------------------------- 1 | toAPI()]; 25 | } 26 | 27 | if (!is_array($data)) { 28 | return $return; 29 | } 30 | 31 | foreach ($data as $entity) { 32 | if ($entity instanceof IAPI) { 33 | $return[] = $entity->toAPI(); 34 | } 35 | } 36 | return $return; 37 | } 38 | 39 | /** 40 | * Serialize an entity 41 | * 42 | * @param IAPI $data 43 | * 44 | * @return array 45 | */ 46 | public function serializeEntityV2($data, bool $reduced = false): array 47 | { 48 | return $data->toAPI2($reduced); 49 | } 50 | 51 | /** 52 | * Serialize array of entities 53 | * 54 | * @param array $data 55 | * 56 | * @return array 57 | */ 58 | public function serializeEntitiesV2($data, bool $reduced = false): array 59 | { 60 | $return = []; 61 | foreach ($data as $entity) { 62 | $return[] = $entity->toAPI2($reduced); 63 | } 64 | return $return; 65 | } 66 | 67 | public function responseV2($data, $code = Http::STATUS_OK) 68 | { 69 | return new JSONResponse($data, $code); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /lib/Controller/Controller.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @author David Guillot 11 | * @copyright 2020 Sean Molenaar 12 | */ 13 | 14 | namespace OCA\News\Controller; 15 | 16 | use OCA\News\AppInfo\Application; 17 | use OCA\News\Controller\Exceptions\NotLoggedInException; 18 | use OCP\AppFramework\Controller as BaseController; 19 | use \OCP\IUser; 20 | use \OCP\IRequest; 21 | use \OCP\IUserSession; 22 | 23 | /** 24 | * Class ApiController 25 | * 26 | * @package OCA\News\Controller 27 | */ 28 | class Controller extends BaseController 29 | { 30 | 31 | /** 32 | * ApiController constructor. 33 | * 34 | * Stores the user session to be able to leverage the user in further methods 35 | * 36 | * @param IRequest $request The request 37 | * @param IUserSession|null $userSession The user session 38 | */ 39 | public function __construct(IRequest $request, private ?IUserSession $userSession) 40 | { 41 | parent::__construct(Application::NAME, $request); 42 | } 43 | 44 | /** 45 | * @return IUser|null 46 | */ 47 | protected function getUser(): ?IUser 48 | { 49 | if ($this->userSession === null) { 50 | throw new NotLoggedInException(); 51 | } 52 | 53 | return $this->userSession->getUser(); 54 | } 55 | 56 | /** 57 | * @return string 58 | */ 59 | protected function getUserId() 60 | { 61 | return $this->getUser()->getUID(); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/utils/itemFilter.ts: -------------------------------------------------------------------------------- 1 | import { FEED_ORDER } from '../enums/index.ts' 2 | 3 | /** 4 | * get sorting for the actual route including individual feed order 5 | * 6 | * @param store - The vuex store instance containing application state 7 | * @param fetchKey - A key used for the selected route 8 | * @return Sort order oldestFirst 9 | */ 10 | export function getOldestFirst(store, fetchKey: string): boolean { 11 | const feedOrdering = store.state.feeds?.ordering?.[fetchKey] 12 | if (!fetchKey.startsWith('feed-') || feedOrdering === FEED_ORDER.DEFAULT) { 13 | return store.getters.oldestFirst 14 | } 15 | return feedOrdering === FEED_ORDER.OLDEST 16 | } 17 | 18 | /** 19 | * filter out items that are already loaded but not in view range 20 | * 21 | * @param store - The vuex store instance containing application state 22 | * @param items - An array of feed items to be filtered 23 | * @param fetchKey - A key used for the selected route 24 | * @return The filtered array of feed items. 25 | */ 26 | export function outOfScopeFilter(store, items: FeedItem[], fetchKey: string): FeedItem[] { 27 | const lastItemLoaded = store.state.items.lastItemLoaded?.[fetchKey] 28 | const oldestFirst = getOldestFirst(store, fetchKey) 29 | 30 | if (!lastItemLoaded) { 31 | return items 32 | } 33 | return items.filter((item) => { 34 | return (oldestFirst ? lastItemLoaded >= item.id : lastItemLoaded <= item.id) 35 | }) 36 | } 37 | 38 | /** 39 | * sort array of feed items 40 | * 41 | * @param items - An array of feed items to be sorted 42 | * @param oldestFirst - Direction of sorting 43 | * @return The sorted array of feed items 44 | */ 45 | export function sortedFeedItems(items: feedItem[], oldestFirst: boolean): FeedItem[] { 46 | return [...items].sort((a, b) => { 47 | return oldestFirst ? a.id - b.id : b.id - a.id 48 | }) 49 | } 50 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Nextcloud News App 2 | site_url: https://nextcloud.github.io/news/ 3 | site_description: Documentation for the Nextcloud News App 4 | site_author: Nextcloud News Team 5 | repo_url: https://github.com/nextcloud/news 6 | edit_uri: blob/master/documentation/ 7 | 8 | nav: 9 | - index.md 10 | - install.md 11 | - clients.md 12 | - user.md 13 | - admin.md 14 | - developer.md 15 | - troubleshooting.md 16 | - Features: 17 | - Integration: features/integration.md 18 | - Custom CSS: features/customCSS.md 19 | - Plugins: features/plugins.md 20 | - Themes: features/themes.md 21 | - REST API: 22 | - API v1.2: api/api-v1-2.md 23 | - API v1.3: api/api-v1-3.md 24 | - API v2: api/api-v2.md 25 | - maintenance.md 26 | 27 | theme: 28 | name: material 29 | logo: assets/logo.svg 30 | favicon: assets/favicon.png 31 | font: false 32 | palette: 33 | - media: "(prefers-color-scheme: light)" 34 | scheme: default 35 | accent: indigo 36 | primary: indigo 37 | toggle: 38 | icon: material/toggle-switch-off-outline 39 | name: Switch to dark mode 40 | - media: "(prefers-color-scheme: dark)" 41 | scheme: slate 42 | primary: indigo 43 | accent: indigo 44 | toggle: 45 | icon: material/toggle-switch 46 | name: Switch to light mode 47 | 48 | features: 49 | - navigation.indexes 50 | - navigation.tracking 51 | - navigation.instant 52 | - navigation.expand 53 | - navigation.sections 54 | 55 | markdown_extensions: 56 | - admonition 57 | - pymdownx.highlight: 58 | anchor_linenums: true 59 | line_spans: "code-line" 60 | pygments_lang_class: true 61 | - pymdownx.inlinehilite 62 | - pymdownx.snippets 63 | - pymdownx.superfences 64 | 65 | # extra: 66 | # version: 67 | # provider: mike 68 | -------------------------------------------------------------------------------- /lib/Command/Config/FolderDelete.php: -------------------------------------------------------------------------------- 1 | folderService = $folderService; 24 | } 25 | 26 | /** 27 | * Configure command 28 | * 29 | * @return void 30 | */ 31 | protected function configure() 32 | { 33 | $this->setName('news:folder:delete') 34 | ->setDescription('Remove a folder') 35 | ->addArgument('user-id', InputArgument::REQUIRED, 'User to remove the folder from') 36 | ->addArgument('folder-id', InputArgument::REQUIRED, 'Folder ID', null); 37 | } 38 | 39 | /** 40 | * Execute command 41 | * 42 | * @param InputInterface $input 43 | * @param OutputInterface $output 44 | * 45 | * @return int 46 | */ 47 | protected function execute(InputInterface $input, OutputInterface $output): int 48 | { 49 | $user = $input->getArgument('user-id'); 50 | $id = $input->getArgument('folder-id'); 51 | 52 | if ($id === null) { 53 | throw new ServiceValidationException('Can not remove root folder!'); 54 | } 55 | 56 | $this->folderService->delete($user, intval($id)); 57 | 58 | return 0; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/command/folders.bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bats 2 | 3 | load "helpers/settings" 4 | 5 | TESTSUITE="Folders" 6 | 7 | teardown() { 8 | ID_LIST=($(./occ news:feed:list 'admin' | grep -Po '"id": \K([0-9]+)' | tr '\n' ' ')) 9 | for ID in $ID_LIST; do 10 | ./occ news:feed:delete "$user" "$ID" 11 | done 12 | } 13 | 14 | @test "[$TESTSUITE] Create new" { 15 | run ./occ news:folder:add "$user" "Something-${BATS_SUITE_TEST_NUMBER}" 16 | [ "$status" -eq 0 ] 17 | 18 | 19 | if echo "$output" | grep 'new folder'; then 20 | ret_status=$? 21 | echo "Folder ID not returned" 22 | return $ret_status 23 | fi 24 | } 25 | 26 | @test "[$TESTSUITE] List all" { 27 | ./occ news:folder:add "$user" "Something-${BATS_SUITE_TEST_NUMBER}" 28 | 29 | run ./occ news:folder:list "$user" 30 | [ "$status" -eq 0 ] 31 | 32 | if echo "$output" | grep "Something-${BATS_SUITE_TEST_NUMBER}"; then 33 | ret_status=$? 34 | echo "Folder not found in list" 35 | return $ret_status 36 | fi 37 | } 38 | 39 | @test "[$TESTSUITE] Read all" { 40 | ./occ news:folder:add "$user" "Something-${BATS_SUITE_TEST_NUMBER}" 41 | 42 | ID=$(./occ news:folder:list 'admin' | grep "Something-${BATS_SUITE_TEST_NUMBER}" -1 | head -1 | grep -oE '[0-9]*') 43 | 44 | run ./occ news:folder:read "$user" "$ID" -v 45 | [ "$status" -eq 0 ] 46 | 47 | if ! echo "$output" | grep "items as read"; then 48 | ret_status=$? 49 | echo "Folder not read" 50 | return $ret_status 51 | fi 52 | } 53 | 54 | @test "[$TESTSUITE] Delete all" { 55 | ID=$(./occ news:folder:add "$user" "Something-${BATS_SUITE_TEST_NUMBER}" | grep -oE '[0-9]*') 56 | 57 | run ./occ news:folder:list "$user" 58 | [ "$status" -eq 0 ] 59 | 60 | echo "$output" | grep "Something-${BATS_SUITE_TEST_NUMBER}" 61 | 62 | run ./occ news:folder:delete "$user" "$ID" 63 | [ "$status" -eq 0 ] 64 | } 65 | -------------------------------------------------------------------------------- /src/components/routes/Starred.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 65 | -------------------------------------------------------------------------------- /lib/Command/Config/OpmlImport.php: -------------------------------------------------------------------------------- 1 | opmlService = $opmlService; 23 | } 24 | 25 | /** 26 | * Configure command 27 | * 28 | * @return void 29 | */ 30 | protected function configure() 31 | { 32 | $this->setName('news:opml:import') 33 | ->setDescription('Import OPML file') 34 | ->addArgument('user-id', InputArgument::REQUIRED, 'User to import data for') 35 | ->addArgument('file', InputArgument::REQUIRED, 'Data to import'); 36 | } 37 | 38 | /** 39 | * Execute command 40 | * 41 | * @param InputInterface $input 42 | * @param OutputInterface $output 43 | * 44 | * @return int 45 | */ 46 | protected function execute(InputInterface $input, OutputInterface $output): int 47 | { 48 | $user = $input->getArgument('user-id'); 49 | $data = file_get_contents($input->getArgument('file')); 50 | if ($data === false) { 51 | $output->writeln('Failed to read data file!'); 52 | return 2; 53 | } 54 | 55 | $success = $this->opmlService->import($user, $data); 56 | if ($success === false) { 57 | $output->write("Failed to import data"); 58 | return 1; 59 | } 60 | 61 | return 0; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nextcloud News app 2 | ![Release status](https://github.com/nextcloud/news/workflows/Build%20and%20publish%20app%20release/badge.svg) ![Integration Tests](https://github.com/nextcloud/news/workflows/Integration%20Tests/badge.svg) ![Frontend tests](https://github.com/nextcloud/news/workflows/Frontend%20tests/badge.svg) 3 | 4 | The News app is an RSS/Atom feed aggregator. It offers a [RESTful API](https://nextcloud.github.io/news/developer/#apis) for app developers. The source code is [available on GitHub](https://github.com/nextcloud/news) 5 | 6 | You can install it via the integrated app store browser in Nextcloud, [News in the appstore](https://apps.nextcloud.com/apps/news). 7 | 8 | ## Documentation 9 | The documentation can be found [here](https://nextcloud.github.io/news/), the source of the documentation is on [GitHub](https://github.com/nextcloud/news/blob/master/docs) 10 | 11 | ### Common Guides 12 | 13 | There are some small guides for dealing with common setup and issues. 14 | 15 | * [Troubleshooting Guide](docs/troubleshooting.md) 16 | * [Integration Guide](docs/features/integration.md) 17 | 18 | 19 | ## Bugs 20 | Please read the [appropriate section in the contributing notices](https://github.com/nextcloud/news/blob/master/CONTRIBUTING.md#issues) 21 | 22 | ## Updating Notices 23 | To receive notifications when a new News app version was released, simply add the following Atom feed in your currently installed News app: 24 | 25 | https://github.com/nextcloud/news/releases.atom 26 | 27 | ## Screenshots 28 | ![](https://raw.githubusercontent.com/nextcloud/news/master/screenshots/1.png) 29 | 30 | ## Maintainers 31 | 32 | * [Benjamin Brahmer](https://github.com/Grotax) 33 | * [Sean Molenaar](https://github.com/SMillerDev) 34 | 35 | ### Special thanks to the Feed-IO library 36 | Feed-io takes care of fetching feeds and processing them. 37 | [alexdebril/feed-io](https://github.com/alexdebril/feed-io) 38 | -------------------------------------------------------------------------------- /.github/workflows/api-php-tests.yml: -------------------------------------------------------------------------------- 1 | name: PHP Tests 2 | on: 3 | pull_request 4 | 5 | jobs: 6 | php: 7 | runs-on: ubuntu-latest 8 | continue-on-error: ${{ matrix.experimental }} 9 | name: "PHP: Nextcloud ${{ matrix.nextcloud }} - PHP ${{ matrix.php-versions }} - DB ${{ matrix.database }}" 10 | strategy: 11 | matrix: 12 | php-versions: ['8.3'] 13 | nextcloud: ['stable31'] 14 | database: ['sqlite'] 15 | experimental: [false] 16 | include: 17 | - php-versions: 8.3 18 | nextcloud: stable31 19 | database: sqlite 20 | experimental: false 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v6.0.1 24 | with: 25 | fetch-depth: 2 # https://github.com/codecov/codecov-action/issues/190#issuecomment-790729633 26 | 27 | - name: Setup PHP 28 | uses: shivammathur/setup-php@v2 29 | with: 30 | php-version: ${{ matrix.php-versions }} 31 | extensions: pdo_sqlite,pdo_mysql,pdo_pgsql,gd,zip 32 | coverage: pcov 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | 36 | ### Back to normal setup 37 | - name: Set up server non MySQL 38 | uses: SMillerDev/nextcloud-actions/setup-nextcloud@main 39 | with: 40 | cron: true 41 | version: ${{ matrix.nextcloud }} 42 | database-type: ${{ matrix.database }} 43 | 44 | - name: Prime app build 45 | run: make 46 | 47 | - name: Configure server with app 48 | uses: SMillerDev/nextcloud-actions/setup-nextcloud-app@main 49 | with: 50 | app: 'news' 51 | check-code: false 52 | 53 | - name: Prep PHP tests 54 | working-directory: ../server/apps/news 55 | run: make php-test-dependencies 56 | 57 | - name: Unittests 58 | working-directory: ../server/apps/news 59 | run: make unit-test 60 | -------------------------------------------------------------------------------- /tests/javascript/unit/components/routes/Item.spec.ts: -------------------------------------------------------------------------------- 1 | import type { Store } from 'vuex' 2 | 3 | import { shallowMount } from '@vue/test-utils' 4 | import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest' 5 | import Vuex from 'vuex' 6 | import ContentTemplate from '../../../../../src/components/ContentTemplate.vue' 7 | import Item from '../../../../../src/components/routes/Item.vue' 8 | 9 | describe('Item.vue', () => { 10 | 'use strict' 11 | let wrapper: any 12 | 13 | const mockItems = [ 14 | { 15 | id: 123, 16 | }, 17 | ] 18 | 19 | let store: Store 20 | beforeAll(() => { 21 | store = new Vuex.Store({ 22 | state: { 23 | items: { 24 | fetchingItems: { 25 | all: false, 26 | }, 27 | lastItemLoaded: { 28 | all: 1, 29 | }, 30 | allItems: mockItems, 31 | }, 32 | feeds: { 33 | }, 34 | app: { 35 | oldestFirst: false, 36 | }, 37 | }, 38 | actions: { 39 | }, 40 | getters: { 41 | allItems: () => mockItems, 42 | oldestFirst: (state) => state.app.oldestFirst, 43 | }, 44 | }) 45 | 46 | store.dispatch = vi.fn() 47 | store.commit = vi.fn() 48 | 49 | wrapper = shallowMount(Item, { 50 | props: { 51 | itemId: '123', 52 | }, 53 | global: { 54 | plugins: [store], 55 | }, 56 | }) 57 | }) 58 | 59 | beforeEach(() => { 60 | vi.clearAllMocks() 61 | }) 62 | 63 | it('should show item with id 123', () => { 64 | expect((wrapper.findComponent(ContentTemplate)).props().items[0].id).toEqual(123) 65 | }) 66 | 67 | it('should dispatch FETCH_ITEMS action if not fetchingItems.all', () => { 68 | wrapper.vm.$store.state.items.fetchingItems.all = false 69 | wrapper.vm.fetchMore() 70 | expect(store.dispatch).toBeCalled() 71 | }) 72 | 73 | it('should not dispatch FETCH_ITEMS action if fetchingItems.all', () => { 74 | wrapper.vm.$store.state.items.fetchingItems.all = true 75 | wrapper.vm.fetchMore() 76 | expect(store.dispatch).not.toBeCalled() 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /lib/Fetcher/IFeedFetcher.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Fetcher; 15 | 16 | use OCA\News\Vendor\FeedIo\Reader\ReadErrorException; 17 | use OCA\News\Db\Feed; 18 | use OCA\News\Db\Item; 19 | 20 | interface IFeedFetcher 21 | { 22 | 23 | /** 24 | * Fetch feed content. 25 | * 26 | * @param string $url remote url of the feed 27 | * @param bool $fullTextEnabled If true use a scraper to download the full article 28 | * @param string|null $user if given, basic auth is set for this feed 29 | * @param string|null $password if given, basic auth is set for this feed. Ignored if user is empty 30 | * @param string|null $httpLastModified if given, will be used when sending a request to servers 31 | * 32 | * @return array an array containing the new feed and its items, first 33 | * element being the Feed and second element being an array of Items 34 | * 35 | * @throws ReadErrorException if the Feed-IO fetcher encounters a problem 36 | */ 37 | public function fetch( 38 | string $url, 39 | bool $fullTextEnabled, 40 | ?string $user, 41 | ?string $password, 42 | ?string $httpLastModified 43 | ): array; 44 | 45 | /** 46 | * Can a fetcher handle a feed. 47 | * 48 | * @param string $url the url that should be fetched 49 | * 50 | * @return boolean if the fetcher can handle the url. This fetcher will be 51 | * used exclusively to fetch the feed and the items of the page 52 | */ 53 | public function canHandle(string $url): bool; 54 | } 55 | -------------------------------------------------------------------------------- /lib/Command/Config/FolderAdd.php: -------------------------------------------------------------------------------- 1 | folderService = $folderService; 24 | } 25 | 26 | /** 27 | * Configure command 28 | * 29 | * @return void 30 | */ 31 | protected function configure() 32 | { 33 | $this->setName('news:folder:add') 34 | ->setDescription('Add a folder') 35 | ->addArgument('user-id', InputArgument::REQUIRED, 'User to add the folder for') 36 | ->addArgument('name', InputArgument::REQUIRED, 'Folder name', null) 37 | ->addOption('parent', null, InputOption::VALUE_OPTIONAL, 'Parent folder'); 38 | } 39 | 40 | /** 41 | * Execute command 42 | * 43 | * @param InputInterface $input 44 | * @param OutputInterface $output 45 | * 46 | * @return int 47 | */ 48 | protected function execute(InputInterface $input, OutputInterface $output): int 49 | { 50 | $user = $input->getArgument('user-id'); 51 | $name = $input->getArgument('name'); 52 | $parent = $input->getOption('parent'); 53 | 54 | if ($parent !== null) { 55 | $parent = intval($parent); 56 | } 57 | 58 | $folder = $this->folderService->create($user, $name, $parent); 59 | 60 | $output->writeln('new folder: ' . $folder->getId()); 61 | 62 | return 0; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /lib/Command/Config/FeedList.php: -------------------------------------------------------------------------------- 1 | feedService = $feedService; 27 | } 28 | 29 | /** 30 | * Configure command 31 | * 32 | * @return void 33 | */ 34 | protected function configure() 35 | { 36 | $this->setName('news:feed:list') 37 | ->setDescription('List all feeds') 38 | ->addArgument('user-id', InputArgument::REQUIRED, 'User to list the feeds for') 39 | ->addOption('recursive', null, InputOption::VALUE_NONE, 'Fetch the feed recursively'); 40 | } 41 | 42 | /** 43 | * Execute command 44 | * 45 | * @param InputInterface $input 46 | * @param OutputInterface $output 47 | * 48 | * @return int|void 49 | */ 50 | protected function execute(InputInterface $input, OutputInterface $output) 51 | { 52 | $user = $input->getArgument('user-id'); 53 | $recursive = $input->getOption('recursive'); 54 | 55 | if ($recursive !== false) { 56 | $feeds = $this->feedService->findAllForUserRecursive($user); 57 | } else { 58 | $feeds = $this->feedService->findAllForUser($user); 59 | } 60 | 61 | $output->writeln(json_encode($this->serialize($feeds), JSON_PRETTY_PRINT)); 62 | 63 | return 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /lib/Controller/ImportController.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | namespace OCA\News\Controller; 15 | 16 | use OCA\News\Service\OpmlService; 17 | use OCA\News\Service\Exceptions\ServiceValidationException; 18 | use \OCP\IRequest; 19 | use OCP\IUserSession; 20 | use OCP\AppFramework\Http\Attribute\NoAdminRequired; 21 | use OCP\AppFramework\Http\Attribute\NoCSRFRequired; 22 | 23 | /** 24 | * Class ExportController 25 | * 26 | * @package OCA\News\Controller 27 | */ 28 | class ImportController extends Controller 29 | { 30 | 31 | public function __construct( 32 | IRequest $request, 33 | ?IUserSession $userSession, 34 | private OpmlService $opmlService 35 | ) { 36 | parent::__construct($request, $userSession); 37 | } 38 | 39 | 40 | #[NoCSRFRequired] 41 | #[NoAdminRequired] 42 | public function opml(): array 43 | { 44 | $data = ''; 45 | if (isset($this->request->files['file'])) { 46 | $file = $this->request->files['file']; 47 | $data = file_get_contents($file['tmp_name']); 48 | } else { 49 | $data = $this->request->getContent(); 50 | } 51 | 52 | 53 | $status = ''; 54 | $message = ''; 55 | try { 56 | $this->opmlService->import($this->getUserId(), $data); 57 | $status = "ok"; 58 | } catch (ServiceValidationException $e) { 59 | $status = "error"; 60 | $message = $e->getMessage(); 61 | } 62 | 63 | $response = [ 64 | 'status' => $status, 65 | 'message' => $message, 66 | ]; 67 | 68 | return $response; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/api-php-static-code-check.yml: -------------------------------------------------------------------------------- 1 | name: Static analysis 2 | on: 3 | pull_request 4 | jobs: 5 | static-phpstan-analysis: 6 | runs-on: ubuntu-latest 7 | continue-on-error: true 8 | strategy: 9 | matrix: 10 | php-versions: ['8.2', '8.3' ] 11 | nextcloud: [ 'stable31' ] 12 | database: [ 'sqlite' ] 13 | include: 14 | - php-versions: 8.3 15 | nextcloud: pre-release 16 | database: sqlite 17 | experimental: true 18 | - php-versions: 8.4 19 | nextcloud: pre-release 20 | database: sqlite 21 | experimental: true 22 | name: "phpstan: Nextcloud ${{ matrix.nextcloud }} with ${{ matrix.php-versions }}" 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v6.0.1 26 | - name: Set up php 27 | uses: shivammathur/setup-php@master 28 | with: 29 | php-version: ${{ matrix.php-versions }} 30 | extensions: pdo_sqlite,pdo_mysql,pdo_pgsql,gd,zip 31 | coverage: none 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | 35 | - name: Set up server non MySQL 36 | uses: SMillerDev/nextcloud-actions/setup-nextcloud@main 37 | with: 38 | cron: true 39 | version: ${{ matrix.nextcloud }} 40 | database-type: ${{ matrix.database }} 41 | 42 | - name: Build app 43 | run: make composer 44 | 45 | - name: Configure server with app 46 | uses: SMillerDev/nextcloud-actions/setup-nextcloud-app@main 47 | with: 48 | app: 'news' 49 | check-code: false 50 | force: true 51 | 52 | - name: Prep code scanning 53 | working-directory: ../server/apps/news 54 | run: make php-test-dependencies 55 | 56 | - name: PHPCS 57 | working-directory: ../server/apps/news 58 | run: make phpcs 59 | 60 | - name: PHPStan 61 | working-directory: ../server/apps/news 62 | run: make phpstan 63 | -------------------------------------------------------------------------------- /l10n/ia.json: -------------------------------------------------------------------------------- 1 | { "translations": { 2 | "Unread articles" : "Articulos non legite", 3 | "Download" : "Discargar", 4 | "Close" : "Clauder", 5 | "Web address" : "Adresse Web", 6 | "No folder" : "Nulle dossier", 7 | "Folder name" : "Nomine de dossier", 8 | "Username" : "Nomine de usator", 9 | "Password" : "Contrasigno", 10 | "Feed exists already!" : "Syndication ja existe!", 11 | "New folder" : "Nove dossier", 12 | "Folder exists already!" : "Dossier ja existe!", 13 | "Credentials" : "Datos de authentication", 14 | "Subscribe" : "Subscribe", 15 | "Show all articles" : "Monstrar tote articulos", 16 | "Move" : "Mover", 17 | "Share" : "Compartir", 18 | "All articles" : "Tote articulos", 19 | "Starred" : "Favoritate", 20 | "Explore" : "Explorar", 21 | "Settings" : "Configurationes", 22 | "Documentation" : "Documentation", 23 | "Keyboard shortcuts" : "Combinationes de claves", 24 | "Disable mark read through scrolling" : "Disactivar le marcation como legite per rolamento", 25 | "Reverse ordering (oldest on top)" : "Ordination reverse (le plus ancian in le topo)", 26 | "Rename" : "Renominar", 27 | "Delete" : "Deler", 28 | "Toggle star article" : "Alternar stato de favorito del articulo", 29 | "by" : "per", 30 | "Play audio" : "Reproducer audio", 31 | "Download audio" : "Discargar audio", 32 | "Download video" : "Discargar video", 33 | "Open website" : "Aperir sito web", 34 | "Keep article unread" : "Mantener articulo como non legite", 35 | "Folder" : "Dossier", 36 | "Keyboard shortcut" : "Combination de claves", 37 | "Jump to next article" : "Saltar al articulo sequente", 38 | "Jump to previous article" : "Saltar al articulo previe", 39 | "right" : "dextere", 40 | "left" : "sinistre", 41 | "Star article and jump to next one" : "Favoritar articulo e saltar al sequente", 42 | "Open article in new tab" : "Aperir articulo in nove scheda", 43 | "Refresh" : "Refrescar" 44 | },"pluralForm" :"nplurals=2; plural=(n != 1);" 45 | } -------------------------------------------------------------------------------- /lib/Command/Config/FolderList.php: -------------------------------------------------------------------------------- 1 | folderService = $folderService; 27 | } 28 | 29 | /** 30 | * Configure command 31 | * 32 | * @return void 33 | */ 34 | protected function configure() 35 | { 36 | $this->setName('news:folder:list') 37 | ->setDescription('List all folders') 38 | ->addArgument('user-id', InputArgument::REQUIRED, 'User to list the folders for') 39 | ->addOption('recursive', null, InputOption::VALUE_NONE, 'Fetch the folder recursively'); 40 | } 41 | 42 | /** 43 | * Execute command 44 | * 45 | * @param InputInterface $input 46 | * @param OutputInterface $output 47 | * 48 | * @return int|void 49 | */ 50 | protected function execute(InputInterface $input, OutputInterface $output) 51 | { 52 | $user = $input->getArgument('user-id'); 53 | $recursive = $input->getOption('recursive'); 54 | 55 | if ($recursive !== false) { 56 | $folders = $this->folderService->findAllForUserRecursive($user); 57 | } else { 58 | $folders = $this->folderService->findAllForUser($user); 59 | } 60 | 61 | $output->writeln(json_encode($this->serialize($folders), JSON_PRETTY_PRINT)); 62 | 63 | return 0; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /l10n/ia.js: -------------------------------------------------------------------------------- 1 | OC.L10N.register( 2 | "news", 3 | { 4 | "Unread articles" : "Articulos non legite", 5 | "Download" : "Discargar", 6 | "Close" : "Clauder", 7 | "Web address" : "Adresse Web", 8 | "No folder" : "Nulle dossier", 9 | "Folder name" : "Nomine de dossier", 10 | "Username" : "Nomine de usator", 11 | "Password" : "Contrasigno", 12 | "Feed exists already!" : "Syndication ja existe!", 13 | "New folder" : "Nove dossier", 14 | "Folder exists already!" : "Dossier ja existe!", 15 | "Credentials" : "Datos de authentication", 16 | "Subscribe" : "Subscribe", 17 | "Show all articles" : "Monstrar tote articulos", 18 | "Move" : "Mover", 19 | "Share" : "Compartir", 20 | "All articles" : "Tote articulos", 21 | "Starred" : "Favoritate", 22 | "Explore" : "Explorar", 23 | "Settings" : "Configurationes", 24 | "Documentation" : "Documentation", 25 | "Keyboard shortcuts" : "Combinationes de claves", 26 | "Disable mark read through scrolling" : "Disactivar le marcation como legite per rolamento", 27 | "Reverse ordering (oldest on top)" : "Ordination reverse (le plus ancian in le topo)", 28 | "Rename" : "Renominar", 29 | "Delete" : "Deler", 30 | "Toggle star article" : "Alternar stato de favorito del articulo", 31 | "by" : "per", 32 | "Play audio" : "Reproducer audio", 33 | "Download audio" : "Discargar audio", 34 | "Download video" : "Discargar video", 35 | "Open website" : "Aperir sito web", 36 | "Keep article unread" : "Mantener articulo como non legite", 37 | "Folder" : "Dossier", 38 | "Keyboard shortcut" : "Combination de claves", 39 | "Jump to next article" : "Saltar al articulo sequente", 40 | "Jump to previous article" : "Saltar al articulo previe", 41 | "right" : "dextere", 42 | "left" : "sinistre", 43 | "Star article and jump to next one" : "Favoritar articulo e saltar al sequente", 44 | "Open article in new tab" : "Aperir articulo in nove scheda", 45 | "Refresh" : "Refrescar" 46 | }, 47 | "nplurals=2; plural=(n != 1);"); 48 | -------------------------------------------------------------------------------- /img/favicon-touch.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /lib/Service/UpdaterService.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Bernhard Posselt 10 | * @copyright 2012 Alessandro Cosentino 11 | * @copyright 2012-2014 Bernhard Posselt 12 | */ 13 | 14 | 15 | namespace OCA\News\Service; 16 | 17 | use OCP\BackgroundJob\IJobList; 18 | use OCA\News\Cron\UpdaterJob; 19 | 20 | class UpdaterService 21 | { 22 | 23 | /** 24 | * @var FolderServiceV2 25 | */ 26 | private $folderService; 27 | 28 | /** 29 | * @var FeedServiceV2 30 | */ 31 | private $feedService; 32 | 33 | /** 34 | * @var ItemServiceV2 35 | */ 36 | private $itemService; 37 | 38 | /** @var IJobList */ 39 | private $jobList; 40 | 41 | public function __construct( 42 | FolderServiceV2 $folderService, 43 | FeedServiceV2 $feedService, 44 | ItemServiceV2 $itemService, 45 | IJobList $jobList 46 | ) { 47 | $this->folderService = $folderService; 48 | $this->feedService = $feedService; 49 | $this->itemService = $itemService; 50 | $this->jobList = $jobList; 51 | } 52 | 53 | 54 | public function beforeUpdate(): void 55 | { 56 | $this->folderService->purgeDeleted(null, null); 57 | $this->feedService->purgeDeleted(null, null); 58 | $this->feedService->purgeFeedLogos(); 59 | } 60 | 61 | 62 | public function update(): void 63 | { 64 | $this->feedService->fetchAll(); 65 | } 66 | 67 | 68 | public function afterUpdate(): void 69 | { 70 | $this->itemService->purgeOverThreshold(); 71 | } 72 | 73 | public function reset(): int 74 | { 75 | $myJobList = $this->jobList->getJobsIterator(UpdaterJob::class, 1, 0); 76 | $job = $myJobList->current(); 77 | 78 | $this->jobList->resetBackgroundJob($job); 79 | 80 | return 0; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /img/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /docs/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /scoper.inc.php: -------------------------------------------------------------------------------- 1 | path($name) 51 | ->files() 52 | ->exclude(["test", "tests", "composer", "bin"]) 53 | ->notName("autoload.php") 54 | ->in("vendor/"); 55 | } 56 | 57 | return [ 58 | "prefix" => "OCA\\News\\Vendor", 59 | 'exclude-namespaces' => [ 'Psr\\Log\\' ], 60 | "finders" => $finderConfig, 61 | ]; 62 | -------------------------------------------------------------------------------- /img/app-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 20 | 22 | image/svg+xml 23 | 25 | 26 | 27 | 28 | 30 | 50 | 55 | 56 | --------------------------------------------------------------------------------