├── .github ├── release-drafter.yml └── workflows │ ├── client-build.yml │ ├── docker-image-build.yml │ ├── extension-build.yml │ ├── release.yml │ ├── server-build.yml │ └── tauri-build.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.en.md ├── README.md ├── app ├── client │ ├── .gitignore │ ├── README.md │ ├── nginx.conf │ ├── openapitools.json │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── index.html │ │ ├── robots.txt │ │ └── site.webmanifest │ ├── src │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── _setupProxy.js │ │ ├── api │ │ │ ├── .gitignore │ │ │ ├── .npmignore │ │ │ ├── .openapi-generator-ignore │ │ │ ├── .openapi-generator │ │ │ │ ├── FILES │ │ │ │ └── VERSION │ │ │ ├── api.ts │ │ │ ├── base.ts │ │ │ ├── common.ts │ │ │ ├── config.yaml │ │ │ ├── configuration.ts │ │ │ ├── generator-template │ │ │ │ └── apiInner.mustache │ │ │ ├── git_push.sh │ │ │ ├── index.ts │ │ │ ├── openapi-generator-mac.sh │ │ │ ├── openapi-generator.ps1 │ │ │ ├── openapi-generator.sh │ │ │ └── openapitools.json │ │ ├── common │ │ │ ├── arrayUtils.ts │ │ │ ├── docUtils.ts │ │ │ ├── objectUtils.ts │ │ │ └── typeUtils.ts │ │ ├── components │ │ │ ├── Header.tsx │ │ │ ├── Layout.tsx │ │ │ ├── Loading.tsx │ │ │ ├── MagazineItem.tsx │ │ │ ├── MainContainer.tsx │ │ │ ├── PageDetail.module.css │ │ │ ├── PageDetailArea.tsx │ │ │ ├── PageDetailModal.tsx │ │ │ ├── PageFilters.tsx │ │ │ ├── PageList.tsx │ │ │ ├── PageOperationButtons.tsx │ │ │ ├── SearchBox.tsx │ │ │ ├── SettingModal │ │ │ │ ├── AccountSetting.tsx │ │ │ │ ├── ConnectorSetting.tsx │ │ │ │ ├── FeedsFormDialog.tsx │ │ │ │ ├── FeedsSetting.tsx │ │ │ │ ├── FolderFormDialog.tsx │ │ │ │ ├── FoldersSetting.tsx │ │ │ │ ├── GeneralSetting.tsx │ │ │ │ └── index.tsx │ │ │ ├── Sidebar │ │ │ │ ├── LibraryNavTree.tsx │ │ │ │ ├── NavLabels.ts │ │ │ │ ├── NavTreeView.tsx │ │ │ │ ├── Sidebar.module.css │ │ │ │ └── Sidebar.tsx │ │ │ ├── SmartMoment.tsx │ │ │ ├── SubHeader.tsx │ │ │ ├── Tweet │ │ │ │ ├── DownloadButton.tsx │ │ │ │ ├── Tweet.module.css │ │ │ │ ├── Tweet.tsx │ │ │ │ └── TweetRoot.tsx │ │ │ └── common │ │ │ │ ├── ConditionalWrapper.tsx │ │ │ │ └── TransitionAlert.tsx │ │ ├── domain │ │ │ ├── electronTypes.ts │ │ │ ├── index.ts │ │ │ ├── pageQueryKey.ts │ │ │ └── utils.ts │ │ ├── env.ts │ │ ├── index.tsx │ │ ├── interfaces │ │ │ ├── connectorType.ts │ │ │ ├── githubRepoProperties.ts │ │ │ ├── librarySaveStatus.ts │ │ │ └── tweetProperties.ts │ │ ├── model.d.ts │ │ ├── pages │ │ │ ├── AllFeeds.tsx │ │ │ ├── Archive.tsx │ │ │ ├── ConnectorList.tsx │ │ │ ├── FolderList.tsx │ │ │ ├── Index.tsx │ │ │ ├── MyList.tsx │ │ │ ├── Page.tsx │ │ │ ├── ReadLater.tsx │ │ │ ├── Search.tsx │ │ │ ├── SignIn.tsx │ │ │ ├── Starred.tsx │ │ │ └── Twitter.tsx │ │ ├── react-app-env.d.ts │ │ ├── reportWebVitals.ts │ │ ├── setupTests.ts │ │ └── styles │ │ │ ├── github-markdown-dark.css │ │ │ ├── github-markdown-light.css │ │ │ ├── github-markdown.css │ │ │ └── globals.css │ ├── tailwind.config.js │ ├── tsconfig.json │ └── yarn.lock ├── electron │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ └── renderer │ │ │ └── index.html │ ├── src │ │ ├── main │ │ │ ├── eventHandler.ts │ │ │ ├── index.ts │ │ │ ├── preload.ts │ │ │ ├── settingStore.ts │ │ │ ├── tsconfig.json │ │ │ ├── types.ts │ │ │ ├── utilsBridge.ts │ │ │ └── window.ts │ │ └── renderer │ │ │ ├── App.tsx │ │ │ └── app.css │ ├── tsconfig.json │ ├── webpack │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ └── webpack.prod.js │ └── yarn.lock ├── extension │ ├── .github │ │ └── workflows │ │ │ └── build._yml │ ├── .gitignore │ ├── .vscode │ │ ├── settings.json │ │ └── tasks.json │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ │ ├── favicon-128x128.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── icon.png │ │ ├── manifest-firefox.json │ │ ├── manifest.json │ │ ├── options.html │ │ └── popup.html │ ├── src │ │ ├── __tests__ │ │ │ └── sum.ts │ │ ├── article.module.css │ │ ├── article.tsx │ │ ├── background.ts │ │ ├── content_script.tsx │ │ ├── env.ts │ │ ├── globals.d.ts │ │ ├── logger.ts │ │ ├── model.d.ts │ │ ├── model │ │ │ ├── librarySaveStatus.ts │ │ │ └── pageOperateResult.ts │ │ ├── options.css │ │ ├── options.tsx │ │ ├── popup.css │ │ ├── popup.tsx │ │ ├── request_interceptor.ts │ │ ├── services.ts │ │ ├── settings.tsx │ │ ├── storage.ts │ │ ├── sum.ts │ │ ├── tweet_interceptor.ts │ │ ├── utils.ts │ │ └── web_clipper.tsx │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── webpack │ │ ├── webpack.common.js │ │ ├── webpack.dev.js │ │ └── webpack.prod.js │ └── yarn.lock ├── server │ ├── .gitignore │ ├── .mvn │ │ └── maven.config │ ├── huntly-common │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── huntly │ │ │ └── common │ │ │ ├── api │ │ │ ├── ApiCode.java │ │ │ ├── ApiResult.java │ │ │ └── model │ │ │ │ ├── ErrorDetail.java │ │ │ │ ├── ErrorMessageType.java │ │ │ │ └── ErrorResponse.java │ │ │ ├── entity │ │ │ └── BaseEntity.java │ │ │ ├── enums │ │ │ ├── BaseEnum.java │ │ │ ├── BaseEnumUtil.java │ │ │ └── EnumVo.java │ │ │ ├── exceptions │ │ │ ├── BaseException.java │ │ │ ├── BusinessException.java │ │ │ ├── DaoException.java │ │ │ ├── DuplicateRecordException.java │ │ │ ├── NoPermissionException.java │ │ │ ├── NoSuchDataException.java │ │ │ ├── RequestVerifyException.java │ │ │ ├── StatefulException.java │ │ │ └── UnAuthorizedException.java │ │ │ ├── pagination │ │ │ ├── PageParam.java │ │ │ └── PagingResult.java │ │ │ ├── util │ │ │ ├── Base64Utils.java │ │ │ ├── MapUtils.java │ │ │ ├── MoreObjectUtils.java │ │ │ ├── NumberUtils.java │ │ │ ├── TextUtils.java │ │ │ ├── UrlUtils.java │ │ │ └── XmlUtils.java │ │ │ └── web │ │ │ └── BaseController.java │ ├── huntly-interfaces │ │ ├── pom.xml │ │ └── src │ │ │ └── main │ │ │ └── java │ │ │ └── com │ │ │ └── huntly │ │ │ └── interfaces │ │ │ └── external │ │ │ ├── dto │ │ │ ├── ConnectorItem.java │ │ │ ├── CursorPageResult.java │ │ │ ├── FolderConnectorView.java │ │ │ ├── FolderConnectors.java │ │ │ ├── LoginUserInfo.java │ │ │ ├── PageItem.java │ │ │ ├── PageOperateResult.java │ │ │ ├── PageSearchResult.java │ │ │ └── PreviewFeedsInfo.java │ │ │ ├── model │ │ │ ├── ArticleContent.java │ │ │ ├── CaptureFromType.java │ │ │ ├── CapturePage.java │ │ │ ├── ContentType.java │ │ │ ├── FeedsSetting.java │ │ │ ├── GitHubSetting.java │ │ │ ├── GithubRepoProperties.java │ │ │ ├── InterceptTweets.java │ │ │ ├── LibrarySaveStatus.java │ │ │ ├── LibrarySaveType.java │ │ │ ├── LoginRequest.java │ │ │ ├── SearchOption.java │ │ │ ├── TweetId.java │ │ │ └── TweetProperties.java │ │ │ └── query │ │ │ ├── PageListQuery.java │ │ │ ├── PageListSort.java │ │ │ ├── PageQuery.java │ │ │ └── SearchQuery.java │ ├── huntly-jpa │ │ ├── pom.xml │ │ └── src │ │ │ ├── main │ │ │ └── java │ │ │ │ └── com │ │ │ │ └── huntly │ │ │ │ └── jpa │ │ │ │ ├── query │ │ │ │ ├── OrderByInfo.java │ │ │ │ ├── QueryCriteria.java │ │ │ │ ├── SpecificationUtils.java │ │ │ │ └── annotation │ │ │ │ │ ├── Keywords.java │ │ │ │ │ ├── Operator.java │ │ │ │ │ ├── OrderBy.java │ │ │ │ │ ├── Queries.java │ │ │ │ │ ├── Query.java │ │ │ │ │ ├── QueryGroup.java │ │ │ │ │ ├── QueryGroups.java │ │ │ │ │ ├── QueryRoot.java │ │ │ │ │ └── QueryType.java │ │ │ │ ├── repository │ │ │ │ ├── JpaRepositoryWithLimit.java │ │ │ │ ├── JpaSpecificationExecutorWithProjection.java │ │ │ │ └── support │ │ │ │ │ └── CustomJpaRepository.java │ │ │ │ └── spec │ │ │ │ ├── PredicateBuilder.java │ │ │ │ ├── Sorts.java │ │ │ │ ├── Specifications.java │ │ │ │ └── specification │ │ │ │ ├── AbstractSpecification.java │ │ │ │ ├── BetweenSpecification.java │ │ │ │ ├── EqualSpecification.java │ │ │ │ ├── GeSpecification.java │ │ │ │ ├── GtSpecification.java │ │ │ │ ├── InSpecification.java │ │ │ │ ├── LeSpecification.java │ │ │ │ ├── LikeSpecification.java │ │ │ │ ├── LtSpecification.java │ │ │ │ ├── NotEqualSpecification.java │ │ │ │ ├── NotInSpecification.java │ │ │ │ └── NotLikeSpecification.java │ │ │ └── test │ │ │ ├── java │ │ │ └── com │ │ │ │ └── huntly │ │ │ │ └── jpa │ │ │ │ ├── Application.java │ │ │ │ ├── builder │ │ │ │ └── PersonBuilder.java │ │ │ │ ├── integration │ │ │ │ ├── query │ │ │ │ │ ├── QueryGreatEqualTest.java │ │ │ │ │ └── model │ │ │ │ │ │ └── PersonQuery.java │ │ │ │ ├── repository │ │ │ │ │ └── RepositoryEqualTest.java │ │ │ │ └── spec │ │ │ │ │ ├── AndOrTest.java │ │ │ │ │ ├── BetweenTest.java │ │ │ │ │ ├── EqualTest.java │ │ │ │ │ ├── GreatEqualTest.java │ │ │ │ │ ├── GreatThanTest.java │ │ │ │ │ ├── InTest.java │ │ │ │ │ ├── JoinTest.java │ │ │ │ │ ├── LessEqualTest.java │ │ │ │ │ ├── LessThanTest.java │ │ │ │ │ ├── LikeTest.java │ │ │ │ │ ├── NotEqualTest.java │ │ │ │ │ ├── NotInTest.java │ │ │ │ │ ├── NotLikeTest.java │ │ │ │ │ ├── OrTest.java │ │ │ │ │ ├── PredicateTest.java │ │ │ │ │ ├── SortsTest.java │ │ │ │ │ └── VirtualViewTest.java │ │ │ │ ├── model │ │ │ │ ├── Address.java │ │ │ │ ├── IdCard.java │ │ │ │ ├── Person.java │ │ │ │ ├── PersonIdCard.java │ │ │ │ ├── PersonInfo.java │ │ │ │ └── Phone.java │ │ │ │ └── repository │ │ │ │ ├── PersonIdCardRepository.java │ │ │ │ ├── PersonRepository.java │ │ │ │ └── PhoneRepository.java │ │ │ └── resources │ │ │ └── application.properties │ ├── huntly-server │ │ ├── pom.xml │ │ └── src │ │ │ ├── main │ │ │ ├── java │ │ │ │ └── com │ │ │ │ │ └── huntly │ │ │ │ │ └── server │ │ │ │ │ ├── HuntlyServerApplication.java │ │ │ │ │ ├── cache │ │ │ │ │ └── CacheService.java │ │ │ │ │ ├── config │ │ │ │ │ ├── DocketConfig.java │ │ │ │ │ ├── HuntlyProperties.java │ │ │ │ │ ├── ServiceExecutorConfig.java │ │ │ │ │ └── WebConfig.java │ │ │ │ │ ├── connector │ │ │ │ │ ├── ConnectorProperties.java │ │ │ │ │ ├── ConnectorType.java │ │ │ │ │ ├── InfoConnector.java │ │ │ │ │ ├── InfoConnectorFactory.java │ │ │ │ │ ├── github │ │ │ │ │ │ └── GithubConnector.java │ │ │ │ │ ├── rss │ │ │ │ │ │ ├── FeedUtils.java │ │ │ │ │ │ ├── OMPLConverter.java │ │ │ │ │ │ └── RSSConnector.java │ │ │ │ │ └── twitter │ │ │ │ │ │ └── TweetParser.java │ │ │ │ │ ├── controller │ │ │ │ │ ├── AuthController.java │ │ │ │ │ ├── ConnectorController.java │ │ │ │ │ ├── FolderController.java │ │ │ │ │ ├── HealthController.java │ │ │ │ │ ├── PageController.java │ │ │ │ │ ├── ReactAppController.java │ │ │ │ │ ├── SearchController.java │ │ │ │ │ ├── SettingController.java │ │ │ │ │ └── TweetController.java │ │ │ │ │ ├── data │ │ │ │ │ └── dialect │ │ │ │ │ │ ├── SQLiteDialect.java │ │ │ │ │ │ ├── SQLiteMetadataBuilderInitializer.java │ │ │ │ │ │ └── identity │ │ │ │ │ │ └── SQLiteDialectIdentityColumnSupport.java │ │ │ │ │ ├── domain │ │ │ │ │ ├── constant │ │ │ │ │ │ ├── AppConstants.java │ │ │ │ │ │ └── DocFields.java │ │ │ │ │ ├── entity │ │ │ │ │ │ ├── Connector.java │ │ │ │ │ │ ├── ConnectorSetting.java │ │ │ │ │ │ ├── Folder.java │ │ │ │ │ │ ├── GlobalSetting.java │ │ │ │ │ │ ├── Page.java │ │ │ │ │ │ ├── PageArticleContent.java │ │ │ │ │ │ ├── PageProperties.java │ │ │ │ │ │ ├── PageRelation.java │ │ │ │ │ │ ├── SearchHistory.java │ │ │ │ │ │ ├── Source.java │ │ │ │ │ │ ├── TweetTrack.java │ │ │ │ │ │ ├── TwitterUserSetting.java │ │ │ │ │ │ └── User.java │ │ │ │ │ ├── enums │ │ │ │ │ │ └── ArticleContentCategory.java │ │ │ │ │ ├── exceptions │ │ │ │ │ │ └── ConnectorFetchException.java │ │ │ │ │ ├── mapper │ │ │ │ │ │ ├── ConnectorItemMapper.java │ │ │ │ │ │ └── PageItemMapper.java │ │ │ │ │ ├── model │ │ │ │ │ │ ├── ProxySetting.java │ │ │ │ │ │ └── tweet │ │ │ │ │ │ │ └── TweetsRoot.java │ │ │ │ │ └── vo │ │ │ │ │ │ └── PageDetail.java │ │ │ │ │ ├── event │ │ │ │ │ ├── EventPublisher.java │ │ │ │ │ ├── InboxChangedEvent.java │ │ │ │ │ ├── InboxChangedListener.java │ │ │ │ │ ├── TweetPageCaptureEvent.java │ │ │ │ │ └── TweetPageCaptureListener.java │ │ │ │ │ ├── handler │ │ │ │ │ └── CustomExceptionHandler.java │ │ │ │ │ ├── repository │ │ │ │ │ ├── BaseRepository.java │ │ │ │ │ ├── ConnectorRepository.java │ │ │ │ │ ├── ConnectorSettingRepository.java │ │ │ │ │ ├── FolderRepository.java │ │ │ │ │ ├── GlobalSettingRepository.java │ │ │ │ │ ├── PageArticleContentRepository.java │ │ │ │ │ ├── PageRepository.java │ │ │ │ │ ├── SearchHistoryRepository.java │ │ │ │ │ ├── SourceRepository.java │ │ │ │ │ ├── TweetTrackRepository.java │ │ │ │ │ ├── TwitterUserSettingRepository.java │ │ │ │ │ ├── UserRepository.java │ │ │ │ │ └── custom │ │ │ │ │ │ ├── PageItemRepository.java │ │ │ │ │ │ └── PageItemRepositoryImpl.java │ │ │ │ │ ├── security │ │ │ │ │ ├── WebSecurityConfig.java │ │ │ │ │ ├── jwt │ │ │ │ │ │ ├── AuthTokenFilter.java │ │ │ │ │ │ ├── JwtUtils.java │ │ │ │ │ │ └── UnAuthEntryPointJwt.java │ │ │ │ │ └── services │ │ │ │ │ │ ├── UserDetailsImpl.java │ │ │ │ │ │ └── UserDetailsServiceImpl.java │ │ │ │ │ ├── service │ │ │ │ │ ├── BasePageService.java │ │ │ │ │ ├── CapturePageService.java │ │ │ │ │ ├── ConnectorFetchService.java │ │ │ │ │ ├── ConnectorService.java │ │ │ │ │ ├── ConnectorSettingService.java │ │ │ │ │ ├── FeedsService.java │ │ │ │ │ ├── FolderService.java │ │ │ │ │ ├── GlobalSettingService.java │ │ │ │ │ ├── LuceneService.java │ │ │ │ │ ├── OPMLService.java │ │ │ │ │ ├── PageArticleContentService.java │ │ │ │ │ ├── PageListService.java │ │ │ │ │ ├── PageService.java │ │ │ │ │ ├── SearchHistoryService.java │ │ │ │ │ ├── SourceService.java │ │ │ │ │ ├── TweetTrackService.java │ │ │ │ │ ├── TwitterUserSettingService.java │ │ │ │ │ └── UserService.java │ │ │ │ │ ├── task │ │ │ │ │ ├── ColdDataClearTask.java │ │ │ │ │ ├── ConnectorScheduledTask.java │ │ │ │ │ └── TweetTrackTask.java │ │ │ │ │ └── util │ │ │ │ │ ├── HtmlText.java │ │ │ │ │ ├── HtmlUtils.java │ │ │ │ │ ├── HttpUtils.java │ │ │ │ │ ├── JSONUtils.java │ │ │ │ │ ├── PageSizeUtils.java │ │ │ │ │ └── SiteUtils.java │ │ │ └── resources │ │ │ │ ├── META-INF │ │ │ │ └── services │ │ │ │ │ └── org.hibernate.boot.spi.MetadataBuilderInitializer │ │ │ │ ├── application.yml │ │ │ │ └── logback-spring.xml │ │ │ └── test │ │ │ ├── java │ │ │ └── com │ │ │ │ └── huntly │ │ │ │ └── server │ │ │ │ ├── connector │ │ │ │ ├── GithubConnectorTest.java │ │ │ │ └── twitter │ │ │ │ │ └── TweetParserTest.java │ │ │ │ └── page │ │ │ │ ├── ContentCleaner.java │ │ │ │ └── ContentCleanerTest.java │ │ │ └── resources │ │ │ └── tweet_timeline.json │ └── pom.xml └── tauri │ ├── .gitignore │ ├── .vscode │ └── extensions.json │ ├── README.md │ ├── index.html │ ├── package.json │ ├── postcss.config.js │ ├── src-tauri │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── build.rs │ ├── huntly-manifest.rc │ ├── huntly.exe.manifest │ ├── icons │ │ ├── favicon-128x128.png │ │ ├── favicon-128x128@2x.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── icon.icns │ │ └── icon.ico │ ├── src │ │ └── main.rs │ └── tauri.conf.json │ ├── src │ ├── App.css │ ├── App.tsx │ ├── main.tsx │ └── vite-env.d.ts │ ├── tailwind.config.js │ ├── tsconfig.json │ ├── tsconfig.node.json │ ├── vite.config.ts │ └── yarn.lock └── static ├── architect.puml └── images ├── intro1.png ├── intro2.png ├── jb_beam.png ├── wechat.JPG └── zfb.JPG /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | # Configuration for Release Drafter: https://github.com/toolmantim/release-drafter 2 | name-template: 'v$NEXT_PATCH_VERSION 🌈' 3 | tag-template: 'v$NEXT_PATCH_VERSION' 4 | version-template: $MAJOR.$MINOR.$PATCH 5 | # Emoji reference: https://gitmoji.carloscuesta.me/ 6 | categories: 7 | - title: '🚀 Features' 8 | labels: 9 | - 'feature' 10 | - 'enhancement' 11 | - 'kind/feature' 12 | - title: '🐛 Bug Fixes' 13 | labels: 14 | - 'fix' 15 | - 'bugfix' 16 | - 'bug' 17 | - 'regression' 18 | - 'kind/bug' 19 | - title: 📝 Documentation updates 20 | labels: 21 | - 'doc' 22 | - 'documentation' 23 | - 'kind/doc' 24 | - title: 👻 Maintenance 25 | labels: 26 | - chore 27 | - dependencies 28 | - 'kind/chore' 29 | - 'kind/dep' 30 | - title: 🚦 Tests 31 | labels: 32 | - test 33 | - tests 34 | exclude-labels: 35 | - reverted 36 | - no-changelog 37 | - skip-changelog 38 | - invalid 39 | change-template: '* $TITLE (#$NUMBER) @$AUTHOR' 40 | template: | 41 | ## What’s Changed 42 | $CHANGES -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /renderer/.next/ 13 | /renderer/out/ 14 | 15 | # production 16 | /main 17 | /dist 18 | 19 | .idea/ 20 | 21 | # misc 22 | .DS_Store 23 | *.pem 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | 30 | # local env files 31 | .env.local 32 | .env.development.local 33 | .env.test.local 34 | .env.production.local 35 | 36 | # vercel 37 | .vercel 38 | 39 | db.sqlite* -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11 2 | 3 | LABEL maintainer="lcomplete" 4 | LABEL version = "0.1.0" 5 | 6 | WORKDIR /app 7 | 8 | VOLUME /data 9 | 10 | RUN mkdir -p /data /data/lucene 11 | 12 | ARG JAR_FILE=./app/server/huntly-server/target/huntly-server-*.jar 13 | ARG JAR_PATH=/app/server.jar 14 | 15 | COPY ${JAR_FILE} ${JAR_PATH} 16 | 17 | ENV JAVA_ARGS="-Xms128m -Xmx1024m" 18 | ENV VM_ARGS="-Duser.timezone=GMT+08" 19 | ENV APP_ARGS="" 20 | ENV PROFILE="default" 21 | ENV PORT=80 22 | ENV JAR_PATH=${JAR_PATH} 23 | 24 | EXPOSE ${PORT} 25 | EXPOSE 443 26 | 27 | ENTRYPOINT ["sh", "-c", "java $JAVA_ARGS $VM_ARGS -jar $JAR_PATH --spring.profiles.active=$PROFILE --server.port=$PORT --huntly.dataDir=/data/ --huntly.luceneDir=/data/lucene $APP_ARGS" ] -------------------------------------------------------------------------------- /app/client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | api-docs.json -------------------------------------------------------------------------------- /app/client/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | # gzip config 4 | gzip on; 5 | gzip_min_length 1k; 6 | gzip_comp_level 9; 7 | gzip_types text/plain text/css text/javascript application/json application/javascript application/x-javascript application/xml; 8 | gzip_vary on; 9 | gzip_disable "MSIE [1-6]\."; 10 | 11 | root /usr/share/nginx/build; 12 | # include /etc/nginx/mime.types; 13 | proxy_set_header Cookie $http_cookie; 14 | 15 | location / { 16 | client_max_body_size 50M; 17 | try_files $uri $uri/ /index.html; 18 | } 19 | 20 | location /api { 21 | client_max_body_size 50M; 22 | proxy_pass http://localhost:8080; 23 | proxy_set_header X-Forwarded-Proto $scheme; 24 | proxy_set_header Host $http_host; 25 | proxy_set_header X-Real-IP $remote_addr; 26 | } 27 | 28 | } -------------------------------------------------------------------------------- /app/client/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "6.2.1" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | 'tailwindcss/nesting': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | }; -------------------------------------------------------------------------------- /app/client/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/client/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /app/client/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/client/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /app/client/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/client/public/apple-touch-icon.png -------------------------------------------------------------------------------- /app/client/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/client/public/favicon-16x16.png -------------------------------------------------------------------------------- /app/client/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/client/public/favicon-32x32.png -------------------------------------------------------------------------------- /app/client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/client/public/favicon.ico -------------------------------------------------------------------------------- /app/client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /app/client/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /app/client/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from './App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /app/client/src/_setupProxy.js: -------------------------------------------------------------------------------- 1 | import { API_BASE } from "./env"; 2 | const {createProxyMiddleware} = require("http-proxy-middleware"); 3 | 4 | module.exports = function (app){ 5 | app.use( 6 | '/api', 7 | createProxyMiddleware({ 8 | target: `${API_BASE}`, 9 | changeOrigin: true, 10 | }) 11 | ); 12 | } -------------------------------------------------------------------------------- /app/client/src/api/.gitignore: -------------------------------------------------------------------------------- 1 | wwwroot/*.js 2 | node_modules 3 | typings 4 | dist 5 | -------------------------------------------------------------------------------- /app/client/src/api/.npmignore: -------------------------------------------------------------------------------- 1 | # empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm -------------------------------------------------------------------------------- /app/client/src/api/.openapi-generator-ignore: -------------------------------------------------------------------------------- 1 | # OpenAPI Generator Ignore 2 | # Generated by openapi-generator https://github.com/openapitools/openapi-generator 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | 25 | base.ts -------------------------------------------------------------------------------- /app/client/src/api/.openapi-generator/FILES: -------------------------------------------------------------------------------- 1 | .gitignore 2 | .npmignore 3 | api.ts 4 | common.ts 5 | configuration.ts 6 | git_push.sh 7 | index.ts 8 | -------------------------------------------------------------------------------- /app/client/src/api/.openapi-generator/VERSION: -------------------------------------------------------------------------------- 1 | 6.2.0 -------------------------------------------------------------------------------- /app/client/src/api/config.yaml: -------------------------------------------------------------------------------- 1 | templateDir: generator-template -------------------------------------------------------------------------------- /app/client/src/api/index.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | /* eslint-disable */ 3 | /** 4 | * huntly api doc 5 | * huntly api doc for code generation 6 | * 7 | * The version of the OpenAPI document: 3.0 8 | * 9 | * 10 | * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). 11 | * https://openapi-generator.tech 12 | * Do not edit the class manually. 13 | */ 14 | 15 | 16 | export * from "./api"; 17 | export * from "./configuration"; 18 | 19 | -------------------------------------------------------------------------------- /app/client/src/api/openapi-generator-mac.sh: -------------------------------------------------------------------------------- 1 | wget --output-document api-docs.json "http://localhost:8080/api/v3/api-docs" 2 | openapi-generator generate -i api-docs.json -g typescript-axios -o . -------------------------------------------------------------------------------- /app/client/src/api/openapi-generator.ps1: -------------------------------------------------------------------------------- 1 | wget "http://localhost:8080/api/v3/api-docs" -outfile "api-docs.json" 2 | openapi-generator-cli generate -i api-docs.json -g typescript-axios -o . 3 | #-c config.yaml -------------------------------------------------------------------------------- /app/client/src/api/openapi-generator.sh: -------------------------------------------------------------------------------- 1 | wget --output-document api-docs.json "http://localhost:8080/api/v3/api-docs" 2 | openapi-generator-cli generate -i api-docs.json -g typescript-axios -o . -------------------------------------------------------------------------------- /app/client/src/api/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "6.2.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /app/client/src/common/arrayUtils.ts: -------------------------------------------------------------------------------- 1 | export const reorder = ( 2 | list: T[], 3 | startIndex: number, 4 | endIndex: number 5 | ): T[] => { 6 | const result = Array.from(list); 7 | const [removed] = result.splice(startIndex, 1); 8 | result.splice(endIndex, 0, removed); 9 | 10 | return result; 11 | }; -------------------------------------------------------------------------------- /app/client/src/common/docUtils.ts: -------------------------------------------------------------------------------- 1 | export function setDocTitle(title:string){ 2 | document.title = title + " / Huntly"; 3 | } -------------------------------------------------------------------------------- /app/client/src/common/objectUtils.ts: -------------------------------------------------------------------------------- 1 | export const isDeepEqual = (object1, object2) => { 2 | 3 | const objKeys1 = Object.keys(object1); 4 | const objKeys2 = Object.keys(object2); 5 | 6 | if (objKeys1.length !== objKeys2.length) return false; 7 | 8 | for (let key of objKeys1) { 9 | const value1 = object1[key]; 10 | const value2 = object2[key]; 11 | 12 | const isObjects = isObject(value1) && isObject(value2); 13 | 14 | if ((isObjects && !isDeepEqual(value1, value2)) || 15 | (!isObjects && value1 !== value2) 16 | ) { 17 | return false; 18 | } 19 | } 20 | return true; 21 | }; 22 | 23 | const isObject = (object) => { 24 | return object != null && typeof object === "object"; 25 | }; -------------------------------------------------------------------------------- /app/client/src/common/typeUtils.ts: -------------------------------------------------------------------------------- 1 | export function safeInt(text: unknown, defaultValue = 0) { 2 | const str = text as string; 3 | const value = parseInt(str); 4 | return isNaN(value) ? defaultValue : value; 5 | } 6 | 7 | export function mapArrayTo(sources: Source[], setter: (source: Source, target: Target) => void): Target[] { 8 | const targets: Target[] = []; 9 | sources.forEach(source => { 10 | const target = mapTo(source, setter); 11 | targets.push(target); 12 | }) 13 | return targets; 14 | } 15 | 16 | export function mapTo(source: Source, setter: (source: Source, target: Target) => void): Target { 17 | const target = {} as Target; 18 | Object.assign(target, source); 19 | setter(source,target); 20 | return target; 21 | } -------------------------------------------------------------------------------- /app/client/src/components/Layout.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {CssBaseline, StyledEngineProvider} from "@mui/material"; 3 | import Sidebar from "./Sidebar/Sidebar"; 4 | import {Outlet, ScrollRestoration, useLocation} from "react-router-dom"; 5 | import Header from "./Header"; 6 | 7 | const Layout = () => { 8 | const location = useLocation(); 9 | useEffect(() => { 10 | const rootEl = document.getElementById('root'); 11 | rootEl?.classList.remove('toggle-sidebar'); 12 | },[location]); 13 | 14 | return ( 15 | 16 | 17 | { 19 | const paths = ["/"]; 20 | // const paths = ["/", "/list","/starred","/later","/archive"]; 21 | return paths.includes(location.pathname) 22 | ? // home and some paths restore by pathname 23 | location.pathname 24 | : // everything else by location like the browser 25 | location.key; 26 | }} 27 | /> 28 |
29 |
30 |
31 |
32 | 33 |
34 |
35 | 36 |
37 |
38 |
39 |
40 | ); 41 | }; 42 | 43 | export default Layout; 44 | -------------------------------------------------------------------------------- /app/client/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | import {CircularProgress} from "@mui/material"; 2 | import React from "react"; 3 | 4 | const Loading = () => { 5 | return
; 6 | } 7 | 8 | export default Loading; -------------------------------------------------------------------------------- /app/client/src/components/MainContainer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | const MainContainer = (props) =>{ 4 | return ( 5 |
6 | {props.children} 7 |
8 | ) 9 | } 10 | 11 | export default MainContainer; -------------------------------------------------------------------------------- /app/client/src/components/PageDetailModal.tsx: -------------------------------------------------------------------------------- 1 | import PageDetailArea from "./PageDetailArea"; 2 | import {Drawer} from "@mui/material"; 3 | import React from "react"; 4 | import {PageOperateEvent} from "./PageOperationButtons"; 5 | 6 | export default function PageDetailModal({ 7 | selectedPageId, 8 | operateSuccess, 9 | onClose, 10 | }: 11 | { 12 | selectedPageId: number, 13 | operateSuccess: (event: PageOperateEvent) => void, 14 | onClose?: () => void 15 | }) { 16 | return 0} onClose={onClose} anchor={'right'}> 17 |
18 | {selectedPageId > 0 && 19 | 20 | } 21 |
22 |
; 23 | } -------------------------------------------------------------------------------- /app/client/src/components/Sidebar/LibraryNavTree.tsx: -------------------------------------------------------------------------------- 1 | import LocalLibraryOutlinedIcon from "@mui/icons-material/LocalLibraryOutlined"; 2 | import NavTreeView from "./NavTreeView"; 3 | import * as React from "react"; 4 | import navLabels from "./NavLabels"; 5 | 6 | export default function LibraryNavTree({selectedNodeId}:{selectedNodeId:string}){ 7 | return ; 30 | } -------------------------------------------------------------------------------- /app/client/src/components/Sidebar/Sidebar.module.css: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | @apply text-[0.85rem]; 3 | } 4 | .sidebox { 5 | @apply border-b-gray-300 pb-3 pt-2; 6 | border-bottom-width: 1px; 7 | border-bottom-style: solid; 8 | } 9 | .folderHead, .sideItem { 10 | @apply hover:bg-slate-200; 11 | cursor: pointer; 12 | } 13 | .folderHead { 14 | @apply text-gray-600 flex items-center py-1 pl-1; 15 | } 16 | .folderChild { 17 | @apply pl-2; 18 | } 19 | /*.folderIcon {*/ 20 | /* @apply p-1 pt-0;*/ 21 | /* .arrowDown {*/ 22 | /* @apply transform rotate-90;*/ 23 | /* }*/ 24 | /* svg {*/ 25 | /* @apply text-xs block;*/ 26 | /* }*/ 27 | /*}*/ 28 | 29 | /*.sideList {*/ 30 | /* .sideItem {*/ 31 | /* @apply flex items-center py-1 pl-3;*/ 32 | /* .itemIcon {*/ 33 | /* @apply pr-1;*/ 34 | /* svg {*/ 35 | /* @apply block;*/ 36 | /* }*/ 37 | /* }*/ 38 | /* .itemName {*/ 39 | /* @apply leading-7 block;*/ 40 | /* }*/ 41 | /* }*/ 42 | /*}*/ 43 | -------------------------------------------------------------------------------- /app/client/src/components/SmartMoment.tsx: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | const SmartMoment = ({dt}) => { 4 | function getMoment() { 5 | let text = ""; 6 | if (moment(dt).isAfter(moment().add(-1, "d"))) { 7 | text = moment(dt).fromNow(true); 8 | } else if (moment(dt).isBefore(moment().startOf("year"))) { 9 | text = moment(dt).format('ll'); 10 | } else { 11 | text = moment(dt).format('M-D HH:mm'); 12 | } 13 | return text; 14 | } 15 | 16 | return ( 17 | 18 | {getMoment()} 19 | 20 | ) 21 | } 22 | 23 | export default SmartMoment; -------------------------------------------------------------------------------- /app/client/src/components/Tweet/DownloadButton.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | export default function DownloadButton({src, className,order}: { src: string, className?: string,order?:number }) { 5 | function handleClick() { 6 | 7 | } 8 | 9 | return ( 10 | 27 | ); 28 | } -------------------------------------------------------------------------------- /app/client/src/components/Tweet/Tweet.module.css: -------------------------------------------------------------------------------- 1 | .mainSize { 2 | font-size: 15px; 3 | font-weight: 400; 4 | } 5 | 6 | .mainColor,.mainColor a { 7 | color: rgb(15, 20, 25) !important; 8 | } 9 | 10 | .secondaryColor,.secondaryColor a { 11 | color: rgb(83, 100, 113) !important; 12 | } 13 | 14 | .mainBorder{ 15 | border: 1px solid rgb(207, 217, 222); 16 | border-radius: 16px; 17 | } 18 | .cardLink{ 19 | text-decoration: none; 20 | } 21 | .cardLink:hover { 22 | text-decoration: none !important; 23 | background: #ccc; 24 | } 25 | .tweet a, .mainLink{ 26 | color: rgb(29, 155, 240); 27 | text-decoration: none; 28 | } 29 | .tweet a:hover, .mainLink:hover{ 30 | text-decoration: underline; 31 | } -------------------------------------------------------------------------------- /app/client/src/components/Tweet/TweetRoot.tsx: -------------------------------------------------------------------------------- 1 | import CardMedia from "@mui/material/CardMedia"; 2 | import * as React from "react"; 3 | import Tweet from "./Tweet"; 4 | import {TweetProperties} from "../../interfaces/tweetProperties"; 5 | import {PageItem} from "../../api"; 6 | import RepeatIcon from "@mui/icons-material/Repeat"; 7 | import styles from './Tweet.module.css'; 8 | 9 | export default function TweetRoot({tweetProps, page}: { tweetProps: TweetProperties, page: PageItem }) { 10 | const tweet = tweetProps.retweetedTweet || tweetProps; 11 | return
12 | { 13 | tweetProps.retweetedTweet && 22 | } 23 |
24 |
25 | } -------------------------------------------------------------------------------- /app/client/src/components/common/ConditionalWrapper.tsx: -------------------------------------------------------------------------------- 1 | export default function ConditionalWrapper({condition, wrapper, children}) { 2 | return condition ? wrapper(children) : children; 3 | } -------------------------------------------------------------------------------- /app/client/src/components/common/TransitionAlert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Alert, {AlertProps} from '@mui/material/Alert'; 3 | import IconButton from '@mui/material/IconButton'; 4 | import Collapse from '@mui/material/Collapse'; 5 | import CloseIcon from '@mui/icons-material/Close'; 6 | 7 | export default function TransitionAlert(props: AlertProps) { 8 | const [open, setOpen] = React.useState(true); 9 | 10 | return ( 11 | 12 | { 20 | setOpen(false); 21 | }} 22 | > 23 | 24 | 25 | } 26 | > 27 | {props.children} 28 | 29 | 30 | ); 31 | } -------------------------------------------------------------------------------- /app/client/src/domain/electronTypes.ts: -------------------------------------------------------------------------------- 1 | export const enum WindowStateListenerType { 2 | Maximized, 3 | Focused, 4 | Fullscreen 5 | } 6 | 7 | // export enum WindowStateListenerType{ 8 | // Maximized = 'maximized', 9 | // Focused = 'focused', 10 | // Fullscreen = 'fullscreen', 11 | // } -------------------------------------------------------------------------------- /app/client/src/domain/index.ts: -------------------------------------------------------------------------------- 1 | // You can include shared interfaces/types in a separate file 2 | // and then use them in any component by importing them. For 3 | // example, to import the interface below do: 4 | // 5 | // import User from 'path/to/interfaces'; 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | 8 | // import {IpcRenderer} from 'electron' 9 | // 10 | // declare global { 11 | // // eslint-disable-next-line @typescript-eslint/no-namespace 12 | // namespace NodeJS { 13 | // interface Global { 14 | // ipcRenderer: IpcRenderer 15 | // } 16 | // } 17 | // } 18 | 19 | export {}; 20 | 21 | declare global { 22 | interface Window { 23 | electron: any 24 | } 25 | } -------------------------------------------------------------------------------- /app/client/src/domain/pageQueryKey.ts: -------------------------------------------------------------------------------- 1 | export const enum PageQueryKey { 2 | // RecentlyRead = 'page_list_recently_read', 3 | // Archive = 'page_list_archive', 4 | // MyList = 'page_list_my_list', 5 | // ReadLater = 'page_list_read_later', 6 | // Starred = 'page_list_starred', 7 | PageDetail = 'page_detail', 8 | PageList = 'page_list', 9 | Search = 'page_search', 10 | // ConnectorListPrefix = 'connector_list_', 11 | // AllFeeds = 'all_feeds', 12 | // FolderListPrefix = 'folder_list_' 13 | } -------------------------------------------------------------------------------- /app/client/src/domain/utils.ts: -------------------------------------------------------------------------------- 1 | import {PageFilterOptions} from "../components/PageFilters"; 2 | import {PageListFilter} from "../components/PageList"; 3 | 4 | export function getPageListFilter(filterOptions:PageFilterOptions) : PageListFilter{ 5 | return { 6 | sort: filterOptions.defaultSortValue, 7 | asc: filterOptions.asc, 8 | contentFilterType: filterOptions.contentFilterType, 9 | startDate: filterOptions.startDate || undefined, 10 | endDate: filterOptions.endDate || undefined 11 | } 12 | } -------------------------------------------------------------------------------- /app/client/src/env.ts: -------------------------------------------------------------------------------- 1 | export const API_BASE = "http://localhost:8080"; -------------------------------------------------------------------------------- /app/client/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import reportWebVitals from "./reportWebVitals"; 5 | import "./styles/globals.css"; 6 | import {QueryClient, QueryClientProvider} from "@tanstack/react-query"; 7 | import {ReactQueryDevtools} from "@tanstack/react-query-devtools"; 8 | import moment from "moment"; 9 | import 'moment/locale/zh-cn' 10 | import {SnackbarProvider} from "notistack"; 11 | 12 | const queryClient = new QueryClient({ 13 | defaultOptions: { 14 | queries: { 15 | refetchOnWindowFocus: false, 16 | retry: false, 17 | refetchOnReconnect: false, 18 | refetchOnMount: true, 19 | } 20 | }, 21 | }); 22 | moment.locale('zh-cn'); 23 | 24 | const root = ReactDOM.createRoot( 25 | document.getElementById("root") as HTMLElement 26 | ); 27 | root.render( 28 | // 29 | 30 | {/**/} 31 | 32 | 33 | 34 | 35 | // 36 | ); 37 | 38 | // If you want to start measuring performance in your app, pass a function 39 | // to log results (for example: reportWebVitals(console.log)) 40 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 41 | reportWebVitals(); 42 | -------------------------------------------------------------------------------- /app/client/src/interfaces/connectorType.ts: -------------------------------------------------------------------------------- 1 | export const enum ConnectorType { 2 | RSS = 1, 3 | GITHUB = 2 4 | } -------------------------------------------------------------------------------- /app/client/src/interfaces/githubRepoProperties.ts: -------------------------------------------------------------------------------- 1 | export interface GithubRepoProperties{ 2 | name: string; 3 | nodeId: string; 4 | defaultBranch: string; 5 | stargazersCount: number; 6 | forksCount: number; 7 | watchersCount: number; 8 | homepage: string; 9 | topics: string[]; 10 | updatedAt: string; 11 | } -------------------------------------------------------------------------------- /app/client/src/interfaces/librarySaveStatus.ts: -------------------------------------------------------------------------------- 1 | export const enum LibrarySaveStatus { 2 | NotSaved = 0, 3 | Saved = 1, 4 | Archived = 2 5 | } -------------------------------------------------------------------------------- /app/client/src/model.d.ts: -------------------------------------------------------------------------------- 1 | export type SORT_VALUE = 'ARCHIVED_AT' | 'CONNECTED_AT' | 'CREATED_AT' | 'LAST_READ_AT' | 'READ_LATER_AT' | 'SAVED_AT' | 'STARRED_AT' | 'VOTE_SCORE'; 2 | 3 | export type ContentType = 'BROWSER_HISTORY' | 'MARKDOWN'| 'QUOTED_TWEET' | 'TWEET'; -------------------------------------------------------------------------------- /app/client/src/pages/AllFeeds.tsx: -------------------------------------------------------------------------------- 1 | import PageList from "../components/PageList"; 2 | import MainContainer from "../components/MainContainer"; 3 | import navLabels from "../components/Sidebar/NavLabels"; 4 | import {PageControllerApiFactory} from "../api"; 5 | import {ConnectorType} from "../interfaces/connectorType"; 6 | import {useState} from "react"; 7 | import PageFilters, {PageFilterOptions} from "../components/PageFilters"; 8 | import {getPageListFilter} from "../domain/utils"; 9 | 10 | const AllFeeds = () => { 11 | function markAllAsRead() { 12 | return PageControllerApiFactory().markReadByConnectorTypeUsingPOST(ConnectorType.RSS); 13 | } 14 | 15 | const [pageFilterOptions, setPageFilterOptions] = useState({ 16 | defaultSortValue: 'CONNECTED_AT', 17 | sortFields: [{ 18 | value: 'CONNECTED_AT', 19 | label: 'Recently connected' 20 | }], 21 | asc: false, 22 | hideContentTypeFilter: true 23 | }) 24 | 25 | function handleFilterChange(options: PageFilterOptions) { 26 | setPageFilterOptions(options); 27 | } 28 | 29 | return ( 30 | 31 | } 40 | /> 41 | 42 | ) 43 | }; 44 | 45 | export default AllFeeds; 46 | -------------------------------------------------------------------------------- /app/client/src/pages/Archive.tsx: -------------------------------------------------------------------------------- 1 | import PageList from "../components/PageList"; 2 | import MainContainer from "../components/MainContainer"; 3 | import navLabels from "../components/Sidebar/NavLabels"; 4 | import {useState} from "react"; 5 | import PageFilters, {PageFilterOptions} from "../components/PageFilters"; 6 | import {getPageListFilter} from "../domain/utils"; 7 | 8 | 9 | const MyList = () => { 10 | const [pageFilterOptions, setPageFilterOptions] = useState({ 11 | defaultSortValue: 'ARCHIVED_AT', 12 | sortFields: [{ 13 | value: 'ARCHIVED_AT', 14 | label: 'Recently archived' 15 | }], 16 | asc: false, 17 | }) 18 | 19 | function handleFilterChange(options: PageFilterOptions) { 20 | setPageFilterOptions(options); 21 | } 22 | 23 | return ( 24 | 25 | } 32 | /> 33 | 34 | ) 35 | }; 36 | 37 | export default MyList; 38 | -------------------------------------------------------------------------------- /app/client/src/pages/Index.tsx: -------------------------------------------------------------------------------- 1 | import PageList from "../components/PageList"; 2 | import MainContainer from "../components/MainContainer"; 3 | import navLabels from "../components/Sidebar/NavLabels"; 4 | import React, {useState} from "react"; 5 | import PageFilters, {PageFilterOptions} from "../components/PageFilters"; 6 | import {getPageListFilter} from "../domain/utils"; 7 | 8 | 9 | const Index = () => { 10 | const [pageFilterOptions, setPageFilterOptions] = useState({ 11 | defaultSortValue: 'LAST_READ_AT', 12 | sortFields: [{ 13 | value: 'LAST_READ_AT', 14 | label: 'Recently read' 15 | }], 16 | asc: false, 17 | }) 18 | 19 | function handleFilterChange(options: PageFilterOptions) { 20 | setPageFilterOptions(options); 21 | } 22 | 23 | return ( 24 | 25 | } 28 | /> 29 | 30 | ) 31 | }; 32 | 33 | export default Index; 34 | -------------------------------------------------------------------------------- /app/client/src/pages/MyList.tsx: -------------------------------------------------------------------------------- 1 | import PageList from "../components/PageList"; 2 | import MainContainer from "../components/MainContainer"; 3 | import navLabels from "../components/Sidebar/NavLabels"; 4 | import {useState} from "react"; 5 | import PageFilters, {PageFilterOptions} from "../components/PageFilters"; 6 | import {getPageListFilter} from "../domain/utils"; 7 | 8 | const MyList = () => { 9 | const [pageFilterOptions, setPageFilterOptions] = useState({ 10 | defaultSortValue: 'SAVED_AT', 11 | sortFields: [{ 12 | value: 'SAVED_AT', 13 | label: 'Recently saved' 14 | }], 15 | asc: false, 16 | }) 17 | 18 | function handleFilterChange(options: PageFilterOptions) { 19 | setPageFilterOptions(options); 20 | } 21 | 22 | return ( 23 | 24 | } 31 | /> 32 | 33 | ) 34 | }; 35 | 36 | export default MyList; 37 | -------------------------------------------------------------------------------- /app/client/src/pages/Page.tsx: -------------------------------------------------------------------------------- 1 | import {safeInt} from "../common/typeUtils"; 2 | import {useParams} from "react-router-dom"; 3 | import MainContainer from "../components/MainContainer"; 4 | import * as React from "react"; 5 | import PageDetailArea from "../components/PageDetailArea"; 6 | import {PageOperation} from "../components/PageOperationButtons"; 7 | import {useSnackbar} from "notistack"; 8 | 9 | const Page = () => { 10 | const {id} = useParams<"id">(); 11 | const {enqueueSnackbar} = useSnackbar(); 12 | 13 | function operateSuccess(event) { 14 | if (event.operation === PageOperation.delete) { 15 | enqueueSnackbar('Page deleted.', { 16 | variant: "success", 17 | anchorOrigin: {vertical: "bottom", horizontal: "center"} 18 | }); 19 | } 20 | } 21 | 22 | return ( 23 | 24 |
25 |
26 | 27 |
28 |
29 |
30 |
31 |
32 | ); 33 | }; 34 | 35 | export default Page; -------------------------------------------------------------------------------- /app/client/src/pages/ReadLater.tsx: -------------------------------------------------------------------------------- 1 | import PageList from "../components/PageList"; 2 | import MainContainer from "../components/MainContainer"; 3 | import navLabels from "../components/Sidebar/NavLabels"; 4 | import {useState} from "react"; 5 | import PageFilters, {PageFilterOptions} from "../components/PageFilters"; 6 | import {getPageListFilter} from "../domain/utils"; 7 | 8 | const MyList = () => { 9 | const [pageFilterOptions, setPageFilterOptions] = useState({ 10 | defaultSortValue: 'READ_LATER_AT', 11 | sortFields: [{ 12 | value: 'READ_LATER_AT', 13 | label: 'Recently saved' 14 | }], 15 | asc: false, 16 | }) 17 | 18 | function handleFilterChange(options: PageFilterOptions) { 19 | setPageFilterOptions(options); 20 | } 21 | 22 | return ( 23 | 24 | } 31 | /> 32 | 33 | ) 34 | }; 35 | 36 | export default MyList; 37 | -------------------------------------------------------------------------------- /app/client/src/pages/Starred.tsx: -------------------------------------------------------------------------------- 1 | import PageList from "../components/PageList"; 2 | import MainContainer from "../components/MainContainer"; 3 | import navLabels from "../components/Sidebar/NavLabels"; 4 | import {useState} from "react"; 5 | import PageFilters, {PageFilterOptions} from "../components/PageFilters"; 6 | import {getPageListFilter} from "../domain/utils"; 7 | 8 | const MyList = () => { 9 | const [pageFilterOptions, setPageFilterOptions] = useState({ 10 | defaultSortValue: 'STARRED_AT', 11 | sortFields: [{ 12 | value: 'STARRED_AT', 13 | label: 'Recently starred' 14 | }], 15 | asc: false, 16 | }) 17 | 18 | function handleFilterChange(options: PageFilterOptions) { 19 | setPageFilterOptions(options); 20 | } 21 | 22 | return ( 23 | 24 | } 31 | /> 32 | 33 | ) 34 | }; 35 | 36 | export default MyList; 37 | -------------------------------------------------------------------------------- /app/client/src/pages/Twitter.tsx: -------------------------------------------------------------------------------- 1 | import PageList from "../components/PageList"; 2 | import navLabels from "../components/Sidebar/NavLabels"; 3 | import MainContainer from "../components/MainContainer"; 4 | import React, {useState} from "react"; 5 | import PageFilters, {PageFilterOptions} from "../components/PageFilters"; 6 | import {getPageListFilter} from "../domain/utils"; 7 | 8 | export default function Twitter() { 9 | const [pageFilterOptions, setPageFilterOptions] = useState({ 10 | defaultSortValue: 'CREATED_AT', 11 | sortFields: [{ 12 | value: 'CREATED_AT', 13 | label: 'Recently hunted' 14 | }, { 15 | value: 'CONNECTED_AT', 16 | label: 'Recently tweeted' 17 | }, { 18 | value: 'LAST_READ_AT', 19 | label: 'Recently read' 20 | }, { 21 | value: 'VOTE_SCORE', 22 | label: 'Most popular' 23 | }], 24 | asc: false, 25 | hideContentTypeFilter: true 26 | }) 27 | 28 | function handleFilterChange(options: PageFilterOptions) { 29 | setPageFilterOptions(options); 30 | } 31 | 32 | return 33 | }/> 40 | ; 41 | } -------------------------------------------------------------------------------- /app/client/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.css'; -------------------------------------------------------------------------------- /app/client/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /app/client/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /app/client/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 3 | important: '', 4 | theme: { 5 | extend: { 6 | boxShadow:{ 7 | 'heavy':'rgba(0, 0, 0, 0.2) 0px 3px 5px -1px, rgba(0, 0, 0, 0.14) 0px 6px 10px 0px, rgba(0, 0, 0, 0.12) 0px 1px 18px 0px' 8 | } 9 | }, 10 | }, 11 | corePlugins: { 12 | // Remove Tailwind CSS's preflight style so it can use the MUI's preflight instead (CssBaseline). 13 | preflight: false, 14 | }, 15 | plugins: [ 16 | require('@tailwindcss/line-clamp'), 17 | ], 18 | }; -------------------------------------------------------------------------------- /app/client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": false, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /app/electron/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | huntly-server.jar* 9 | 10 | # testing 11 | /coverage 12 | 13 | # next.js 14 | /renderer/.next/ 15 | /renderer/out/ 16 | 17 | # production 18 | /main 19 | /dist 20 | 21 | .idea/ 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # local env files 33 | .env.local 34 | .env.development.local 35 | .env.test.local 36 | .env.production.local 37 | 38 | # vercel 39 | .vercel 40 | 41 | db.sqlite* -------------------------------------------------------------------------------- /app/electron/README.md: -------------------------------------------------------------------------------- 1 | yarn config set ELECTRON_MIRROR https://npmmirror.com/mirrors/electron/ -------------------------------------------------------------------------------- /app/electron/public/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Huntly 6 | 7 | 8 | 9 |
10 | 11 | 12 | -------------------------------------------------------------------------------- /app/electron/src/main/preload.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-namespace */ 2 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 3 | import { ipcRenderer, IpcRenderer, contextBridge } from 'electron' 4 | import { utilsBridge } from "./utilsBridge"; 5 | 6 | declare global { 7 | var ipcRenderer: IpcRenderer 8 | } 9 | 10 | // Since we disabled nodeIntegration we can reintroduce 11 | // needed node functionality here 12 | process.once('loaded', () => { 13 | global.ipcRenderer = ipcRenderer 14 | }) 15 | 16 | contextBridge.exposeInMainWorld("electron", { utilsBridge: utilsBridge }); -------------------------------------------------------------------------------- /app/electron/src/main/settingStore.ts: -------------------------------------------------------------------------------- 1 | import Store from "electron-store"; 2 | 3 | export default class SettingStore { 4 | store = new Store({ 5 | schema: { 6 | serverPort: { 7 | type: 'number', 8 | maximum: 65535, 9 | minimum: 1, 10 | default: 8123 11 | }, 12 | removeServerUrl: { 13 | type: 'string', 14 | format: 'uri' 15 | } 16 | } 17 | }); 18 | 19 | getServerPort() { 20 | return this.store.get("serverPort"); 21 | } 22 | 23 | setServerPort(port: number) { 24 | this.store.set("serverPort", port); 25 | } 26 | } -------------------------------------------------------------------------------- /app/electron/src/main/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "emitDecoratorMetadata": true, 5 | "allowJs": true, 6 | "alwaysStrict": true, 7 | "esModuleInterop": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "strictPropertyInitialization": false, 10 | "isolatedModules": true, 11 | "jsx": "preserve", 12 | "lib": ["dom", "es2017"], 13 | "module": "commonjs", 14 | "moduleResolution": "node", 15 | "noEmit": false, 16 | "noFallthroughCasesInSwitch": true, 17 | // "noUnusedLocals": true, 18 | // "noUnusedParameters": true, 19 | "noImplicitAny": false, 20 | "resolveJsonModule": true, 21 | "skipLibCheck": true, 22 | "strict": true, 23 | "target": "esnext", 24 | "outDir": "../../dist/" 25 | }, 26 | "exclude": ["node_modules"], 27 | "include": ["**/*.ts", "**/*.tsx", "**/*.js"] 28 | } 29 | -------------------------------------------------------------------------------- /app/electron/src/main/types.ts: -------------------------------------------------------------------------------- 1 | export const enum WindowStateListenerType { 2 | Maximized, 3 | Focused, 4 | Fullscreen 5 | } -------------------------------------------------------------------------------- /app/electron/src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import {createRoot} from "react-dom/client"; 2 | import {useEffect, useState} from "react"; 3 | 4 | export default function App() { 5 | const {utilsBridge} = window.electron; 6 | const [loadingServer, setLoadingServer] = useState(true); 7 | 8 | useEffect(() => { 9 | utilsBridge.startServer(); 10 | location.href = "http://localhost:" + utilsBridge.getServerPort(); 11 | }, []); 12 | 13 | return
14 | {loadingServer &&
loading server...
} 15 |
; 16 | } 17 | 18 | const root = createRoot( 19 | document.getElementById("root") as HTMLElement 20 | ); 21 | 22 | root.render( 23 | 24 | ); -------------------------------------------------------------------------------- /app/electron/src/renderer/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/electron/src/renderer/app.css -------------------------------------------------------------------------------- /app/electron/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "target": "es2021", 5 | "module": "commonjs", 6 | "lib": ["dom", "es2021"], 7 | "jsx": "react-jsx", 8 | "strict": false, 9 | "sourceMap": true, 10 | "baseUrl": "./src", 11 | "moduleResolution": "node", 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "resolveJsonModule": true, 15 | "allowJs": true, 16 | }, 17 | "exclude": ["test", "release/build", "release/app/dist"] 18 | } -------------------------------------------------------------------------------- /app/electron/webpack/webpack.common.js: -------------------------------------------------------------------------------- 1 | const webpack = require("webpack"); 2 | const path = require("path"); 3 | const CopyPlugin = require("copy-webpack-plugin"); 4 | const srcDir = path.join(__dirname, "..", "src/renderer"); 5 | 6 | module.exports = { 7 | entry: { 8 | app: path.join(srcDir, 'App.tsx'), 9 | }, 10 | output: { 11 | path: path.join(__dirname, "../dist/renderer/"), 12 | filename: "[name].js", 13 | }, 14 | optimization: { 15 | splitChunks: { 16 | name: "vendor", 17 | }, 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.css$/i, 23 | use: [ 24 | {loader: "style-loader"}, 25 | {loader: "css-loader"} 26 | ] 27 | }, 28 | { 29 | test: /\.tsx?$/, 30 | use: "ts-loader", 31 | exclude: /node_modules/, 32 | }, 33 | ], 34 | }, 35 | resolve: { 36 | extensions: [".ts", ".tsx", ".js", ".css"], 37 | }, 38 | plugins: [ 39 | new CopyPlugin({ 40 | patterns: [{from: ".", to: "../", context: "public"}], 41 | options: {}, 42 | }), 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /app/electron/webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | devtool: 'inline-source-map', 6 | mode: 'development', 7 | }); -------------------------------------------------------------------------------- /app/electron/webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production' 6 | }); -------------------------------------------------------------------------------- /app/extension/.github/workflows/build._yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | #name: build 5 | # 6 | #on: 7 | # push: 8 | # branches: [ master ] 9 | # pull_request: 10 | # branches: [ master ] 11 | # 12 | #jobs: 13 | # build: 14 | # 15 | # runs-on: ubuntu-latest 16 | # 17 | # strategy: 18 | # matrix: 19 | # node-version: [14.x, 15.x] 20 | # # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | # 22 | # steps: 23 | # - uses: actions/checkout@v2 24 | # - name: Use Node.js ${{ matrix.node-version }} 25 | # uses: actions/setup-node@v1 26 | # with: 27 | # node-version: ${{ matrix.node-version }} 28 | # - run: npm ci 29 | # - run: npm run build --if-present 30 | # - run: npm test 31 | -------------------------------------------------------------------------------- /app/extension/.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | node_modules/ 3 | dist/ 4 | tmp/ 5 | .idea/ 6 | .DS_Store 7 | yarn-error.log 8 | 9 | .output/ 10 | dist/ 11 | dist_firefox/ -------------------------------------------------------------------------------- /app/extension/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "files.eol": "\n", 4 | "json.schemas": [ 5 | { 6 | "fileMatch": [ 7 | "/manifest.json" 8 | ], 9 | "url": "http://json.schemastore.org/chrome-manifest" 10 | } 11 | ] 12 | } -------------------------------------------------------------------------------- /app/extension/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "command": "npm", 6 | "tasks": [ 7 | { 8 | "label": "install", 9 | "type": "shell", 10 | "command": "npm", 11 | "args": ["install"] 12 | }, 13 | { 14 | "label": "update", 15 | "type": "shell", 16 | "command": "npm", 17 | "args": ["update"] 18 | }, 19 | { 20 | "label": "test", 21 | "type": "shell", 22 | "command": "npm", 23 | "args": ["run", "test"] 24 | }, 25 | { 26 | "label": "build", 27 | "type": "shell", 28 | "group": "build", 29 | "command": "npm", 30 | "args": ["run", "watch"] 31 | } 32 | ] 33 | } -------------------------------------------------------------------------------- /app/extension/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tomofumi Chiba 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/extension/README.md: -------------------------------------------------------------------------------- 1 | # Chrome Extension TypeScript Starter 2 | 3 | ![build](https://github.com/chibat/chrome-extension-typescript-starter/workflows/build/badge.svg) 4 | 5 | Chrome Extension, TypeScript and Visual Studio Code 6 | 7 | ## Prerequisites 8 | 9 | * [node + yarn](https://nodejs.org/) (Current Version) 10 | 11 | ## Option 12 | 13 | * [Visual Studio Code](https://code.visualstudio.com/) 14 | 15 | ## Includes the following 16 | 17 | * TypeScript 18 | * Webpack 19 | * React 20 | * Jest 21 | * Example Code 22 | * Chrome Storage 23 | * Options Version 2 24 | * content script 25 | * count up badge number 26 | * background 27 | 28 | ## Project Structure 29 | 30 | * src/typescript: TypeScript source files 31 | * src/assets: static files 32 | * dist: Chrome Extension directory 33 | * dist/js: Generated JavaScript files 34 | 35 | ## Setup 36 | 37 | ``` 38 | yarn 39 | ``` 40 | 41 | ## Import as Visual Studio Code project 42 | 43 | ... 44 | 45 | ## Build 46 | 47 | ``` 48 | yarn build 49 | ``` 50 | 51 | ## Build in watch mode 52 | 53 | ### terminal 54 | 55 | ``` 56 | yarn watch 57 | ``` 58 | 59 | ### Visual Studio Code 60 | 61 | Run watch mode. 62 | 63 | type `Ctrl + Shift + B` 64 | 65 | ## Load extension to chrome 66 | 67 | Load `dist` directory 68 | 69 | ## Test 70 | `npx jest` or `npm run test` 71 | 72 | ```sh 73 | export NODE_OPTIONS=--openssl-legacy-provider 74 | ``` -------------------------------------------------------------------------------- /app/extension/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "roots": [ 3 | "src" 4 | ], 5 | "transform": { 6 | "^.+\\.ts$": "ts-jest" 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /app/extension/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: 3 | { 4 | 'postcss-preset-env': {}, 5 | 'tailwindcss/nesting': {}, 6 | tailwindcss: {}, 7 | autoprefixer: {}, 8 | }, 9 | }; -------------------------------------------------------------------------------- /app/extension/public/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/extension/public/favicon-128x128.png -------------------------------------------------------------------------------- /app/extension/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/extension/public/favicon-16x16.png -------------------------------------------------------------------------------- /app/extension/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/extension/public/favicon-32x32.png -------------------------------------------------------------------------------- /app/extension/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/extension/public/icon.png -------------------------------------------------------------------------------- /app/extension/public/manifest-firefox.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Huntly", 4 | "description": "Huntly - Automatic saving browsed contents", 5 | "version": "0.3.1", 6 | "options_ui": { 7 | "page": "options.html" 8 | }, 9 | "icons": { 10 | "16": "favicon-16x16.png", 11 | "32": "favicon-32x32.png", 12 | "128": "favicon-128x128.png" 13 | }, 14 | "action": { 15 | "default_icon": "favicon-32x32.png", 16 | "default_popup": "popup.html" 17 | }, 18 | "web_accessible_resources": [ 19 | { 20 | "resources": [ 21 | "/js/tweet_interceptor.js" 22 | ], 23 | "matches": [ 24 | "http://*/*", 25 | "https://*/*" 26 | ] 27 | } 28 | ], 29 | "content_scripts": [ 30 | { 31 | "matches": [ 32 | "" 33 | ], 34 | "js": [ 35 | "js/vendor.js", 36 | "js/content_script.js" 37 | ], 38 | "run_at": "document_start" 39 | }, 40 | { 41 | "matches": [ 42 | "" 43 | ], 44 | "js": [ 45 | "js/web_clipper.js" 46 | ], 47 | "run_at": "document_end" 48 | } 49 | ], 50 | "background": { 51 | "scripts": ["js/background.js"] 52 | }, 53 | "permissions": [ 54 | "storage", 55 | "tabs" 56 | ], 57 | "host_permissions": [ 58 | "" 59 | ], 60 | "browser_specific_settings": { 61 | "gecko": { 62 | "id": "huntlyextension@gmail.com" 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/extension/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Huntly", 4 | "description": "Huntly - Automatic saving browsed contents", 5 | "version": "0.3.8", 6 | "options_ui": { 7 | "page": "options.html" 8 | }, 9 | "icons": { 10 | "16": "favicon-16x16.png", 11 | "32": "favicon-32x32.png", 12 | "128": "favicon-128x128.png" 13 | }, 14 | "action": { 15 | "default_icon": "favicon-32x32.png", 16 | "default_popup": "popup.html" 17 | }, 18 | "web_accessible_resources": [ 19 | { 20 | "resources": [ 21 | "/js/tweet_interceptor.js" 22 | ], 23 | "matches": [ 24 | "http://*/*", 25 | "https://*/*" 26 | ] 27 | } 28 | ], 29 | "content_scripts": [ 30 | { 31 | "matches": [ 32 | "" 33 | ], 34 | "js": [ 35 | "js/vendor.js", 36 | "js/content_script.js" 37 | ], 38 | "run_at": "document_start" 39 | }, 40 | { 41 | "matches": [ 42 | "" 43 | ], 44 | "js": [ 45 | "js/web_clipper.js" 46 | ], 47 | "run_at": "document_end" 48 | } 49 | ], 50 | "background": { 51 | "service_worker": "js/background.js" 52 | }, 53 | "permissions": [ 54 | "storage", 55 | "tabs" 56 | ], 57 | "host_permissions": [ 58 | "" 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /app/extension/public/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Huntly Extension Options 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | -------------------------------------------------------------------------------- /app/extension/public/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Huntly Extension's Popup 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/extension/src/__tests__/sum.ts: -------------------------------------------------------------------------------- 1 | import { sum } from "../sum"; 2 | 3 | test("1 + 1 = 2", () => { 4 | expect(sum(1, 1)).toBe(2); 5 | }); 6 | 7 | test("1 + 2 != 2", () => { 8 | expect(sum(1, 2)).not.toBe(2); 9 | }); 10 | -------------------------------------------------------------------------------- /app/extension/src/background.ts: -------------------------------------------------------------------------------- 1 | import {log} from "./logger"; 2 | import {readSyncStorageSettings} from "./storage"; 3 | import {autoSaveArticle, saveArticle, sendData} from "./services"; 4 | 5 | chrome.runtime.onMessage.addListener(function (msg: Message, sender, sendResponse) { 6 | if (msg.type === "auto_save_clipper") { 7 | autoSaveArticle(msg.payload).then(handleSaveArticleResponse); 8 | } else if (msg.type === "save_clipper") { 9 | saveArticle(msg.payload); 10 | } else if (msg.type === 'auto_save_tweets') { 11 | readSyncStorageSettings().then((settings) => { 12 | if (settings.autoSaveTweet) { 13 | sendData("tweet/saveTweets", msg.payload); 14 | } 15 | }); 16 | } else if (msg.type === 'read_tweet') { 17 | sendData("tweet/trackRead", msg.payload); 18 | } 19 | }); 20 | 21 | function handleSaveArticleResponse(resp: string) { 22 | log("save article result", resp); 23 | if (resp) { 24 | const json = JSON.parse(resp); 25 | chrome.runtime.sendMessage({ 26 | type: "save_clipper_success", 27 | payload: {id: json.data} 28 | }) 29 | } 30 | } 31 | 32 | chrome.tabs.onUpdated.addListener(function (tabId: number, changeInfo, tab) { 33 | if (changeInfo.status == "complete") { 34 | chrome.tabs.sendMessage(tabId, { 35 | type: "tab_complete" 36 | }) 37 | } 38 | }) -------------------------------------------------------------------------------- /app/extension/src/content_script.tsx: -------------------------------------------------------------------------------- 1 | import {log} from "./logger"; 2 | 3 | log("content script loaded"); 4 | 5 | // 在页面上插入代码,这样插入的代码才能访问页面中的 window 等全局变量 6 | const script = document.createElement('script'); 7 | script.setAttribute('type', 'text/javascript'); 8 | script.setAttribute('src', chrome.runtime.getURL('/js/tweet_interceptor.js')); 9 | document.documentElement.appendChild(script); 10 | 11 | window.addEventListener("message",function(event:MessageEvent){ 12 | chrome.runtime.sendMessage(event.data); 13 | }); -------------------------------------------------------------------------------- /app/extension/src/env.ts: -------------------------------------------------------------------------------- 1 | export const isDebugging = /dev/.test(process.env.NODE_ENV || '') -------------------------------------------------------------------------------- /app/extension/src/globals.d.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | 3 | declare global { 4 | interface Window { 5 | XMLHttpRequest: any; 6 | // fetch:fetch; 7 | } 8 | } -------------------------------------------------------------------------------- /app/extension/src/logger.ts: -------------------------------------------------------------------------------- 1 | import {isDebugging} from "./env"; 2 | 3 | export function log(...args: any[]) { 4 | if (isDebugging) { 5 | console.log(...args) 6 | } 7 | } 8 | 9 | export function logError(...args: any[]) { 10 | if (isDebugging) { 11 | console.error(...args) 12 | } 13 | } 14 | 15 | export function logWarn(...args: any[]) { 16 | if (isDebugging) { 17 | console.warn(...args) 18 | } 19 | } 20 | 21 | export function logInfo(...args: any[]) { 22 | if (isDebugging) { 23 | console.info(...args) 24 | } 25 | } 26 | 27 | export function logDebug(...args: any[]) { 28 | if (isDebugging) { 29 | console.debug(...args) 30 | } 31 | } -------------------------------------------------------------------------------- /app/extension/src/model.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | 3 | interface PageModel { 4 | title: string 5 | content: string, 6 | url: string, 7 | thumbUrl: string, 8 | description: string, 9 | author: string, 10 | siteName: string, 11 | language: string, 12 | category: string, 13 | isLiked: boolean, 14 | isFavorite: boolean, 15 | domain: string, 16 | faviconUrl: string, 17 | } 18 | 19 | interface Message { 20 | type: "auto_save_clipper" | "save_clipper" | 'tab_complete' | 'auto_save_tweets' | 'read_tweet' | 'parse_doc' | 'save_clipper_success' | 'article_preview', 21 | payload?: object 22 | } 23 | 24 | -------------------------------------------------------------------------------- /app/extension/src/model/librarySaveStatus.ts: -------------------------------------------------------------------------------- 1 | export enum LibrarySaveStatus { 2 | NotSaved = 0, 3 | Saved = 1, 4 | Archived = 2 5 | } -------------------------------------------------------------------------------- /app/extension/src/model/pageOperateResult.ts: -------------------------------------------------------------------------------- 1 | export type PageOperateResult = { 2 | id?: number; 3 | librarySaveStatus?: number, 4 | starred?: boolean, 5 | readLater?: boolean 6 | } -------------------------------------------------------------------------------- /app/extension/src/options.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .formHeader { 6 | @apply text-lg font-medium text-gray-700 pb-2 mb-2; 7 | border-bottom: 1px solid #ccc; 8 | } 9 | -------------------------------------------------------------------------------- /app/extension/src/options.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {createRoot} from "react-dom/client"; 3 | import {Settings} from "./settings"; 4 | import {CssBaseline, StyledEngineProvider} from "@mui/material"; 5 | 6 | const root = createRoot( 7 | document.getElementById("root") as HTMLElement 8 | ); 9 | 10 | root.render( 11 | 12 | 13 | 14 | 15 | ); 16 | -------------------------------------------------------------------------------- /app/extension/src/popup.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | body{ 6 | padding: 0; 7 | margin: 0; 8 | } 9 | .commonPadding{ 10 | @apply pl-4 pr-4; 11 | } 12 | .header{ 13 | border-bottom: 1px solid #bbb; 14 | background: #eeeff4; 15 | @apply mb-2 pt-1 pb-1 flex items-center text-base justify-between; 16 | } 17 | .mainBorder{ 18 | border: 1px solid rgb(207, 217, 222); 19 | } -------------------------------------------------------------------------------- /app/extension/src/storage.ts: -------------------------------------------------------------------------------- 1 | export const STORAGE_SERVER_URL = "serverUrl"; 2 | export const STORAGE_SERVER_URL_LIST = "serverUrlList"; 3 | export const STORAGE_AUTO_SAVE_ENABLED = "autoSaveEnabled"; 4 | export const STORAGE_AUTO_SAVE_MIN_SCORE = "autoSaveMinScore"; 5 | export const STORAGE_AUTO_SAVE_MIN_CONTENT_LENGTH = "autoSaveMinContentLength"; 6 | export const STORAGE_AUTO_SAVE_TWEET = "autoSaveTweet"; 7 | 8 | export type ServerUrlItem = { 9 | url: string, 10 | } 11 | 12 | export type StorageSettings = { 13 | serverUrl: string; 14 | serverUrlList: ServerUrlItem[]; 15 | autoSaveEnabled: boolean; 16 | autoSaveMinScore: number; 17 | autoSaveMinContentLength: number; 18 | autoSaveTweet: boolean; 19 | } 20 | 21 | export const DefaultStorageSettings: StorageSettings = { 22 | serverUrl: "", 23 | serverUrlList: [], 24 | autoSaveEnabled: true, 25 | autoSaveMinScore: 20, 26 | autoSaveMinContentLength: 40, 27 | autoSaveTweet: false 28 | } 29 | 30 | export async function readSyncStorageSettings(): Promise { 31 | const items = await chrome.storage.sync.get(DefaultStorageSettings); 32 | return { 33 | serverUrl: items[STORAGE_SERVER_URL] || DefaultStorageSettings.serverUrl, 34 | serverUrlList: items[STORAGE_SERVER_URL_LIST] || DefaultStorageSettings.serverUrlList, 35 | autoSaveEnabled: items[STORAGE_AUTO_SAVE_ENABLED], 36 | autoSaveMinScore: items[STORAGE_AUTO_SAVE_MIN_SCORE] || DefaultStorageSettings.autoSaveMinScore, 37 | autoSaveMinContentLength: items[STORAGE_AUTO_SAVE_MIN_CONTENT_LENGTH] || DefaultStorageSettings.autoSaveMinContentLength, 38 | autoSaveTweet: items[STORAGE_AUTO_SAVE_TWEET] 39 | }; 40 | } -------------------------------------------------------------------------------- /app/extension/src/sum.ts: -------------------------------------------------------------------------------- 1 | export function sum(x: number, y: number) { 2 | return x + y; 3 | } 4 | -------------------------------------------------------------------------------- /app/extension/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ["./src/*.{js,jsx,ts,tsx}", "./public/*.{html,js}","./dist/*.{html,js}"], 4 | corePlugins: { 5 | // Remove Tailwind CSS's preflight style so it can use the MUI's preflight instead (CssBaseline). 6 | preflight: false, 7 | }, 8 | theme: { 9 | extend: { 10 | boxShadow:{ 11 | 'heavy':'rgba(0, 0, 0, 0.2) 0px 3px 5px -1px, rgba(0, 0, 0, 0.14) 0px 6px 10px 0px, rgba(0, 0, 0, 0.12) 0px 1px 18px 0px' 12 | } 13 | }, 14 | }, 15 | plugins: [ 16 | ], 17 | } 18 | -------------------------------------------------------------------------------- /app/extension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": false, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "target": "es6", 7 | "esModuleInterop": true, 8 | "sourceMap": false, 9 | "rootDir": "src", 10 | "outDir": "dist/js", 11 | "noEmitOnError": true, 12 | "jsx": "react", 13 | "typeRoots": [ "node_modules/@types" ] 14 | }, 15 | "include": [ 16 | "src" 17 | ] 18 | } -------------------------------------------------------------------------------- /app/extension/webpack/webpack.dev.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | devtool: 'inline-source-map', 6 | mode: 'development', 7 | }); -------------------------------------------------------------------------------- /app/extension/webpack/webpack.prod.js: -------------------------------------------------------------------------------- 1 | const { merge } = require('webpack-merge'); 2 | const common = require('./webpack.common.js'); 3 | 4 | module.exports = merge(common, { 5 | mode: 'production' 6 | }); -------------------------------------------------------------------------------- /app/server/.mvn/maven.config: -------------------------------------------------------------------------------- 1 | -Drevision=0.3.8 -------------------------------------------------------------------------------- /app/server/huntly-common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | integrated 7 | com.huntly 8 | ${revision} 9 | 10 | 4.0.0 11 | 12 | huntly-common 13 | 14 | 15 | 11 16 | 11 17 | UTF-8 18 | 19 | 20 | 21 | 22 | org.apache.commons 23 | commons-lang3 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/api/model/ErrorDetail.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.api.model; 2 | 3 | 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.io.Serializable; 8 | 9 | /** 10 | * 错误详情 11 | * @author lcomplete 12 | */ 13 | @Getter 14 | @Setter 15 | public class ErrorDetail implements Serializable { 16 | 17 | private static final long serialVersionUID = 2616876803298549771L; 18 | 19 | private int code; 20 | 21 | private String message; 22 | 23 | private String type; 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/api/model/ErrorMessageType.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.api.model; 2 | 3 | 4 | /** 5 | * 错误消息类型 6 | * @author lcomplete 7 | */ 8 | public enum ErrorMessageType { 9 | 10 | /** 11 | * api code message 12 | */ 13 | API_CODE, 14 | /** 15 | * exception message 16 | */ 17 | EXCEPTION; 18 | } 19 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/api/model/ErrorResponse.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.api.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.io.Serializable; 7 | 8 | /** 9 | * 错误响应 10 | * @author lcomplete 11 | */ 12 | @Getter 13 | @Setter 14 | public class ErrorResponse implements Serializable { 15 | 16 | private static final long serialVersionUID = 706966450577903961L; 17 | 18 | private ErrorDetail error; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/entity/BaseEntity.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.entity; 2 | 3 | import java.io.Serializable; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | public abstract class BaseEntity implements Serializable { 9 | 10 | private static final long serialVersionUID = -763580109285060857L; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/enums/BaseEnum.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.enums; 2 | 3 | /** 4 | * @author lcomplete 5 | */ 6 | public interface BaseEnum { 7 | 8 | /** 9 | * 获取枚举标识 10 | * 11 | * @return 12 | */ 13 | Integer getCode(); 14 | 15 | /** 16 | * 获取枚举描述 17 | * 18 | * @return 19 | */ 20 | String getDesc(); 21 | 22 | /** 23 | * 通过枚举类型和code值获取对应的枚举类型 24 | * @param enumType 25 | * @param code 26 | * @param 27 | * @return 28 | */ 29 | static T valueOf(Class enumType, Integer code) { 30 | if (enumType == null || code == null) { 31 | return null; 32 | } 33 | T[] enumConstants = (T[]) enumType.getEnumConstants(); 34 | if (enumConstants == null) { 35 | return null; 36 | } 37 | for (T enumConstant : enumConstants) { 38 | int enumCode = enumConstant.getCode(); 39 | if (code.equals(enumCode)) { 40 | return enumConstant; 41 | } 42 | } 43 | return null; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/enums/EnumVo.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.enums; 2 | 3 | import lombok.Data; 4 | import lombok.experimental.Accessors; 5 | 6 | @Data 7 | @Accessors(chain = true) 8 | public class EnumVo { 9 | 10 | /** 11 | * 枚举code 12 | */ 13 | private Integer code; 14 | 15 | /** 16 | * 枚举描述 17 | */ 18 | private String desc; 19 | 20 | /** 21 | * 枚举类型 22 | */ 23 | private T baseEnum; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/BaseException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | /** 4 | * @author lcomplete 5 | */ 6 | public class BaseException extends StatefulException { 7 | private static final long serialVersionUID = 1L; 8 | 9 | public BaseException() { 10 | } 11 | 12 | public BaseException(String msg) { 13 | super(msg); 14 | } 15 | 16 | public BaseException(Throwable throwable) { 17 | super(throwable); 18 | } 19 | 20 | public BaseException(String msg, Throwable throwable) { 21 | super(msg, throwable); 22 | } 23 | 24 | public BaseException(int status, String msg) { 25 | super(status, msg); 26 | } 27 | 28 | public BaseException(int status, Throwable throwable) { 29 | super(status, throwable); 30 | } 31 | 32 | public BaseException(int status, String msg, Throwable throwable) { 33 | super(status, msg, throwable); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/BusinessException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | /** 4 | * 业务层通用异常(一般在service中抛出,service中的异常继承该异常) 5 | * @author lcomplete 6 | */ 7 | public class BusinessException extends BaseException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | public BusinessException() { 11 | super(); 12 | } 13 | 14 | public BusinessException(String msg) { 15 | super(msg); 16 | } 17 | 18 | public BusinessException(Throwable throwable) { 19 | super(throwable); 20 | } 21 | 22 | public BusinessException(String msg, Throwable throwable) { 23 | super(msg, throwable); 24 | } 25 | 26 | public BusinessException(int status, String msg) { 27 | super(status, msg); 28 | } 29 | 30 | public BusinessException(int status, Throwable throwable) { 31 | super(status, throwable); 32 | } 33 | 34 | public BusinessException(int status, String msg, Throwable throwable) { 35 | super(status, msg, throwable); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/DaoException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | /** 4 | * dao层异常 5 | * @author lcomplete 6 | */ 7 | public class DaoException extends BaseException { 8 | 9 | private static final long serialVersionUID = 1L; 10 | 11 | public DaoException() { 12 | } 13 | 14 | public DaoException(String msg) { 15 | super(msg); 16 | } 17 | 18 | public DaoException(Throwable throwable) { 19 | super(throwable); 20 | } 21 | 22 | public DaoException(String msg, Throwable throwable) { 23 | super(msg, throwable); 24 | } 25 | 26 | public DaoException(int status, String msg) { 27 | super(status, msg); 28 | } 29 | 30 | public DaoException(int status, Throwable throwable) { 31 | super(status, throwable); 32 | } 33 | 34 | public DaoException(int status, String msg, Throwable throwable) { 35 | super(status, msg, throwable); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/DuplicateRecordException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | /** 4 | * 数据记录重复异常 5 | * @author lcomplete 6 | */ 7 | public class DuplicateRecordException extends BaseException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | public DuplicateRecordException() { 11 | } 12 | 13 | public DuplicateRecordException(String msg) { 14 | super(msg); 15 | } 16 | 17 | public DuplicateRecordException(Throwable throwable) { 18 | super(throwable); 19 | } 20 | 21 | public DuplicateRecordException(String msg, Throwable throwable) { 22 | super(msg, throwable); 23 | } 24 | 25 | public DuplicateRecordException(int status, String msg) { 26 | super(status, msg); 27 | } 28 | 29 | public DuplicateRecordException(int status, Throwable throwable) { 30 | super(status, throwable); 31 | } 32 | 33 | public DuplicateRecordException(int status, String msg, Throwable throwable) { 34 | super(status, msg, throwable); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/NoPermissionException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | import com.huntly.common.api.ApiCode; 4 | 5 | /** 6 | * 登录了但无权限异常 7 | * 8 | * @author lcomplete 9 | */ 10 | public class NoPermissionException extends BaseException { 11 | private static final long serialVersionUID = 1L; 12 | 13 | public NoPermissionException() { 14 | super(ApiCode.NO_PERMISSION.getCode(), ApiCode.NO_PERMISSION.getMessage()); 15 | } 16 | 17 | public NoPermissionException(Throwable throwable) { 18 | super(ApiCode.NO_PERMISSION.getCode(), throwable); 19 | } 20 | 21 | public NoPermissionException(String msg, Throwable throwable) { 22 | super(ApiCode.NO_PERMISSION.getCode(), msg, throwable); 23 | } 24 | 25 | public NoPermissionException(String msg) { 26 | super(ApiCode.NO_PERMISSION.getCode(), msg); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/NoSuchDataException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | /** 4 | * 数据不存在异常 5 | * @author lcomplete 6 | */ 7 | public class NoSuchDataException extends BaseException { 8 | private static final long serialVersionUID = 1L; 9 | 10 | public NoSuchDataException() { 11 | } 12 | 13 | public NoSuchDataException(String msg) { 14 | super(msg); 15 | } 16 | 17 | public NoSuchDataException(Throwable throwable) { 18 | super(throwable); 19 | } 20 | 21 | public NoSuchDataException(String msg, Throwable throwable) { 22 | super(msg, throwable); 23 | } 24 | 25 | public NoSuchDataException(int status, String msg) { 26 | super(status, msg); 27 | } 28 | 29 | public NoSuchDataException(int status, Throwable throwable) { 30 | super(status, throwable); 31 | } 32 | 33 | public NoSuchDataException(int status, String msg, Throwable throwable) { 34 | super(status, msg, throwable); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/RequestVerifyException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | /** 4 | * 请求验证不通过异常 5 | * @author lcomplete 6 | */ 7 | public class RequestVerifyException extends BaseException { 8 | 9 | private static final long serialVersionUID = 1L; 10 | 11 | public RequestVerifyException() { 12 | } 13 | 14 | public RequestVerifyException(String msg) { 15 | super(msg); 16 | } 17 | 18 | public RequestVerifyException(Throwable throwable) { 19 | super(throwable); 20 | } 21 | 22 | public RequestVerifyException(String msg, Throwable throwable) { 23 | super(msg, throwable); 24 | } 25 | 26 | public RequestVerifyException(int status, String msg) { 27 | super(status, msg); 28 | } 29 | 30 | public RequestVerifyException(int status, Throwable throwable) { 31 | super(status, throwable); 32 | } 33 | 34 | public RequestVerifyException(int status, String msg, Throwable throwable) { 35 | super(status, msg, throwable); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/StatefulException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | 4 | /** 5 | * 带有状态码的异常 6 | */ 7 | public class StatefulException extends RuntimeException { 8 | private static final long serialVersionUID = 6057602589533840889L; 9 | 10 | /** 11 | * 异常状态码 12 | */ 13 | protected int status; 14 | 15 | public StatefulException() { 16 | } 17 | 18 | public StatefulException(String msg) { 19 | super(msg); 20 | } 21 | 22 | public StatefulException(Throwable throwable) { 23 | super(throwable); 24 | } 25 | 26 | public StatefulException(String msg, Throwable throwable) { 27 | super(msg, throwable); 28 | } 29 | 30 | public StatefulException(int status, String msg) { 31 | super(msg); 32 | this.status = status; 33 | } 34 | 35 | public StatefulException(int status, Throwable throwable) { 36 | super(throwable); 37 | this.status = status; 38 | } 39 | 40 | public StatefulException(int status, String msg, Throwable throwable) { 41 | super(msg, throwable); 42 | this.status = status; 43 | } 44 | 45 | /** 46 | * @return 获得异常状态码 47 | */ 48 | public int getStatus() { 49 | return status; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/exceptions/UnAuthorizedException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.exceptions; 2 | 3 | import com.huntly.common.api.ApiCode; 4 | 5 | /** 6 | * 未认证 未登陆异常 7 | * 8 | * @author lcomplete 9 | */ 10 | public class UnAuthorizedException extends BaseException { 11 | private static final long serialVersionUID = 1L; 12 | 13 | public UnAuthorizedException() { 14 | super(ApiCode.UNAUTHORIZED.getCode(), ApiCode.UNAUTHORIZED.getMessage()); 15 | } 16 | 17 | public UnAuthorizedException(Throwable throwable) { 18 | super(ApiCode.UNAUTHORIZED.getCode(), throwable); 19 | } 20 | 21 | public UnAuthorizedException(String msg, Throwable throwable) { 22 | super(ApiCode.UNAUTHORIZED.getCode(), msg, throwable); 23 | } 24 | 25 | public UnAuthorizedException(String msg) { 26 | super(ApiCode.UNAUTHORIZED.getCode(), msg); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/pagination/PageParam.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.pagination; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class PageParam implements Serializable { 9 | private static final long serialVersionUID = 1L; 10 | 11 | private long pageSize = 10; 12 | 13 | private long pageIndex = 1; 14 | 15 | private boolean isSearchCount = true; 16 | 17 | private long total; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/pagination/PagingResult.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019-2029 geekidea(https://github.com/geekidea) 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.huntly.common.pagination; 18 | 19 | import lombok.Data; 20 | 21 | import java.io.Serializable; 22 | import java.util.Collections; 23 | import java.util.List; 24 | 25 | /** 26 | * 分页结果对象 27 | */ 28 | 29 | @Data 30 | public class PagingResult implements Serializable { 31 | private static final long serialVersionUID = 4784961132604516495L; 32 | 33 | private long total = 0; 34 | 35 | private List records = Collections.emptyList(); 36 | 37 | private Long pageIndex; 38 | 39 | private Long pageSize; 40 | 41 | public PagingResult() { 42 | 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/util/Base64Utils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.apache.commons.lang3.CharSet; 5 | 6 | import java.nio.charset.Charset; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.Base64; 9 | 10 | @UtilityClass 11 | public class Base64Utils { 12 | /** 13 | * use utf-8 decode base64 string 14 | * 15 | * @param encodedString 16 | * @return 17 | */ 18 | public static String decode(String encodedString) { 19 | return decode(encodedString, StandardCharsets.UTF_8); 20 | } 21 | 22 | public static String decode(String encodedString, Charset charset) { 23 | byte[] decodedBytes = Base64.getDecoder().decode(encodedString.getBytes(charset)); 24 | return new String(decodedBytes); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/util/MapUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.util; 2 | 3 | import java.util.Map; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | public class MapUtils { 9 | /** 10 | * Map是否为空 11 | * 12 | * @param map 集合 13 | * @return 是否为空 14 | */ 15 | public static boolean isEmpty(Map map) { 16 | return null == map || map.isEmpty(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/util/MoreObjectUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | @UtilityClass 9 | public class MoreObjectUtils { 10 | public static T firstNonNull(T... items) { 11 | for (T i : items) { 12 | if (i != null) { 13 | return i; 14 | } 15 | } 16 | return null; 17 | } 18 | 19 | // below firstNonNull functions are for efficient reasons 20 | 21 | public static T firstNonNull(T a, T b) { 22 | return a == null ? b : a; 23 | } 24 | 25 | public static T firstNonNull(T a, T b, T c) { 26 | return a != null ? a : (b != null ? b : c); 27 | } 28 | 29 | public static T firstNonNull(T a, T b, T c, T d) { 30 | return a != null ? a : (b != null ? b : (c != null ? c : d)); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/util/NumberUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.apache.commons.lang3.ObjectUtils; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @UtilityClass 10 | public class NumberUtils { 11 | 12 | public static Integer safeSum(Integer a, Integer b) { 13 | return ObjectUtils.defaultIfNull(a, 0) + ObjectUtils.defaultIfNull(b, 0); 14 | } 15 | 16 | public static Integer safeSum(int a, Integer b) { 17 | return a + ObjectUtils.defaultIfNull(b, 0); 18 | } 19 | 20 | public static Integer safeSum(Integer a, int b) { 21 | return ObjectUtils.defaultIfNull(a, 0) + b; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/util/TextUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | @UtilityClass 9 | public class TextUtils { 10 | public static String trimTruncate(String str, int length) { 11 | if (str != null) { 12 | str = str.trim(); 13 | if (str.length() > length) { 14 | str = str.substring(0, length); 15 | } 16 | } 17 | return str; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/util/UrlUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | import java.net.MalformedURLException; 6 | import java.net.URI; 7 | import java.net.URL; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @UtilityClass 13 | public class UrlUtils { 14 | 15 | public static String getDomainName(String url) { 16 | try { 17 | URI uri = new URI(url); 18 | return uri.getHost(); 19 | } catch (Exception ex) { 20 | return ""; 21 | } 22 | } 23 | 24 | public static boolean isHttpUrl(String url) { 25 | URL uri = null; 26 | try { 27 | uri = new URL(url); 28 | } catch (MalformedURLException e) { 29 | return false; 30 | } 31 | var protocol = uri.getProtocol(); 32 | return "http".equals(protocol); 33 | } 34 | 35 | public static boolean isHttpsUrl(String url) { 36 | URL uri = null; 37 | try { 38 | uri = new URL(url); 39 | } catch (MalformedURLException e) { 40 | return false; 41 | } 42 | var protocol = uri.getProtocol(); 43 | return "https".equals(protocol); 44 | } 45 | 46 | public static String getHttpsUrl(String httpUrl){ 47 | return httpUrl.replaceFirst("^http:","https:"); 48 | } 49 | 50 | public static String getHttpUrl(String httpsUrl){ 51 | return httpsUrl.replaceFirst("^https:","http:"); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/server/huntly-common/src/main/java/com/huntly/common/util/XmlUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.common.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @UtilityClass 10 | public class XmlUtils { 11 | public static String removeInvalidXmlCharacters(String xml) { 12 | if (StringUtils.isBlank(xml)) { 13 | return null; 14 | } 15 | StringBuilder sb = new StringBuilder(); 16 | 17 | boolean firstTagFound = false; 18 | for (int i = 0; i < xml.length(); i++) { 19 | char c = xml.charAt(i); 20 | 21 | if (!firstTagFound) { 22 | if (c == '<') { 23 | firstTagFound = true; 24 | } else { 25 | continue; 26 | } 27 | } 28 | 29 | if (c >= 32 || c == 9 || c == 10 || c == 13) { 30 | if (!Character.isHighSurrogate(c) && !Character.isLowSurrogate(c)) { 31 | sb.append(c); 32 | } 33 | } 34 | } 35 | return sb.toString(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | integrated 7 | com.huntly 8 | ${revision} 9 | 10 | jar 11 | 4.0.0 12 | 13 | huntly-interfaces 14 | 15 | 16 | 11 17 | 11 18 | UTF-8 19 | 20 | 21 | 22 | 23 | org.springframework.boot 24 | spring-boot-starter-validation 25 | 26 | 27 | io.springfox 28 | springfox-boot-starter 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/ConnectorItem.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | @Data 9 | public class ConnectorItem { 10 | private Integer id; 11 | 12 | private String name; 13 | 14 | private Integer type; 15 | 16 | private String iconUrl; 17 | 18 | private int inboxCount; 19 | } 20 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/CursorPageResult.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.Instant; 6 | import java.util.List; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Data 12 | public class CursorPageResult { 13 | private Instant firstId; 14 | 15 | private Instant lastId; 16 | 17 | List pageItems; 18 | } 19 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/FolderConnectorView.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class FolderConnectorView { 9 | private List folderConnectors; 10 | 11 | private List folderFeedConnectors; 12 | } 13 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/FolderConnectors.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.List; 6 | 7 | @Data 8 | public class FolderConnectors { 9 | private Integer id; 10 | 11 | private String name; 12 | 13 | private List connectorItems; 14 | } 15 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/LoginUserInfo.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class LoginUserInfo { 12 | private String username; 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/PageItem.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Data; 4 | 5 | import java.time.Instant; 6 | 7 | /** 8 | * @author lcomplete 9 | */ 10 | @Data 11 | public class PageItem { 12 | 13 | private Long id; 14 | 15 | private Integer sourceId; 16 | 17 | private Integer connectorId; 18 | 19 | private Integer connectorType; 20 | 21 | private Integer folderId; 22 | 23 | private String title; 24 | 25 | private String url; 26 | 27 | private String pageUniqueId; 28 | 29 | private String pubDate; 30 | 31 | private String description; 32 | 33 | private String author; 34 | 35 | private String language; 36 | 37 | private String category; 38 | 39 | private Integer readCount; 40 | 41 | /** 42 | * not row create time, but the operation time. 43 | */ 44 | private Instant recordAt; 45 | 46 | private Instant connectedAt; 47 | 48 | private Integer librarySaveStatus; 49 | 50 | private Boolean starred; 51 | 52 | private Boolean readLater; 53 | 54 | private Boolean markRead; 55 | 56 | private String thumbUrl; 57 | 58 | private Integer contentType; 59 | 60 | private String pageJsonProperties; 61 | 62 | //region source 63 | 64 | private String siteName; 65 | 66 | private String domain; 67 | 68 | private String faviconUrl; 69 | 70 | private Long voteScore; 71 | 72 | //endregion 73 | 74 | } 75 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/PageOperateResult.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | @Data 9 | public class PageOperateResult { 10 | private Long id; 11 | 12 | private Integer librarySaveStatus; 13 | 14 | private Boolean starred; 15 | 16 | private Boolean readLater; 17 | } 18 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/PageSearchResult.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Setter 12 | @Getter 13 | public class PageSearchResult { 14 | private List items; 15 | 16 | private double costSeconds; 17 | 18 | private long totalHits; 19 | 20 | private Integer page; 21 | } 22 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/dto/PreviewFeedsInfo.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.dto; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class PreviewFeedsInfo { 12 | private String title; 13 | 14 | private String description; 15 | 16 | private String siteLink; 17 | 18 | private String feedUrl; 19 | 20 | private String siteFaviconUrl; 21 | 22 | private Boolean subscribed; 23 | } 24 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/ArticleContent.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | /** 8 | * @author lcomplete 9 | */ 10 | @Getter 11 | @Setter 12 | @AllArgsConstructor 13 | public class ArticleContent { 14 | private Long pageId; 15 | 16 | private String content; 17 | } 18 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/CaptureFromType.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | /** 4 | * @author lcomplete 5 | */ 6 | 7 | public enum CaptureFromType { 8 | BROWSER(0), 9 | CONNECTOR(1), 10 | FEED(2); 11 | 12 | private final int code; 13 | 14 | public int getCode(){ 15 | return code; 16 | } 17 | 18 | CaptureFromType(int code) { 19 | this.code = code; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/CapturePage.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Data; 4 | 5 | import javax.validation.constraints.NotBlank; 6 | import java.time.Instant; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Data 12 | public class CapturePage { 13 | private Integer id; 14 | 15 | private String title; 16 | 17 | private String content; 18 | 19 | @NotBlank 20 | private String url; 21 | 22 | /** 23 | * base url for page link img url, if null then use url 24 | */ 25 | private String baseUrl; 26 | 27 | private String thumbUrl; 28 | 29 | private Boolean needFindThumbUrl; 30 | 31 | private String description; 32 | 33 | private String author; 34 | 35 | private String language; 36 | 37 | private String category; 38 | 39 | private Boolean isLiked; 40 | private Boolean isFavorite; 41 | 42 | @NotBlank 43 | private String domain; 44 | 45 | private String siteName; 46 | 47 | private String faviconUrl; 48 | 49 | private String homeUrl; 50 | 51 | private String subscribeUrl; 52 | 53 | //private CaptureFromType captureFrom = CaptureFromType.BROWSER; 54 | 55 | /** 56 | * null or 0 means from browser 57 | */ 58 | private Integer connectorId; 59 | 60 | private Instant connectedAt; 61 | 62 | private String pageJsonProperties; 63 | } 64 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/ContentType.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | /** 4 | * page content type 5 | * @author louch 6 | */ 7 | public enum ContentType { 8 | BROWSER_HISTORY(0), 9 | TWEET(1), 10 | MARKDOWN(2), 11 | /* 12 | * is in quoted tweet 13 | */ 14 | QUOTED_TWEET(3); 15 | 16 | private final int code; 17 | 18 | public int getCode() { 19 | return code; 20 | } 21 | 22 | ContentType(int code) { 23 | this.code = code; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/FeedsSetting.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class FeedsSetting { 12 | private Integer connectorId; 13 | 14 | private Integer folderId; 15 | 16 | private String name; 17 | 18 | private Boolean crawlFullContent; 19 | 20 | private String subscribeUrl; 21 | 22 | private Integer fetchIntervalMinutes; 23 | 24 | private Boolean enabled; 25 | } 26 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/GitHubSetting.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class GitHubSetting { 12 | private int connectorId; 13 | 14 | private String apiToken; 15 | 16 | private boolean isTokenSet; 17 | 18 | private String name; 19 | 20 | private Integer fetchIntervalMinutes; 21 | 22 | private Integer fetchPageSize; 23 | 24 | private Boolean enabled; 25 | } 26 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/GithubRepoProperties.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import java.time.Instant; 7 | import java.util.List; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Getter 13 | @Setter 14 | public class GithubRepoProperties { 15 | private String nodeId; 16 | 17 | private String name; 18 | 19 | private String defaultBranch; 20 | 21 | private Integer stargazersCount; 22 | 23 | private Integer forksCount; 24 | 25 | private Integer watchersCount; 26 | 27 | private String homepage; 28 | 29 | private List topics; 30 | 31 | private Instant updatedAt; 32 | } 33 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/InterceptTweets.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class InterceptTweets { 12 | 13 | private String category; 14 | 15 | private String jsonData; 16 | 17 | private String loginScreenName; 18 | 19 | private String browserScreenName; 20 | } 21 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/LibrarySaveStatus.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | /** 4 | * @author lcomplete 5 | * library 保存状态(保存和存档到 library 中的数据将不会被自动删除) 6 | */ 7 | 8 | public enum LibrarySaveStatus { 9 | /** 10 | * 未保存(自动记录到数据库中的记录) 11 | */ 12 | NOT_SAVED(0), 13 | 14 | /** 15 | * 已保存 16 | */ 17 | SAVED(1), 18 | 19 | /** 20 | * 已存档 21 | */ 22 | ARCHIVED(2); 23 | 24 | private final int code; 25 | 26 | public int getCode() { 27 | return code; 28 | } 29 | 30 | LibrarySaveStatus(int code) { 31 | this.code = code; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/LibrarySaveType.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | /** 4 | * @author lcomplete 5 | */ 6 | 7 | public enum LibrarySaveType { 8 | NONE(0), 9 | MY_LIST(1), 10 | STARRED(2), 11 | READ_LATER(3), 12 | ARCHIVE(4); 13 | 14 | private final int code; 15 | 16 | LibrarySaveType(int code) { 17 | this.code = code; 18 | } 19 | 20 | private int getCode() { 21 | return code; 22 | } 23 | 24 | public static LibrarySaveType fromCode(Integer code) { 25 | if (code == null) { 26 | return null; 27 | } 28 | var types = LibrarySaveType.class.getEnumConstants(); 29 | for (var saveType : types) { 30 | if (code.equals(saveType.getCode())) { 31 | return saveType; 32 | } 33 | } 34 | return null; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/LoginRequest.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | import javax.validation.constraints.NotBlank; 7 | 8 | @Getter 9 | @Setter 10 | public class LoginRequest { 11 | @NotBlank 12 | private String username; 13 | 14 | @NotBlank 15 | private String password; 16 | } 17 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/SearchOption.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class SearchOption { 12 | private Type type; 13 | 14 | private Library library; 15 | 16 | private Boolean alreadyRead; 17 | 18 | private Boolean onlySearchTitle; 19 | 20 | public enum Type { 21 | TWEET, 22 | GITHUB_STARRED_REPO, 23 | BROWSER_HISTORY, 24 | FEEDS 25 | } 26 | 27 | public enum Library{ 28 | MY_LIST, 29 | STARRED, 30 | READ_LATER, 31 | ARCHIVE 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/model/TweetId.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class TweetId { 12 | private String id; 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/PageListQuery.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.query; 2 | 3 | import com.huntly.interfaces.external.model.ContentType; 4 | import com.huntly.interfaces.external.model.LibrarySaveStatus; 5 | import lombok.Data; 6 | 7 | import java.time.Instant; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Data 13 | public class PageListQuery { 14 | 15 | private int sourceId; 16 | 17 | private int connectorId; 18 | 19 | private Instant firstRecordAt; 20 | 21 | private Instant lastRecordAt; 22 | 23 | private Long firstVoteScore; 24 | 25 | private Long lastVoteScore; 26 | 27 | private int count; 28 | 29 | private Boolean starred; 30 | 31 | private Boolean readLater; 32 | 33 | private Boolean markRead; 34 | 35 | private LibrarySaveStatus saveStatus; 36 | 37 | private PageListSort sort; 38 | 39 | private boolean isAsc; 40 | 41 | private Integer connectorType; 42 | 43 | private ContentType contentType; 44 | 45 | /** 46 | * 0: all 47 | * 1: article 48 | * 2: tweet 49 | */ 50 | private Integer contentFilterType; 51 | 52 | private int folderId; 53 | 54 | private String startDate; 55 | 56 | private String endDate; 57 | 58 | } 59 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/PageListSort.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.query; 2 | 3 | /** 4 | * @author lcomplete 5 | */ 6 | 7 | public enum PageListSort { 8 | CREATED_AT("createdAt"), 9 | ARCHIVED_AT("archivedAt"), 10 | 11 | LAST_READ_AT("lastReadAt"), 12 | 13 | READ_LATER_AT("readLaterAt"), 14 | 15 | SAVED_AT("savedAt"), 16 | 17 | STARRED_AT("starredAt"), 18 | 19 | CONNECTED_AT("connectedAt"), 20 | 21 | VOTE_SCORE("voteScore"); 22 | 23 | //ID("id"); 24 | 25 | private String sortField; 26 | 27 | PageListSort(String sortField) { 28 | this.sortField = sortField; 29 | } 30 | 31 | public String getSortField() { 32 | return sortField; 33 | } 34 | 35 | static PageListSort valueOfSort(String sort) { 36 | if (sort == null) { 37 | return null; 38 | } 39 | var sorts = PageListSort.class.getEnumConstants(); 40 | for (var enumSort : sorts) { 41 | if (sort.equals(enumSort.getSortField())) { 42 | return enumSort; 43 | } 44 | } 45 | return null; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/PageQuery.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.query; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class PageQuery { 12 | private Long id; 13 | 14 | private String url; 15 | } 16 | -------------------------------------------------------------------------------- /app/server/huntly-interfaces/src/main/java/com/huntly/interfaces/external/query/SearchQuery.java: -------------------------------------------------------------------------------- 1 | package com.huntly.interfaces.external.query; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Setter 10 | @Getter 11 | public class SearchQuery { 12 | /** 13 | * keyword 14 | */ 15 | private String q; 16 | 17 | private String queryOptions; 18 | 19 | private Integer page; 20 | 21 | private Integer size; 22 | 23 | //private Integer connectorId; 24 | 25 | //private Instant readAfterAt; 26 | 27 | //private Instant readBeforeAt; 28 | } 29 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | integrated 7 | com.huntly 8 | ${revision} 9 | 10 | 4.0.0 11 | 12 | huntly-jpa 13 | 14 | 15 | 11 16 | 11 17 | UTF-8 18 | 19 | 20 | 21 | 22 | org.springframework.boot 23 | spring-boot-starter-data-jpa 24 | 25 | 26 | 27 | 28 | com.h2database 29 | h2 30 | test 31 | 32 | 33 | org.springframework.boot 34 | spring-boot-starter-test 35 | test 36 | 37 | 38 | org.apache.commons 39 | commons-lang3 40 | test 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/OrderByInfo.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query; 2 | 3 | 4 | import com.huntly.jpa.query.annotation.OrderBy; 5 | 6 | import java.io.Serializable; 7 | 8 | class OrderByInfo implements Comparable, Serializable { 9 | 10 | private int priority; 11 | private String path; 12 | private OrderBy.OrderType type; 13 | 14 | public OrderByInfo(int priority, String path, OrderBy.OrderType type) { 15 | this.priority = priority; 16 | this.path = path; 17 | this.type = type; 18 | } 19 | 20 | @Override 21 | public int compareTo(OrderByInfo o) { 22 | return priority - o.priority; 23 | } 24 | 25 | public int getPriority() { 26 | return priority; 27 | } 28 | 29 | public void setPriority(int priority) { 30 | this.priority = priority; 31 | } 32 | 33 | public String getPath() { 34 | return path; 35 | } 36 | 37 | public void setPath(String path) { 38 | this.path = path; 39 | } 40 | 41 | public OrderBy.OrderType getType() { 42 | return type; 43 | } 44 | 45 | public void setType(OrderBy.OrderType type) { 46 | this.type = type; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/QueryCriteria.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query; 2 | 3 | import org.springframework.data.jpa.domain.Specification; 4 | 5 | public interface QueryCriteria { 6 | default Specification toSpecification(){ 7 | return SpecificationUtils.fromQueryCriteria(this); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/Keywords.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target(ElementType.FIELD) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | public @interface Keywords { 12 | 13 | /** 14 | * 需要查询的字段名,在Specification中称为path(路径) 15 | * @return 16 | */ 17 | String[] value() default {""}; 18 | 19 | /** 20 | * @see QueryGroup 21 | * 该查询的名称,方便我们后面QueryGroup做分组查询 22 | * @return 23 | */ 24 | String name() default ""; 25 | 26 | /** 27 | * 模糊查询的前缀,默认为 % 28 | * @return 29 | */ 30 | String prefix() default "%"; 31 | 32 | /** 33 | * 模糊查询的后缀,默认为 % 34 | * @return 35 | */ 36 | String suffix() default "%"; 37 | 38 | /** 39 | * @see Operator 40 | * 模糊查询连接条件 分为 AND 和 OR 41 | * @return 42 | */ 43 | Operator operator() default Operator.OR; 44 | } 45 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/Operator.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | import javax.persistence.criteria.CriteriaBuilder; 4 | import javax.persistence.criteria.Predicate; 5 | import java.util.Collection; 6 | 7 | public enum Operator { 8 | OR { 9 | @Override 10 | public Predicate operation(CriteriaBuilder criteriaBuilder, Collection predicates) { 11 | return criteriaBuilder.or(predicates.toArray(PREDICATES)); 12 | } 13 | }, AND { 14 | @Override 15 | public Predicate operation(CriteriaBuilder criteriaBuilder, Collection predicates) { 16 | return criteriaBuilder.and(predicates.toArray(PREDICATES)); 17 | } 18 | }; 19 | 20 | private static final Predicate[] PREDICATES = new Predicate[0]; 21 | 22 | public abstract Predicate operation(CriteriaBuilder criteriaBuilder, Collection predicates); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/OrderBy.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target({ElementType.FIELD, ElementType.TYPE}) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface OrderBy { 11 | 12 | int priority() default 10; 13 | 14 | String path() default ""; 15 | 16 | OrderType type() default OrderType.ASC; 17 | 18 | enum OrderType { 19 | ASC, DESC 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/Queries.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Retention(RetentionPolicy.RUNTIME) 9 | @Target(ElementType.FIELD) 10 | public @interface Queries { 11 | Query[] value() default {}; 12 | } 13 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/Query.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.FIELD) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Repeatable(value = Queries.class) 8 | public @interface Query { 9 | 10 | /** 11 | * @see QueryType 12 | * 查询类型 13 | * NOT_EQUAL 不相等 14 | * EQUAL 相等 15 | * GREATER_THAN 大于 16 | * GREATER_THAN_OR_EQUAL 大于等于 17 | * LESS_THAN 小于 18 | * LESS_THAN_OR_EQUAL 小于等于 19 | * IS_NULL 等于null 20 | * IS_NOT_NULL 不等于null 21 | * IN 在集合或数组内 必须注解在 array 或者 Collection上 22 | * BETWEEN 在范围内 必须注解在 array 或者 List 上,并且元素要大于或等于2 23 | * @return 24 | */ 25 | 26 | QueryType type() default QueryType.EQUAL; 27 | 28 | /** 29 | * @see QueryGroup 30 | * 该查询名称,方便我们后面QueryGroup做分组查询 31 | * @return 默认使用属性名 32 | */ 33 | String name() default ""; 34 | 35 | /** 36 | * 查询字段 path 37 | * @return 优先使用配置的路径,然后使用名称,最后使用属性名 38 | */ 39 | String path() default ""; 40 | } 41 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/QueryGroup.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | import java.lang.annotation.*; 4 | 5 | @Target(ElementType.TYPE) 6 | @Retention(RetentionPolicy.RUNTIME) 7 | @Repeatable(QueryGroups.class) 8 | public @interface QueryGroup { 9 | 10 | /** 11 | * 查询名称 可以把几个查询合并为一个查询,然后在用于其他的组 12 | * @return 13 | */ 14 | String name(); 15 | 16 | /** 17 | * 查询的目标名称 18 | * @return 19 | */ 20 | String[] targetNames() default {}; 21 | 22 | /** 23 | * 分组查询的条件 OR 或 AND 24 | * @return 25 | */ 26 | Operator operator() default Operator.AND; 27 | 28 | } 29 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/QueryGroups.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Target(ElementType.TYPE) 11 | public @interface QueryGroups { 12 | 13 | QueryGroup[] value() default {}; 14 | } 15 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/query/annotation/QueryRoot.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.query.annotation; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | @Target(ElementType.TYPE) 9 | @Retention(RetentionPolicy.RUNTIME) 10 | public @interface QueryRoot { 11 | Operator value() default Operator.AND; 12 | } 13 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/repository/JpaRepositoryWithLimit.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.repository; 2 | 3 | import org.springframework.data.domain.Sort; 4 | import org.springframework.data.jpa.domain.Specification; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.repository.NoRepositoryBean; 7 | 8 | import java.io.Serializable; 9 | import java.util.List; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @NoRepositoryBean 15 | public interface JpaRepositoryWithLimit extends JpaRepository { 16 | 17 | List findAll(Specification spec, int offset, int maxResults, Sort sort); 18 | 19 | List findAll(Specification spec, int offset, int maxResults); 20 | 21 | List findAll(Specification spec, int maxResults, Sort sort); 22 | 23 | List findAll(Specification spec, int maxResults); 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/main/java/com/huntly/jpa/repository/JpaSpecificationExecutorWithProjection.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.repository; 2 | 3 | import org.springframework.data.domain.Page; 4 | import org.springframework.data.domain.Pageable; 5 | import org.springframework.data.jpa.domain.Specification; 6 | import org.springframework.data.jpa.repository.EntityGraph; 7 | import org.springframework.data.jpa.repository.query.JpaEntityGraph; 8 | import org.springframework.data.repository.NoRepositoryBean; 9 | 10 | import java.util.Optional; 11 | 12 | /** 13 | * Created by pramoth on 9/29/2016 AD. 14 | */ 15 | @NoRepositoryBean 16 | public interface JpaSpecificationExecutorWithProjection { 17 | 18 | Optional findOne(Specification spec, Class projectionClass); 19 | 20 | Page findAll(Specification spec, Class projectionClass, Pageable pageable); 21 | 22 | /** 23 | * Use Spring Data Annotation instead of manually provide EntityGraph. 24 | * @param spec 25 | * @param projectionType 26 | * @param namedEntityGraph 27 | * @param type 28 | * @param pageable 29 | * @param 30 | * @return 31 | */ 32 | @Deprecated 33 | Page findAll(Specification spec, Class projectionType, String namedEntityGraph, EntityGraph.EntityGraphType type, Pageable pageable); 34 | 35 | /** 36 | * Use Spring Data Annotation instead of manually provide EntityGraph. 37 | * @param spec 38 | * @param projectionClass 39 | * @param dynamicEntityGraph 40 | * @param pageable 41 | * @param 42 | * @return 43 | */ 44 | @Deprecated 45 | Page findAll(Specification spec, Class projectionClass, JpaEntityGraph dynamicEntityGraph, Pageable pageable); 46 | } 47 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/test/java/com/huntly/jpa/integration/query/model/PersonQuery.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.integration.query.model; 2 | 3 | import com.huntly.jpa.model.Person; 4 | import com.huntly.jpa.query.QueryCriteria; 5 | import com.huntly.jpa.query.annotation.Query; 6 | import com.huntly.jpa.query.annotation.QueryType; 7 | import lombok.Data; 8 | 9 | @Data 10 | public class PersonQuery implements QueryCriteria { 11 | 12 | @Query(type = QueryType.GREATER_THAN_OR_EQUAL) 13 | private Integer age; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/test/java/com/huntly/jpa/model/PersonInfo.java: -------------------------------------------------------------------------------- 1 | package com.huntly.jpa.model; 2 | 3 | public interface PersonInfo { 4 | public String getName(); 5 | } 6 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/test/java/com/huntly/jpa/repository/PersonIdCardRepository.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2019, Wen Hao . 3 | *

4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | *

11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | *

14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | package com.huntly.jpa.repository; 23 | 24 | import com.huntly.jpa.model.PersonIdCard; 25 | import org.springframework.data.jpa.repository.JpaRepository; 26 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 27 | 28 | public interface PersonIdCardRepository extends JpaRepository, JpaSpecificationExecutor { 29 | } 30 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/test/java/com/huntly/jpa/repository/PersonRepository.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2019, Wen Hao . 3 | *

4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | *

11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | *

14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | package com.huntly.jpa.repository; 23 | 24 | import com.huntly.jpa.model.Person; 25 | import org.springframework.data.jpa.repository.JpaRepository; 26 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 27 | 28 | public interface PersonRepository extends JpaRepository, JpaSpecificationExecutor, JpaSpecificationExecutorWithProjection { 29 | } 30 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/test/java/com/huntly/jpa/repository/PhoneRepository.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright © 2019, Wen Hao . 3 | *

4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | *

11 | * The above copyright notice and this permission notice shall be included in all 12 | * copies or substantial portions of the Software. 13 | *

14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | * SOFTWARE. 21 | */ 22 | package com.huntly.jpa.repository; 23 | 24 | import com.huntly.jpa.model.Phone; 25 | import org.springframework.data.jpa.repository.JpaRepository; 26 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 27 | 28 | public interface PhoneRepository extends JpaRepository, JpaSpecificationExecutor { 29 | } 30 | -------------------------------------------------------------------------------- /app/server/huntly-jpa/src/test/resources/application.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright © 2019, Wen Hao . 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a copy 5 | # of this software and associated documentation files (the "Software"), to deal 6 | # in the Software without restriction, including without limitation the rights 7 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | # copies of the Software, and to permit persons to whom the Software is 9 | # furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in all 12 | # copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 20 | # SOFTWARE. 21 | # 22 | 23 | spring.datasource.url=jdbc:h2:mem:jpa-spec;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 24 | spring.datasource.driverClassName=org.h2.Driver 25 | spring.datasource.username=sa 26 | spring.datasource.password= 27 | spring.jpa.database-platform=org.hibernate.dialect.H2Dialect 28 | 29 | spring.jpa.generate-ddl = true 30 | spring.h2.console.enabled = true 31 | spring.h2.console.settings.web-allow-others = true 32 | spring.jpa.hibernate.ddl-auto = none 33 | spring.jpa.show-sql = true 34 | spring.jpa.properties.hibernate.format_sql = true -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/HuntlyServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.scheduling.annotation.EnableAsync; 6 | import org.springframework.scheduling.annotation.EnableScheduling; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @SpringBootApplication 12 | @EnableScheduling 13 | @EnableAsync 14 | public class HuntlyServerApplication { 15 | public static void main(String[] args){ 16 | SpringApplication.run(HuntlyServerApplication.class,args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/cache/CacheService.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.cache; 2 | 3 | import com.huntly.server.domain.entity.Connector; 4 | import com.huntly.server.domain.entity.Source; 5 | import com.huntly.server.repository.ConnectorRepository; 6 | import com.huntly.server.repository.SourceRepository; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.Optional; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @Service 15 | public class CacheService { 16 | 17 | private final ConnectorRepository connectorRepository; 18 | 19 | private final SourceRepository sourceRepository; 20 | 21 | public CacheService(ConnectorRepository connectorRepository, SourceRepository sourceRepository) { 22 | this.connectorRepository = connectorRepository; 23 | this.sourceRepository = sourceRepository; 24 | } 25 | 26 | // todo add cache 27 | public Optional getConnector(Integer id) { 28 | return connectorRepository.findById(id); 29 | } 30 | 31 | // todo add Cacheable 32 | public Optional getSource(Integer id) { 33 | var source = sourceRepository.findById(id); 34 | return source; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/config/DocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.config; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import springfox.documentation.builders.ApiInfoBuilder; 6 | import springfox.documentation.service.ApiInfo; 7 | import springfox.documentation.spi.DocumentationType; 8 | import springfox.documentation.spring.web.plugins.Docket; 9 | 10 | @Configuration 11 | public class DocketConfig { 12 | @Bean 13 | public Docket docket() { 14 | Docket docket = new Docket(DocumentationType.OAS_30) 15 | .forCodeGeneration(true) 16 | .apiInfo(apiInfo()); 17 | return docket; 18 | } 19 | 20 | private ApiInfo apiInfo(){ 21 | return new ApiInfoBuilder() 22 | .title("huntly api doc") 23 | .version("3.0") 24 | .description("huntly api doc for code generation") 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/config/HuntlyProperties.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.config; 2 | 3 | import lombok.Data; 4 | import org.springframework.boot.context.properties.ConfigurationProperties; 5 | import org.springframework.stereotype.Component; 6 | 7 | /** 8 | * @author lcomplete 9 | */ 10 | @Data 11 | @ConfigurationProperties(prefix = "huntly") 12 | @Component 13 | public class HuntlyProperties { 14 | private String jwtSecret; 15 | 16 | private int jwtExpirationDays; 17 | 18 | private boolean enableFetchThreadPool = true; 19 | 20 | private Integer connectorFetchCorePoolSize; 21 | 22 | private Integer connectorFetchMaxPoolSize; 23 | 24 | private Integer defaultFeedFetchIntervalSeconds = 600; 25 | 26 | private String luceneDir; 27 | 28 | private String dataDir; 29 | } 30 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/config/ServiceExecutorConfig.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.scheduling.annotation.AsyncConfigurer; 5 | import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; 6 | 7 | import java.util.concurrent.Executor; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Configuration 13 | public class ServiceExecutorConfig implements AsyncConfigurer { 14 | 15 | @Override 16 | public Executor getAsyncExecutor() { 17 | ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); 18 | taskExecutor.setCorePoolSize(2); 19 | taskExecutor.setMaxPoolSize(10); 20 | taskExecutor.setQueueCapacity(10000); 21 | taskExecutor.initialize(); 22 | return taskExecutor; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/config/WebConfig.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.config; 2 | 3 | import com.huntly.jpa.repository.support.CustomJpaRepository; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 6 | 7 | /** 8 | * @author lcomplete 9 | */ 10 | @Configuration 11 | @EnableJpaRepositories(repositoryBaseClass = CustomJpaRepository.class, basePackages = {"com.huntly.server.repository"}) 12 | public class WebConfig { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/connector/ConnectorProperties.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.connector; 2 | 3 | import com.huntly.server.domain.model.ProxySetting; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import java.time.Instant; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Getter 13 | @Setter 14 | public class ConnectorProperties { 15 | private Instant lastFetchAt; 16 | 17 | private String subscribeUrl; 18 | 19 | private String apiToken; 20 | 21 | private Boolean crawlFullContent; 22 | 23 | private ProxySetting proxySetting; 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/connector/ConnectorType.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.connector; 2 | 3 | import com.huntly.common.enums.BaseEnum; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | 9 | public enum ConnectorType implements BaseEnum { 10 | RSS(1), 11 | GITHUB(2); 12 | 13 | private final Integer code; 14 | 15 | public Integer getCode() { 16 | return code; 17 | } 18 | 19 | @Override 20 | public String getDesc() { 21 | return null; 22 | } 23 | 24 | ConnectorType(Integer code) { 25 | this.code = code; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/connector/InfoConnector.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.connector; 2 | 3 | import com.huntly.interfaces.external.model.CapturePage; 4 | import com.huntly.server.util.HttpUtils; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import java.net.InetSocketAddress; 8 | import java.net.ProxySelector; 9 | import java.net.http.HttpClient; 10 | import java.time.Duration; 11 | import java.util.List; 12 | 13 | /** 14 | * @author lcomplete 15 | */ 16 | public abstract class InfoConnector { 17 | 18 | protected HttpClient buildHttpClient(ConnectorProperties properties) { 19 | return HttpUtils.buildHttpClient(properties.getProxySetting()); 20 | } 21 | 22 | public abstract List fetchNewestPages(); 23 | 24 | public abstract List fetchAllPages(); 25 | 26 | public abstract CapturePage fetchPageContent(CapturePage capturePage); 27 | } 28 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/connector/InfoConnectorFactory.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.connector; 2 | 3 | import com.huntly.common.enums.BaseEnum; 4 | import com.huntly.server.connector.github.GithubConnector; 5 | import com.huntly.server.connector.rss.RSSConnector; 6 | import lombok.experimental.UtilityClass; 7 | import org.apache.commons.lang3.NotImplementedException; 8 | import org.apache.commons.lang3.StringUtils; 9 | 10 | /** 11 | * @author lcomplete 12 | */ 13 | @UtilityClass 14 | public class InfoConnectorFactory { 15 | public static InfoConnector createInfoConnector(Integer connectorType, ConnectorProperties connectorProperties) { 16 | ConnectorType type = BaseEnum.valueOf(ConnectorType.class, connectorType); 17 | if (type == null) { 18 | return null; 19 | } 20 | if (ConnectorType.RSS.equals(type)) { 21 | return new RSSConnector(connectorProperties); 22 | } 23 | if (ConnectorType.GITHUB.equals(type) && StringUtils.isNotBlank(connectorProperties.getApiToken())) { 24 | return new GithubConnector(connectorProperties); 25 | } 26 | throw new NotImplementedException("connector type not implemented"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/controller/ConnectorController.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.controller; 2 | 3 | import com.huntly.interfaces.external.dto.FolderConnectorView; 4 | import com.huntly.server.domain.entity.Connector; 5 | import com.huntly.server.service.ConnectorService; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.validation.Valid; 9 | import javax.validation.constraints.NotNull; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @RestController 15 | @RequestMapping("/api/connector") 16 | public class ConnectorController { 17 | 18 | private final ConnectorService connectorService; 19 | 20 | public ConnectorController(ConnectorService connectorService) { 21 | this.connectorService = connectorService; 22 | } 23 | 24 | @GetMapping("folder-connectors") 25 | public FolderConnectorView getFolderConnectorView() { 26 | return connectorService.getFolderConnectorView(true); 27 | } 28 | 29 | @GetMapping("/{id}") 30 | public Connector getConnectorById(@Valid @NotNull @PathVariable("id") Integer id) { 31 | return connectorService.findById(id); 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/controller/FolderController.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.controller; 2 | 3 | import com.huntly.server.domain.entity.Folder; 4 | import com.huntly.server.service.FolderService; 5 | import org.springframework.validation.annotation.Validated; 6 | import org.springframework.web.bind.annotation.*; 7 | 8 | import javax.validation.Valid; 9 | import javax.validation.constraints.NotNull; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @Validated 15 | @RestController 16 | @RequestMapping("/api/folder") 17 | public class FolderController { 18 | 19 | private final FolderService folderService; 20 | 21 | public FolderController(FolderService folderService) { 22 | this.folderService = folderService; 23 | } 24 | 25 | @GetMapping("/{id}") 26 | public Folder getFolderById(@Valid @NotNull @PathVariable("id") Integer id) { 27 | return folderService.findById(id); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/controller/HealthController.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.controller; 2 | 3 | import org.springframework.web.bind.annotation.GetMapping; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | 7 | /** 8 | * @author lcomplete 9 | */ 10 | @RestController 11 | @RequestMapping("/api/health") 12 | public class HealthController { 13 | @GetMapping 14 | public String health() { 15 | return "OK"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/controller/ReactAppController.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import springfox.documentation.annotations.ApiIgnore; 6 | 7 | /** 8 | * @author lcomplete 9 | */ 10 | @Controller 11 | @ApiIgnore 12 | public class ReactAppController { 13 | @RequestMapping(value = {"/", "/{x:[\\w\\-]+}", "/{x:^(?!api$).*$}/**/{y:[\\w\\-]+}"}) 14 | public String getIndex() { 15 | return "/index.html"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/controller/SearchController.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.controller; 2 | 3 | import com.huntly.interfaces.external.dto.PageSearchResult; 4 | import com.huntly.interfaces.external.query.SearchQuery; 5 | import com.huntly.server.domain.entity.SearchHistory; 6 | import com.huntly.server.service.LuceneService; 7 | import com.huntly.server.service.SearchHistoryService; 8 | import com.huntly.server.service.TweetTrackService; 9 | import org.springframework.web.bind.annotation.*; 10 | 11 | import java.util.List; 12 | 13 | /** 14 | * @author lcomplete 15 | */ 16 | @RestController 17 | @RequestMapping("/api/search") 18 | public class SearchController { 19 | 20 | private final LuceneService luceneService; 21 | 22 | private final SearchHistoryService searchHistoryService; 23 | 24 | public SearchController(LuceneService luceneService, SearchHistoryService searchHistoryService) { 25 | this.luceneService = luceneService; 26 | this.searchHistoryService = searchHistoryService; 27 | } 28 | 29 | @PostMapping 30 | public PageSearchResult searchPages(@RequestBody SearchQuery searchQuery) { 31 | searchHistoryService.save(searchQuery.getQ(), searchQuery.getQueryOptions()); 32 | return luceneService.searchPages(searchQuery); 33 | } 34 | 35 | @GetMapping("/recent") 36 | public List getRecentSearches() { 37 | return searchHistoryService.getRecentSearches(5); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/data/dialect/identity/SQLiteDialectIdentityColumnSupport.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.data.dialect.identity; 2 | 3 | import org.hibernate.dialect.identity.IdentityColumnSupportImpl; 4 | 5 | public class SQLiteDialectIdentityColumnSupport extends IdentityColumnSupportImpl { 6 | @Override 7 | public boolean supportsIdentityColumns() { 8 | return true; 9 | } 10 | 11 | @Override 12 | public boolean supportsInsertSelectIdentity() { 13 | return true; // https://sqlite.org/lang_returning.html 14 | } 15 | 16 | @Override 17 | public boolean hasDataTypeInIdentityColumn() { 18 | // As specified in NHibernate dialect 19 | // FIXME true 20 | return false; 21 | } 22 | 23 | @Override 24 | public String appendIdentitySelectToInsert(String insertString) { 25 | return insertString + " RETURNING rowid"; 26 | } 27 | 28 | @Override 29 | public String getIdentitySelectString(String table, String column, int type) { 30 | return "select last_insert_rowid()"; 31 | } 32 | 33 | @Override 34 | public String getIdentityColumnString(int type) { 35 | // return "integer primary key autoincrement"; 36 | // FIXME "autoincrement" 37 | return "integer"; 38 | } 39 | } -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/constant/AppConstants.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.constant; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | @UtilityClass 9 | public class AppConstants { 10 | public static final String DEFAULT_LUCENE_DIR = "lucene"; 11 | 12 | public static final String HTTP_FEED_CACHE_DIR = "feed_cache"; 13 | 14 | public static final Long HTTP_FEED_CACHE_MAXSIZE = 50L * 1024L * 1024L; // 50 MB 15 | 16 | public static final Integer DEFAULT_COLD_DATA_KEEP_DAYS = 60; 17 | 18 | public static final String AUTH_TOKEN_COOKIE_NAME = "auth_token"; 19 | 20 | public static final Integer DEFAULT_CONNECTOR_FETCH_CORE_POOL_SIZE = 3; 21 | public static final Integer DEFAULT_CONNECTOR_FETCH_MAX_POOL_SIZE = 30; 22 | 23 | public static final Integer GITHUB_DEFAULT_FETCH_PAGE_SIZE = 20; 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/constant/DocFields.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.constant; 2 | 3 | import lombok.experimental.UtilityClass; 4 | 5 | /** 6 | * lucene page doc fields 7 | * @author lcomplete 8 | */ 9 | @UtilityClass 10 | public class DocFields { 11 | public static final String ID = "id"; 12 | public static final String TITLE = "title"; 13 | public static final String CONTENT = "content"; 14 | public static final String DESCRIPTION = "description"; 15 | public static final String SOURCE_ID = "sourceId"; 16 | public static final String CONNECTOR_ID = "connectorId"; 17 | public static final String CONNECTOR_TYPE = "connectorType"; 18 | public static final String FOLDER_ID = "folderId"; 19 | public static final String CREATED_AT = "createdAt"; 20 | public static final String LAST_READ_AT = "lastReadAt"; 21 | public static final String LIBRARY_SAVE_STATUS = "librarySaveStatus"; 22 | public static final String STARRED = "starred"; 23 | public static final String READ_LATER = "readLater"; 24 | @Deprecated 25 | public static final String URL = "url"; 26 | public static final String URL_TEXT = "url_text"; 27 | public static final String THUMB_URL = "thumbUrl"; 28 | public static final String PAGE_JSON_PROPERTIES= "pageJsonProperties"; 29 | public static final String CONTENT_TYPE = "contentType"; 30 | public static final String AUTHOR = "author_text"; 31 | } 32 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/ConnectorSetting.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Entity 12 | @Data 13 | @Table(name = "connector_setting") 14 | public class ConnectorSetting implements Serializable { 15 | 16 | private static final long serialVersionUID = 1L; 17 | 18 | @Id 19 | @Column(name = "id") 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Integer id; 22 | 23 | @Column(name = "connector_id") 24 | private Integer connectorId; 25 | 26 | @Column(name = "setting_key") 27 | private String settingKey; 28 | 29 | @Column(name = "setting_value") 30 | private String settingValue; 31 | 32 | } 33 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/Folder.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | @Data 12 | @Entity 13 | @Table(name = "folder") 14 | public class Folder implements Serializable { 15 | 16 | private static final long serialVersionUID = 1L; 17 | 18 | @Id 19 | @Column(name = "id") 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Integer id; 22 | 23 | @Column(name = "name") 24 | private String name; 25 | 26 | @Column(name = "display_sequence") 27 | private Integer displaySequence; 28 | 29 | @Column(name = "created_at") 30 | private Instant createdAt; 31 | 32 | @Transient 33 | private List connectors = new ArrayList<>(); 34 | } 35 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/GlobalSetting.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | 7 | import javax.persistence.*; 8 | import java.io.Serializable; 9 | import java.time.Instant; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @Data 15 | @Entity 16 | @Table(name = "global_setting") 17 | public class GlobalSetting implements Serializable { 18 | private static final long serialVersionUID = 1L; 19 | 20 | @Id 21 | @Column(name = "id") 22 | @GeneratedValue(strategy = GenerationType.IDENTITY) 23 | private Integer id; 24 | 25 | @Column(name = "proxy_host") 26 | private String proxyHost; 27 | 28 | @Column(name = "proxy_port") 29 | private Integer proxyPort; 30 | 31 | @Column(name = "is_enable_proxy") 32 | private Boolean enableProxy; 33 | 34 | @Column(name = "cold_data_keep_days") 35 | private Integer coldDataKeepDays; 36 | 37 | @Column(name = "open_api_key") 38 | private String openApiKey; 39 | 40 | @Column(name = "auto_save_site_blacklists") 41 | private String autoSaveSiteBlacklists; 42 | 43 | @Column(name = "created_at") 44 | private Instant createdAt; 45 | 46 | @Column(name = "updated_at") 47 | private Instant updatedAt; 48 | 49 | @Transient 50 | private Boolean changedOpenApiKey; 51 | } 52 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/PageArticleContent.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Data 13 | @Entity 14 | @Table(name = "page_article_content") 15 | public class PageArticleContent implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | @Id 19 | @Column(name = "id") 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Long id; 22 | 23 | @Column(name = "page_id") 24 | private Long pageId; 25 | 26 | @Column(name = "content") 27 | private String content; 28 | 29 | @Column(name = "article_content_category") 30 | private Integer articleContentCategory; 31 | 32 | @Column(name = "updated_at") 33 | private Instant updatedAt; 34 | } 35 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/PageProperties.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Data 12 | @Entity 13 | @Table(name="page_properties") 14 | public class PageProperties implements Serializable { 15 | 16 | private static final long serialVersionUID = 1L; 17 | 18 | @Id 19 | @Column(name = "id") 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Long id; 22 | 23 | @Column(name = "page_id") 24 | private Long pageId; 25 | 26 | @Column(name = "property_key") 27 | private String propertyKey; 28 | 29 | @Column(name = "property_value") 30 | private String propertyValue; 31 | } 32 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/PageRelation.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | import org.hibernate.annotations.DynamicUpdate; 5 | 6 | import javax.persistence.*; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Data 12 | @Entity 13 | @Table(name = "page_relation") 14 | public class PageRelation { 15 | @Id 16 | @Column(name = "page_id") 17 | private Long pageId; 18 | 19 | @Column(name = "page_unique_id") 20 | private String pageUniqueId; 21 | 22 | @Column(name = "page_self_thread_id") 23 | private String pageSelfThreadId; 24 | 25 | @Column(name = "page_conversation_id") 26 | private String pageConversationId; 27 | 28 | @Column(name = "page_reply_to_id") 29 | private String pageReplyToId; 30 | } 31 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/SearchHistory.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Data 13 | @Entity 14 | @Table(name = "search_history") 15 | public class SearchHistory implements Serializable { 16 | 17 | private static final long serialVersionUID = 1L; 18 | 19 | @Id 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | @Column(name = "id") 22 | private Long id; 23 | 24 | @Column(name = "query") 25 | private String query; 26 | 27 | @Column(name = "options") 28 | private String options; 29 | 30 | @Column(name = "search_at") 31 | private Instant searchAt; 32 | } 33 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/Source.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | 8 | @Data 9 | @Entity 10 | @Table(name = "source") 11 | public class Source implements Serializable { 12 | 13 | private static final long serialVersionUID = -5460676068284121149L; 14 | 15 | @Id 16 | @Column(name = "id") 17 | @GeneratedValue(strategy = GenerationType.IDENTITY) 18 | private Integer id; 19 | 20 | @Column(name = "site_name") 21 | private String siteName; 22 | 23 | @Column(name = "home_url") 24 | private String homeUrl; 25 | 26 | @Column(name = "subscribe_url") 27 | private String subscribeUrl; 28 | 29 | @Column(name = "domain") 30 | private String domain; 31 | 32 | @Column(name = "favicon_url") 33 | private String faviconUrl; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/TweetTrack.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Data 13 | @Entity 14 | @Table(name = "tweet_track") 15 | public class TweetTrack implements Serializable { 16 | 17 | private static final long serialVersionUID = 1L; 18 | 19 | @Id 20 | @Column(name = "id") 21 | @GeneratedValue(strategy = GenerationType.IDENTITY) 22 | private Long id; 23 | 24 | @Column(name = "tweet_id") 25 | private String tweetId; 26 | 27 | @Column(name = "read_at") 28 | private Instant readAt; 29 | 30 | @Column(name = "is_set_read_at") 31 | private Boolean setReadAt; 32 | } 33 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/TwitterUserSetting.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Data 13 | @Entity 14 | @Table(name = "twitter_user_setting") 15 | public class TwitterUserSetting implements Serializable { 16 | private static final long serialVersionUID = 1L; 17 | 18 | @Id 19 | @Column(name = "id") 20 | @GeneratedValue(strategy = GenerationType.IDENTITY) 21 | private Integer id; 22 | 23 | @Column(name = "name") 24 | private String name; 25 | 26 | @Column(name = "screen_name") 27 | private String screenName; 28 | 29 | @Column(name = "bookmark_to_library_type") 30 | private Integer bookmarkToLibraryType; 31 | 32 | @Column(name = "like_to_library_type") 33 | private Integer likeToLibraryType; 34 | 35 | @Column(name = "retweet_to_library_type") 36 | private Integer tweetToLibraryType; 37 | 38 | @Column(name = "is_myself") 39 | private Boolean myself; 40 | 41 | @Column(name = "created_at") 42 | private Instant createdAt; 43 | 44 | @Column(name = "updated_at") 45 | private Instant updatedAt; 46 | } 47 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/entity/User.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.entity; 2 | 3 | import lombok.Data; 4 | 5 | import javax.persistence.*; 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | 9 | @Data 10 | @Entity 11 | @Table(name="users") 12 | public class User implements Serializable { 13 | 14 | private static final long serialVersionUID = 1L; 15 | 16 | @Id 17 | @Column(name = "id") 18 | @GeneratedValue(strategy = GenerationType.IDENTITY) 19 | private Long id; 20 | 21 | @Column(name = "username") 22 | private String username; 23 | 24 | @Column(name = "password") 25 | private String password; 26 | 27 | @Column(name = "created_at") 28 | private Instant createdAt; 29 | 30 | @Column(name = "updated_at") 31 | private Instant updatedAt; 32 | 33 | @Column(name = "last_login_at") 34 | private Instant lastLoginAt; 35 | } 36 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/enums/ArticleContentCategory.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.enums; 2 | 3 | /** 4 | * @author lcomplete 5 | */ 6 | public enum ArticleContentCategory { 7 | RAW_CONTENT(0), 8 | SUMMARY(1); 9 | 10 | private final int code; 11 | 12 | ArticleContentCategory(int code) { 13 | this.code = code; 14 | } 15 | 16 | public int getCode(){ 17 | return code; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/exceptions/ConnectorFetchException.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.exceptions; 2 | 3 | import com.huntly.common.exceptions.BaseException; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | public class ConnectorFetchException extends BaseException { 9 | 10 | public ConnectorFetchException() { 11 | super(); 12 | } 13 | 14 | public ConnectorFetchException(String msg) { 15 | super(msg); 16 | } 17 | 18 | public ConnectorFetchException(Throwable throwable) { 19 | super(throwable); 20 | } 21 | 22 | public ConnectorFetchException(String msg, Throwable throwable) { 23 | super(msg, throwable); 24 | } 25 | 26 | public ConnectorFetchException(int status, String msg) { 27 | super(status, msg); 28 | } 29 | 30 | public ConnectorFetchException(int status, Throwable throwable) { 31 | super(status, throwable); 32 | } 33 | 34 | public ConnectorFetchException(int status, String msg, Throwable throwable) { 35 | super(status, msg, throwable); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/mapper/ConnectorItemMapper.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.mapper; 2 | 3 | import com.huntly.interfaces.external.dto.ConnectorItem; 4 | import com.huntly.server.domain.entity.Connector; 5 | import org.mapstruct.Mapper; 6 | import org.mapstruct.factory.Mappers; 7 | 8 | @Mapper 9 | public interface ConnectorItemMapper { 10 | ConnectorItemMapper INSTANCE = Mappers.getMapper(ConnectorItemMapper.class); 11 | 12 | ConnectorItem fromConnector(Connector connector); 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/model/ProxySetting.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.model; 2 | 3 | import lombok.Getter; 4 | import lombok.Setter; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Getter 10 | @Setter 11 | public class ProxySetting { 12 | private String host; 13 | private Integer port; 14 | } 15 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/domain/vo/PageDetail.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.domain.vo; 2 | 3 | import com.huntly.interfaces.external.dto.ConnectorItem; 4 | import com.huntly.server.domain.entity.Page; 5 | import com.huntly.server.domain.entity.PageArticleContent; 6 | import com.huntly.server.domain.entity.Source; 7 | import lombok.Getter; 8 | import lombok.Setter; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * @author lcomplete 14 | */ 15 | @Getter 16 | @Setter 17 | public class PageDetail { 18 | private Page page; 19 | 20 | private ConnectorItem connector; 21 | 22 | private Source source; 23 | 24 | private List pageContents; 25 | } 26 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/event/EventPublisher.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.event; 2 | 3 | import org.springframework.context.ApplicationEventPublisher; 4 | import org.springframework.stereotype.Component; 5 | 6 | /** 7 | * @author lcomplete 8 | */ 9 | @Component 10 | public class EventPublisher { 11 | private final ApplicationEventPublisher applicationEventPublisher; 12 | 13 | public EventPublisher(ApplicationEventPublisher eventPublisher) { 14 | this.applicationEventPublisher = eventPublisher; 15 | } 16 | 17 | public void publishInboxChangedEvent(InboxChangedEvent inboxChangedEvent){ 18 | applicationEventPublisher.publishEvent(inboxChangedEvent); 19 | } 20 | 21 | public void publishTweetPageCaptureEvent(TweetPageCaptureEvent tweetPageCaptureEvent) { 22 | applicationEventPublisher.publishEvent(tweetPageCaptureEvent); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/event/InboxChangedEvent.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.Setter; 6 | import lombok.experimental.Accessors; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Setter 12 | @Getter 13 | @Accessors(chain = true) 14 | public class InboxChangedEvent { 15 | public InboxChangedEvent(Integer connectorId) { 16 | this.connectorId = connectorId; 17 | } 18 | 19 | private Integer connectorId; 20 | 21 | /** 22 | * if this data is null, will compute inbox count at time 23 | */ 24 | private Integer inboxCount; 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/event/InboxChangedListener.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.event; 2 | 3 | import com.huntly.server.service.ConnectorService; 4 | import com.huntly.server.service.PageService; 5 | import org.springframework.context.event.EventListener; 6 | import org.springframework.scheduling.annotation.Async; 7 | import org.springframework.stereotype.Component; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @Component 13 | public class InboxChangedListener { 14 | 15 | private final ConnectorService connectorService; 16 | 17 | public InboxChangedListener(ConnectorService connectorService) { 18 | this.connectorService = connectorService; 19 | } 20 | 21 | @EventListener 22 | public void inboxChangedEvent(InboxChangedEvent event) { 23 | if (event.getConnectorId() != null && event.getConnectorId() > 0) { 24 | if (event.getInboxCount() != null) { 25 | connectorService.updateInboxCount(event.getConnectorId(), event.getInboxCount()); 26 | } else { 27 | connectorService.updateInboxCount(event.getConnectorId()); 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/event/TweetPageCaptureEvent.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.event; 2 | 3 | import com.huntly.server.domain.entity.Page; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.Setter; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Getter 12 | @Setter 13 | @AllArgsConstructor 14 | public class TweetPageCaptureEvent { 15 | private Page page; 16 | 17 | private String loginScreenName; 18 | 19 | private String browserScreenName; 20 | } 21 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/event/TweetPageCaptureListener.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.event; 2 | 3 | import com.huntly.server.service.CapturePageService; 4 | import org.springframework.context.event.EventListener; 5 | import org.springframework.scheduling.annotation.Async; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Component 12 | public class TweetPageCaptureListener { 13 | private final CapturePageService capturePageService; 14 | 15 | public TweetPageCaptureListener(CapturePageService capturePageService) { 16 | this.capturePageService = capturePageService; 17 | } 18 | 19 | @EventListener 20 | @Async 21 | public void tweetPageCaptureEvent(TweetPageCaptureEvent event) { 22 | capturePageService.saveTweetPage(event.getPage(), event.getLoginScreenName(), event.getBrowserScreenName()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/BaseRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.jpa.repository.JpaRepositoryWithLimit; 4 | import com.huntly.jpa.repository.JpaSpecificationExecutorWithProjection; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 7 | import org.springframework.data.repository.NoRepositoryBean; 8 | 9 | import java.io.Serializable; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @NoRepositoryBean 15 | public interface BaseRepository extends JpaRepository, JpaSpecificationExecutor, JpaSpecificationExecutorWithProjection, JpaRepositoryWithLimit { 16 | } 17 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/ConnectorRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.Connector; 4 | import org.springframework.data.domain.Sort; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | public interface ConnectorRepository extends JpaRepository, JpaSpecificationExecutor { 15 | List findByEnabledTrue(); 16 | 17 | Optional findBySubscribeUrlAndType(String subscribeUrl, Integer type); 18 | 19 | List findByFolderId(Integer folderId); 20 | 21 | List findByFolderIdAndType(Integer folderId,Integer type, Sort ascending); 22 | 23 | List findByType(Integer type); 24 | } -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/ConnectorSettingRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.ConnectorSetting; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | 7 | import java.util.List; 8 | 9 | public interface ConnectorSettingRepository extends JpaRepository, JpaSpecificationExecutor { 10 | public List findAllByConnectorId(Integer connectorId); 11 | } -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/FolderRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.Folder; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | @Repository 12 | public interface FolderRepository extends JpaRepository, JpaSpecificationExecutor { 13 | Optional findByName(String name); 14 | } -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/GlobalSettingRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.GlobalSetting; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.stereotype.Repository; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Repository 12 | public interface GlobalSettingRepository extends JpaRepository, JpaSpecificationExecutor { 13 | 14 | } 15 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/PageArticleContentRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.PageArticleContent; 4 | import org.springframework.stereotype.Repository; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | import java.util.List; 8 | import java.util.Optional; 9 | 10 | /** 11 | * @author lcomplete 12 | */ 13 | @Repository 14 | public interface PageArticleContentRepository extends BaseRepository { 15 | Optional findByPageIdAndArticleContentCategory(Long pageId, Integer articleContentCategory); 16 | 17 | @Transactional 18 | void deleteByPageIdAndArticleContentCategory(Long pageId, Integer articleContentCategory); 19 | 20 | List findAllByPageId(Long pageId); 21 | 22 | @Transactional 23 | void deleteByPageId(Long pageId); 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/SearchHistoryRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.SearchHistory; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * @author lcomplete 12 | */ 13 | @Repository 14 | public interface SearchHistoryRepository extends JpaRepository, JpaSpecificationExecutor { 15 | List findTop10ByOrderByIdDesc(); 16 | } 17 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/SourceRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.Source; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface SourceRepository extends JpaRepository, JpaSpecificationExecutor { 12 | 13 | Optional findByDomain(String domain); 14 | } -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/TweetTrackRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.jpa.repository.JpaRepositoryWithLimit; 4 | import com.huntly.server.domain.entity.TweetTrack; 5 | import org.springframework.data.jpa.repository.JpaRepository; 6 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 7 | import org.springframework.data.jpa.repository.Modifying; 8 | import org.springframework.data.jpa.repository.Query; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.transaction.annotation.Transactional; 11 | 12 | import java.time.Instant; 13 | import java.util.Optional; 14 | 15 | /** 16 | * @author lcomplete 17 | */ 18 | @Repository 19 | public interface TweetTrackRepository extends JpaRepository, JpaSpecificationExecutor,JpaRepositoryWithLimit { 20 | Optional findByTweetId(String tweetId); 21 | 22 | @Transactional 23 | @Modifying(clearAutomatically = true) 24 | @Query("DELETE FROM TweetTrack t WHERE t.readAt < :createdBefore") 25 | Integer deleteHistoryTrack(Instant createdBefore); 26 | } 27 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/TwitterUserSettingRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.TwitterUserSetting; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface TwitterUserSettingRepository extends JpaRepository, JpaSpecificationExecutor { 12 | Optional findByScreenName(String screenName); 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository; 2 | 3 | import com.huntly.server.domain.entity.User; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.data.jpa.repository.JpaSpecificationExecutor; 6 | import org.springframework.stereotype.Repository; 7 | 8 | import java.util.Optional; 9 | 10 | @Repository 11 | public interface UserRepository extends JpaRepository, JpaSpecificationExecutor { 12 | Optional findByUsername(String username); 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/custom/PageItemRepository.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository.custom; 2 | 3 | import com.huntly.interfaces.external.dto.PageItem; 4 | import com.huntly.interfaces.external.query.PageListQuery; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | public interface PageItemRepository { 12 | List list(PageListQuery listQuery); 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/repository/custom/PageItemRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.repository.custom; 2 | 3 | import com.huntly.interfaces.external.dto.PageItem; 4 | import com.huntly.interfaces.external.query.PageListQuery; 5 | import org.springframework.stereotype.Repository; 6 | 7 | import javax.persistence.EntityManager; 8 | import javax.persistence.PersistenceContext; 9 | import java.util.List; 10 | 11 | @Repository 12 | public class PageItemRepositoryImpl implements PageItemRepository { 13 | 14 | @PersistenceContext 15 | private EntityManager entityManager; 16 | 17 | @Override 18 | public List list(PageListQuery listQuery) { 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/security/jwt/UnAuthEntryPointJwt.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.security.jwt; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.huntly.common.api.model.ErrorDetail; 5 | import com.huntly.common.api.model.ErrorResponse; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.security.core.AuthenticationException; 9 | import org.springframework.security.web.AuthenticationEntryPoint; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.servlet.ServletException; 13 | import javax.servlet.http.HttpServletRequest; 14 | import javax.servlet.http.HttpServletResponse; 15 | import java.io.IOException; 16 | 17 | /** 18 | * @author lcomplete 19 | */ 20 | @Component 21 | @Slf4j 22 | public class UnAuthEntryPointJwt implements AuthenticationEntryPoint { 23 | 24 | @Override 25 | public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) 26 | throws IOException, ServletException { 27 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 28 | response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); 29 | 30 | ErrorDetail errorDetail = new ErrorDetail(); 31 | errorDetail.setCode(HttpServletResponse.SC_UNAUTHORIZED); 32 | errorDetail.setMessage(authException.getMessage()); 33 | errorDetail.setType("Unauthorized"); 34 | 35 | ErrorResponse errorResponse = new ErrorResponse(); 36 | errorResponse.setError(errorDetail); 37 | final ObjectMapper mapper = new ObjectMapper(); 38 | mapper.writeValue(response.getOutputStream(), errorResponse); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/security/services/UserDetailsServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.security.services; 2 | 3 | import com.huntly.common.exceptions.NoSuchDataException; 4 | import com.huntly.server.domain.entity.User; 5 | import com.huntly.server.repository.UserRepository; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Service; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @Service 15 | public class UserDetailsServiceImpl implements UserDetailsService { 16 | private final UserRepository userRepository; 17 | 18 | public UserDetailsServiceImpl(UserRepository userRepository) { 19 | this.userRepository = userRepository; 20 | } 21 | 22 | @Override 23 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 24 | User user = userRepository.findByUsername(username).orElseThrow(() -> new NoSuchDataException("User not found with username: " + username)); 25 | return UserDetailsImpl.build(user); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/service/BasePageService.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.service; 2 | 3 | import com.huntly.server.domain.entity.Page; 4 | import com.huntly.server.repository.PageRepository; 5 | import org.springframework.transaction.annotation.Transactional; 6 | 7 | import java.time.Instant; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | public abstract class BasePageService { 13 | 14 | protected PageRepository pageRepository; 15 | 16 | LuceneService luceneService; 17 | 18 | protected BasePageService(PageRepository pageRepository, LuceneService luceneService) { 19 | this.pageRepository = pageRepository; 20 | this.luceneService = luceneService; 21 | } 22 | 23 | //@Transactional(rollbackFor = Exception.class) 24 | protected Page save(Page page) { 25 | page.setUpdatedAt(Instant.now()); 26 | pageRepository.save(page); 27 | luceneService.indexPage(page); 28 | return page; 29 | } 30 | 31 | protected void deleteById(Long id){ 32 | pageRepository.deleteById(id); 33 | luceneService.deletePage(id); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/service/ConnectorSettingService.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.service; 2 | 3 | import com.huntly.server.repository.ConnectorSettingRepository; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class ConnectorSettingService { 9 | 10 | @Autowired 11 | private ConnectorSettingRepository connectorSettingRepository; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/service/SourceService.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.service; 2 | 3 | import com.huntly.server.domain.entity.Source; 4 | import com.huntly.server.repository.SourceRepository; 5 | import org.springframework.stereotype.Service; 6 | 7 | import java.util.NoSuchElementException; 8 | import java.util.Optional; 9 | 10 | @Service 11 | public class SourceService { 12 | 13 | private final SourceRepository sourceRepository; 14 | 15 | public SourceService(SourceRepository sourceRepository) { 16 | this.sourceRepository = sourceRepository; 17 | } 18 | 19 | public Optional findById(Integer id) { 20 | return sourceRepository.findById(id); 21 | } 22 | 23 | private Source requireOne(Integer id) { 24 | return sourceRepository.findById(id) 25 | .orElseThrow(() -> new NoSuchElementException("Resource not found: " + id)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/task/ConnectorScheduledTask.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.task; 2 | 3 | import com.huntly.server.service.ConnectorFetchService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.stereotype.Component; 7 | 8 | /** 9 | * @author lcomplete 10 | */ 11 | @Component 12 | @Slf4j 13 | public class ConnectorScheduledTask { 14 | private final ConnectorFetchService connectorFetchService; 15 | 16 | public ConnectorScheduledTask(ConnectorFetchService connectorFetchService) { 17 | this.connectorFetchService = connectorFetchService; 18 | } 19 | 20 | @Scheduled(initialDelay = 1000 * 10, fixedDelay = 1000 * 60) 21 | public void connectorFetchPages() { 22 | connectorFetchService.fetchAllConnectPages(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/task/TweetTrackTask.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.task; 2 | 3 | import com.huntly.server.service.TweetTrackService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.scheduling.annotation.Scheduled; 6 | import org.springframework.stereotype.Component; 7 | 8 | import java.time.Instant; 9 | import java.time.temporal.ChronoUnit; 10 | 11 | /** 12 | * @author lcomplete 13 | */ 14 | @Component 15 | @Slf4j 16 | public class TweetTrackTask { 17 | private final TweetTrackService tweetTrackService; 18 | 19 | public TweetTrackTask(TweetTrackService tweetTrackService) { 20 | this.tweetTrackService = tweetTrackService; 21 | } 22 | 23 | @Scheduled(initialDelay = 1000 * 5, fixedDelay = 1000 * 60 * 5) 24 | public void trackRead() { 25 | tweetTrackService.trackNotSetReads(); 26 | } 27 | 28 | @Scheduled(initialDelay = 1000 * 60, fixedDelay = 1000 * 60 * 60) 29 | public void cleanHistoryTrack() { 30 | Instant createdBefore = Instant.now().minus(1, ChronoUnit.DAYS); 31 | Integer effectCount = tweetTrackService.cleanHistoryTrack(createdBefore); 32 | log.info("clean history track, effect count: {}", effectCount); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/util/HtmlText.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.util; 2 | 3 | import lombok.Data; 4 | 5 | /** 6 | * @author lcomplete 7 | */ 8 | @Data 9 | public class HtmlText { 10 | private String html; 11 | 12 | private String text; 13 | } 14 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/util/JSONUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.util; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import com.fasterxml.jackson.databind.SerializationFeature; 6 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 7 | import lombok.experimental.UtilityClass; 8 | 9 | /** 10 | * @author lcomplete 11 | */ 12 | @UtilityClass 13 | public class JSONUtils { 14 | public static String toJson(Object obj) { 15 | ObjectMapper mapper = new ObjectMapper(); 16 | mapper.registerModule(new JavaTimeModule()); 17 | mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 18 | try { 19 | return mapper.writeValueAsString(obj); 20 | } catch (JsonProcessingException e) { 21 | throw new RuntimeException(e); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/java/com/huntly/server/util/PageSizeUtils.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.util; 2 | 3 | import lombok.experimental.UtilityClass; 4 | import org.apache.commons.lang3.ObjectUtils; 5 | 6 | @UtilityClass 7 | public class PageSizeUtils { 8 | 9 | public static final int DEFAULT_PAGE_SIZE = 30; 10 | 11 | public static final int MAX_PAGE_SIZE = 500; 12 | 13 | public static int getPageSize(int requestPageSize, int defaultPageSize, int maxPageSize) { 14 | if (requestPageSize <= 0) { 15 | requestPageSize = defaultPageSize; 16 | } 17 | return Math.min(requestPageSize, maxPageSize); 18 | } 19 | 20 | public static int getPageSize(int requestPageSize, int defaultPageSize) { 21 | return getPageSize(requestPageSize, defaultPageSize, MAX_PAGE_SIZE); 22 | } 23 | 24 | public static int getPageSize(int requestPageSize) { 25 | return getPageSize(requestPageSize, DEFAULT_PAGE_SIZE, MAX_PAGE_SIZE); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/resources/META-INF/services/org.hibernate.boot.spi.MetadataBuilderInitializer: -------------------------------------------------------------------------------- 1 | com.huntly.server.data.dialect.SQLiteMetadataBuilderInitializer -------------------------------------------------------------------------------- /app/server/huntly-server/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | app: 2 | id: huntly-server 3 | 4 | spring: 5 | datasource: 6 | driver-class-name: org.sqlite.JDBC 7 | url: jdbc:sqlite:${huntly.dataDir:}db.sqlite?date_class=TEXT 8 | hikari: 9 | maximum-pool-size: 1 10 | jpa: 11 | show-sql: false 12 | hibernate: 13 | ddl-auto: update 14 | database-platform: com.huntly.server.data.dialect.SQLiteDialect 15 | mvc: 16 | pathmatch: 17 | matching-strategy: ant_path_matcher # use this to make springfox work 18 | web: 19 | resources: 20 | cache: 21 | cachecontrol: 22 | max-age: 7d 23 | 24 | huntly: 25 | jwtSecret: MTI2ZTc1NzAtMjJlMy00MmVlLTkwYmQtOTVjNGM4ZTRhN2YzMTI2ZTc1NzAtMjJlMy00MmVlLTkwYmQtOTVjNGM4ZTRhN2Yz 26 | jwtExpirationDays: 365 27 | connectorFetchCorePoolSize: 3 28 | 29 | server: 30 | servlet: 31 | context-path: / 32 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/test/java/com/huntly/server/connector/twitter/TweetParserTest.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.connector.twitter; 2 | 3 | import com.huntly.interfaces.external.model.InterceptTweets; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.io.IOException; 7 | import java.nio.charset.StandardCharsets; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | class TweetParserTest { 12 | 13 | String getTestJsonData() throws IOException { 14 | ClassLoader classLoader=getClass().getClassLoader(); 15 | try (var stream = classLoader.getResourceAsStream("tweet_timeline.json")) { 16 | assert stream != null; 17 | var bytes = stream.readAllBytes(); 18 | return new String(bytes, StandardCharsets.UTF_8); 19 | } 20 | } 21 | 22 | @Test 23 | void tweetsToPages() throws IOException { 24 | InterceptTweets interceptTweets= new InterceptTweets(); 25 | interceptTweets.setCategory("timeline"); 26 | interceptTweets.setJsonData(getTestJsonData()); 27 | TweetParser tweetParser = new TweetParser(); 28 | var pages= tweetParser.tweetsToPages(interceptTweets); 29 | System.out.println(pages.size()); 30 | assertThat(pages).hasSizeGreaterThanOrEqualTo(30); 31 | } 32 | } -------------------------------------------------------------------------------- /app/server/huntly-server/src/test/java/com/huntly/server/page/ContentCleaner.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.page; 2 | 3 | import com.huntly.common.util.TextUtils; 4 | import com.huntly.server.util.HtmlText; 5 | import com.huntly.server.util.HtmlUtils; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | public class ContentCleaner { 9 | private final String content; 10 | 11 | private final String description; 12 | 13 | private final String baseUri; 14 | 15 | private String cleanHtml; 16 | 17 | private String cleanDescription; 18 | 19 | private String cleanText; 20 | 21 | public ContentCleaner(String content, String description, String baseUri) { 22 | this.content = content; 23 | this.description = description; 24 | this.baseUri = baseUri; 25 | 26 | extract(); 27 | } 28 | 29 | private void extract() { 30 | boolean hasContent = StringUtils.isNotBlank(content); 31 | boolean hasDescription = StringUtils.isNotBlank(description); 32 | if (!hasContent && !hasDescription) { 33 | return; 34 | } 35 | HtmlText htmlText = HtmlUtils.clean(hasContent ? content : description, baseUri); 36 | cleanDescription = hasDescription ? HtmlUtils.clean(this.description, baseUri).getText() : TextUtils.trimTruncate(htmlText.getText(), 512); 37 | cleanHtml = htmlText.getHtml(); 38 | cleanText = htmlText.getText(); 39 | } 40 | 41 | public String getCleanHtml() { 42 | return cleanHtml; 43 | } 44 | 45 | public String getCleanDescription() { 46 | return cleanDescription; 47 | } 48 | 49 | public String getCleanText() { 50 | return cleanText; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/server/huntly-server/src/test/java/com/huntly/server/page/ContentCleanerTest.java: -------------------------------------------------------------------------------- 1 | package com.huntly.server.page; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.assertj.core.api.Assertions.assertThat; 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | class ContentCleanerTest { 9 | 10 | @Test 11 | void getCleanHtml_Styled() { 12 | ContentCleaner cleaner = new ContentCleaner("

test
","desc","http://codelc.com"); 13 | //System.out.println(cleaner.getCleanHtml()); 14 | assertThat(cleaner.getCleanHtml()).contains("style"); 15 | } 16 | 17 | @Test 18 | void getCleanHtml_SpanStyled() { 19 | ContentCleaner cleaner = new ContentCleaner("test","desc","http://codelc.com"); 20 | //System.out.println(cleaner.getCleanHtml()); 21 | assertThat(cleaner.getCleanHtml()).contains("span"); 22 | assertThat(cleaner.getCleanHtml()).contains("style"); 23 | } 24 | 25 | @Test 26 | void getCleanDescription() { 27 | } 28 | 29 | @Test 30 | void getCleanText() { 31 | } 32 | } -------------------------------------------------------------------------------- /app/tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | app.settings.json 27 | **.jar 28 | 29 | /src-tauri/server_bin -------------------------------------------------------------------------------- /app/tauri/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["tauri-apps.tauri-vscode", "rust-lang.rust-analyzer"] 3 | } 4 | -------------------------------------------------------------------------------- /app/tauri/README.md: -------------------------------------------------------------------------------- 1 | # Tauri + React + Typescript 2 | 3 | This template should help get you started developing with Tauri, React and Typescript in Vite. 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) 8 | -------------------------------------------------------------------------------- /app/tauri/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Huntly 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/tauri/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tauri", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "tauri": "tauri" 11 | }, 12 | "dependencies": { 13 | "@emotion/react": "^11.10.6", 14 | "@emotion/styled": "^11.10.6", 15 | "@mui/icons-material": "^5.11.11", 16 | "@mui/material": "^5.11.15", 17 | "@tauri-apps/api": "^1.2.0", 18 | "formik": "^2.2.9", 19 | "react": "^18.2.0", 20 | "react-dom": "^18.2.0", 21 | "tauri-plugin-autostart-api": "https://github.com/tauri-apps/tauri-plugin-autostart", 22 | "yup": "^1.0.2" 23 | }, 24 | "devDependencies": { 25 | "@tauri-apps/cli": "^1.2.3", 26 | "@types/node": "^18.7.10", 27 | "@types/react": "^18.0.15", 28 | "@types/react-dom": "^18.0.6", 29 | "@vitejs/plugin-react": "^3.0.0", 30 | "autoprefixer": "^10.4.14", 31 | "postcss": "^8.4.21", 32 | "tailwindcss": "^3.3.1", 33 | "typescript": "^4.6.4", 34 | "vite": "^4.0.0" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/tauri/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /app/tauri/src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | -------------------------------------------------------------------------------- /app/tauri/src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tauri" 3 | version = "0.0.0" 4 | description = "A Tauri App" 5 | authors = ["you"] 6 | license = "" 7 | repository = "" 8 | edition = "2021" 9 | 10 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 11 | 12 | [build-dependencies] 13 | tauri-build = { version = "1.2.1", features = [] } 14 | embed-resource = "2.1" 15 | 16 | [dependencies] 17 | tauri = { version = "1.2.4", features = ["fs-read-file", "fs-write-file", "shell-open", "system-tray"] } 18 | tauri-plugin-autostart = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" } 19 | serde = { version = "1.0", features = ["derive"] } 20 | reqwest = { version = "0.11.16", features = ["json"] } 21 | serde_json = "1.0" 22 | lazy_static = "1.4" 23 | 24 | [features] 25 | # this feature is used for production builds or when `devPath` points to the filesystem 26 | # DO NOT REMOVE!! 27 | custom-protocol = ["tauri/custom-protocol"] 28 | -------------------------------------------------------------------------------- /app/tauri/src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | extern crate embed_resource; 2 | 3 | fn main() { 4 | tauri_build::build(); 5 | // embed_resource::compile("huntly-manifest.rc", embed_resource::NONE); 6 | } 7 | -------------------------------------------------------------------------------- /app/tauri/src-tauri/huntly-manifest.rc: -------------------------------------------------------------------------------- 1 | #define RT_MANIFEST 24 2 | 1 RT_MANIFEST "huntly.exe.manifest" -------------------------------------------------------------------------------- /app/tauri/src-tauri/huntly.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/tauri/src-tauri/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/tauri/src-tauri/icons/favicon-128x128.png -------------------------------------------------------------------------------- /app/tauri/src-tauri/icons/favicon-128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/tauri/src-tauri/icons/favicon-128x128@2x.png -------------------------------------------------------------------------------- /app/tauri/src-tauri/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/tauri/src-tauri/icons/favicon-16x16.png -------------------------------------------------------------------------------- /app/tauri/src-tauri/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/tauri/src-tauri/icons/favicon-32x32.png -------------------------------------------------------------------------------- /app/tauri/src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/tauri/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /app/tauri/src-tauri/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/app/tauri/src-tauri/icons/icon.ico -------------------------------------------------------------------------------- /app/tauri/src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { 3 | "beforeDevCommand": "yarn dev", 4 | "beforeBuildCommand": "yarn build", 5 | "devPath": "http://localhost:1420", 6 | "distDir": "../dist", 7 | "withGlobalTauri": false 8 | }, 9 | "package": { 10 | "productName": "Huntly", 11 | "version": "0.3.5" 12 | }, 13 | "tauri": { 14 | "allowlist": { 15 | "all": false, 16 | "shell": { 17 | "all": false, 18 | "open": true 19 | }, 20 | "fs": { 21 | "readFile": true, 22 | "writeFile": true, 23 | "scope": ["$APP/*", "$RESOURCE/*","server_bin/*"] 24 | } 25 | }, 26 | "bundle": { 27 | "active": true, 28 | "icon": [ 29 | "icons/favicon-16x16.png", 30 | "icons/favicon-32x32.png", 31 | "icons/favicon-128x128.png", 32 | "icons/favicon-128x128@2x.png", 33 | "icons/icon.icns", 34 | "icons/icon.ico" 35 | ], 36 | "identifier": "lcomplete.huntly", 37 | "targets": "all", 38 | "resources": ["./server_bin/*"] 39 | }, 40 | "security": { 41 | "csp": "" 42 | }, 43 | "updater": { 44 | "active": false 45 | }, 46 | "windows": [ 47 | { 48 | "fullscreen": false, 49 | "resizable": false, 50 | "title": "Huntly", 51 | "width": 550, 52 | "height": 550 53 | } 54 | ], 55 | "systemTray": { 56 | "iconPath": "icons/icon.ico", 57 | "iconAsTemplate": true, 58 | "menuOnLeftClick": true 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/tauri/src/App.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | html,body,#root{ 6 | @apply h-full; 7 | } -------------------------------------------------------------------------------- /app/tauri/src/main.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom/client"; 3 | import App from "./App"; 4 | import { CssBaseline, StyledEngineProvider } from "@mui/material"; 5 | 6 | ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( 7 | 8 | 9 | 10 | 11 | ); 12 | -------------------------------------------------------------------------------- /app/tauri/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /app/tauri/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: ["./src/**/*.{js,jsx,ts,tsx}"], 4 | theme: { 5 | extend: {}, 6 | }, 7 | corePlugins: { 8 | // Remove Tailwind CSS's preflight style so it can use the MUI's preflight instead (CssBaseline). 9 | preflight: false, 10 | }, 11 | plugins: [], 12 | } 13 | 14 | -------------------------------------------------------------------------------- /app/tauri/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "lib": ["DOM", "DOM.Iterable", "ESNext"], 6 | "allowJs": false, 7 | "skipLibCheck": true, 8 | "esModuleInterop": false, 9 | "allowSyntheticDefaultImports": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "module": "ESNext", 13 | "moduleResolution": "Node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src"], 20 | "references": [{ "path": "./tsconfig.node.json" }] 21 | } 22 | -------------------------------------------------------------------------------- /app/tauri/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /app/tauri/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import react from "@vitejs/plugin-react"; 3 | 4 | const mobile = 5 | process.env.TAURI_PLATFORM === "android" || 6 | process.env.TAURI_PLATFORM === "ios"; 7 | 8 | // https://vitejs.dev/config/ 9 | export default defineConfig(async () => ({ 10 | plugins: [react()], 11 | 12 | // Vite options tailored for Tauri development and only applied in `tauri dev` or `tauri build` 13 | // prevent vite from obscuring rust errors 14 | clearScreen: false, 15 | // tauri expects a fixed port, fail if that port is not available 16 | server: { 17 | port: 1420, 18 | strictPort: true, 19 | }, 20 | // to make use of `TAURI_DEBUG` and other env variables 21 | // https://tauri.studio/v1/api/config#buildconfig.beforedevcommand 22 | envPrefix: ["VITE_", "TAURI_"], 23 | build: { 24 | // Tauri supports es2021 25 | target: process.env.TAURI_PLATFORM == "windows" ? "chrome105" : "safari13", 26 | // don't minify for debug builds 27 | minify: !process.env.TAURI_DEBUG ? "esbuild" : false, 28 | // produce sourcemaps for debug builds 29 | sourcemap: !!process.env.TAURI_DEBUG, 30 | }, 31 | })); 32 | -------------------------------------------------------------------------------- /static/architect.puml: -------------------------------------------------------------------------------- 1 | @startuml 2 | allowmixing 3 | 4 | node 浏览器插件 5 | 6 | node 前端网站 7 | 8 | database SQLite [ 9 | SQLite 10 | ---- 11 | 网页浏览数据 12 | ] 13 | database Lucene [ 14 | Lucene 15 | ---- 16 | 文本索引 17 | ] 18 | 19 | package 桌面应用 { 20 | node 客户端 [ 21 | 客户端 22 | ---- 23 | Tauri 24 | ] 25 | node 服务端 [ 26 | 服务端 27 | ---- 28 | Spring Boot 29 | ] 30 | } 31 | 32 | 33 | 浏览器插件 --> 服务端 : 捕获数据 34 | 前端网站 --> 服务端 : REST API 35 | 客户端 -right-> 服务端 : 启动 36 | 服务端 -down-> SQLite : 存储 37 | 服务端 -down-> Lucene : 搜索 38 | 39 | @enduml 40 | -------------------------------------------------------------------------------- /static/images/intro1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/static/images/intro1.png -------------------------------------------------------------------------------- /static/images/intro2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/static/images/intro2.png -------------------------------------------------------------------------------- /static/images/jb_beam.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/static/images/jb_beam.png -------------------------------------------------------------------------------- /static/images/wechat.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/static/images/wechat.JPG -------------------------------------------------------------------------------- /static/images/zfb.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/huntly/d47594a2a7655d137304924a4cc10f9a00fed8c1/static/images/zfb.JPG --------------------------------------------------------------------------------