Showing preview only (4,960K chars total). Download the full file or copy to clipboard to get everything.
Repository: mvdicarlo/postybirb
Branch: main
Commit: 079e51f45ede
Files: 1230
Total size: 4.4 MB
Directory structure:
gitextract_minozs1b/
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .github/
│ └── workflows/
│ ├── build.yml
│ ├── ci.yml
│ └── i18n.yml.disabled
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── post-merge
├── .prettierignore
├── .prettierrc
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── .yarn/
│ └── patches/
│ ├── @handlewithcare-prosemirror-inputrules-npm-0.1.3-897e37b56f.patch
│ ├── @tiptap-html-npm-3.15.3-a9641901db.patch
│ ├── jest-snapshot-npm-29.7.0-15ef0a4ad6.patch
│ └── strong-log-transformer-npm-2.1.0-45addd9278.patch
├── .yarnrc.yml
├── Dockerfile
├── LICENSE
├── README.md
├── TRANSLATION.md
├── apps/
│ ├── client-server/
│ │ ├── .eslintrc.json
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── account/
│ │ │ │ │ ├── account.controller.ts
│ │ │ │ │ ├── account.events.ts
│ │ │ │ │ ├── account.module.ts
│ │ │ │ │ ├── account.service.spec.ts
│ │ │ │ │ ├── account.service.ts
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── create-account.dto.ts
│ │ │ │ │ │ ├── set-website-data-request.dto.ts
│ │ │ │ │ │ └── update-account.dto.ts
│ │ │ │ │ └── login-state-poller.ts
│ │ │ │ ├── app.controller.ts
│ │ │ │ ├── app.module.ts
│ │ │ │ ├── app.service.ts
│ │ │ │ ├── common/
│ │ │ │ │ ├── controller/
│ │ │ │ │ │ └── postybirb-controller.ts
│ │ │ │ │ └── service/
│ │ │ │ │ └── postybirb-service.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── custom-shortcuts/
│ │ │ │ │ ├── custom-shortcut.events.ts
│ │ │ │ │ ├── custom-shortcuts.controller.ts
│ │ │ │ │ ├── custom-shortcuts.module.ts
│ │ │ │ │ ├── custom-shortcuts.service.ts
│ │ │ │ │ └── dtos/
│ │ │ │ │ ├── create-custom-shortcut.dto.ts
│ │ │ │ │ └── update-custom-shortcut.dto.ts
│ │ │ │ ├── directory-watchers/
│ │ │ │ │ ├── directory-watcher.events.ts
│ │ │ │ │ ├── directory-watchers.controller.ts
│ │ │ │ │ ├── directory-watchers.module.ts
│ │ │ │ │ ├── directory-watchers.service.spec.ts
│ │ │ │ │ ├── directory-watchers.service.ts
│ │ │ │ │ └── dtos/
│ │ │ │ │ ├── check-path.dto.ts
│ │ │ │ │ ├── create-directory-watcher.dto.ts
│ │ │ │ │ └── update-directory-watcher.dto.ts
│ │ │ │ ├── drizzle/
│ │ │ │ │ ├── models/
│ │ │ │ │ │ ├── account.entity.ts
│ │ │ │ │ │ ├── custom-shortcut.entity.ts
│ │ │ │ │ │ ├── database-entity.spec.ts
│ │ │ │ │ │ ├── database-entity.ts
│ │ │ │ │ │ ├── directory-watcher.entity.ts
│ │ │ │ │ │ ├── file-buffer.entity.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── notification.entity.ts
│ │ │ │ │ │ ├── post-event.entity.ts
│ │ │ │ │ │ ├── post-queue-record.entity.ts
│ │ │ │ │ │ ├── post-record.entity.ts
│ │ │ │ │ │ ├── settings.entity.ts
│ │ │ │ │ │ ├── submission-file.entity.ts
│ │ │ │ │ │ ├── submission.entity.ts
│ │ │ │ │ │ ├── tag-converter.entity.ts
│ │ │ │ │ │ ├── tag-group.entity.ts
│ │ │ │ │ │ ├── user-converter.entity.ts
│ │ │ │ │ │ ├── user-specified-website-options.entity.ts
│ │ │ │ │ │ ├── website-data.entity.ts
│ │ │ │ │ │ └── website-options.entity.ts
│ │ │ │ │ ├── postybirb-database/
│ │ │ │ │ │ ├── find-options.type.ts
│ │ │ │ │ │ ├── postybirb-database.spec.ts
│ │ │ │ │ │ ├── postybirb-database.ts
│ │ │ │ │ │ ├── postybirb-database.util.ts
│ │ │ │ │ │ └── schema-entity-map.ts
│ │ │ │ │ └── transaction-context.ts
│ │ │ │ ├── file/
│ │ │ │ │ ├── file.controller.ts
│ │ │ │ │ ├── file.module.ts
│ │ │ │ │ ├── file.service.spec.ts
│ │ │ │ │ ├── file.service.ts
│ │ │ │ │ ├── models/
│ │ │ │ │ │ ├── multer-file-info.ts
│ │ │ │ │ │ ├── task-type.enum.ts
│ │ │ │ │ │ └── task.ts
│ │ │ │ │ ├── services/
│ │ │ │ │ │ ├── create-file.service.ts
│ │ │ │ │ │ └── update-file.service.ts
│ │ │ │ │ └── utils/
│ │ │ │ │ └── image.util.ts
│ │ │ │ ├── file-converter/
│ │ │ │ │ ├── converters/
│ │ │ │ │ │ ├── file-converter.ts
│ │ │ │ │ │ └── text-file-converter.ts
│ │ │ │ │ ├── file-converter.module.ts
│ │ │ │ │ ├── file-converter.service.spec.ts
│ │ │ │ │ └── file-converter.service.ts
│ │ │ │ ├── form-generator/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ └── form-generation-request.dto.ts
│ │ │ │ │ ├── form-generator.controller.ts
│ │ │ │ │ ├── form-generator.module.ts
│ │ │ │ │ ├── form-generator.service.spec.ts
│ │ │ │ │ └── form-generator.service.ts
│ │ │ │ ├── image-processing/
│ │ │ │ │ ├── image-processing.module.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── sharp-instance-manager.ts
│ │ │ │ ├── legacy-database-importer/
│ │ │ │ │ ├── converters/
│ │ │ │ │ │ ├── legacy-converter.ts
│ │ │ │ │ │ ├── legacy-custom-shortcut.converter.spec.ts
│ │ │ │ │ │ ├── legacy-custom-shortcut.converter.ts
│ │ │ │ │ │ ├── legacy-tag-converter.converter.spec.ts
│ │ │ │ │ │ ├── legacy-tag-converter.converter.ts
│ │ │ │ │ │ ├── legacy-tag-group.converter.spec.ts
│ │ │ │ │ │ ├── legacy-tag-group.converter.ts
│ │ │ │ │ │ ├── legacy-user-account.converter.spec.ts
│ │ │ │ │ │ ├── legacy-user-account.converter.ts
│ │ │ │ │ │ ├── legacy-website-data.converter.spec.ts
│ │ │ │ │ │ └── legacy-website-data.converter.ts
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ └── legacy-import.dto.ts
│ │ │ │ │ ├── legacy-database-importer.controller.ts
│ │ │ │ │ ├── legacy-database-importer.module.ts
│ │ │ │ │ ├── legacy-database-importer.service.ts
│ │ │ │ │ ├── legacy-entities/
│ │ │ │ │ │ ├── legacy-converter-entity.ts
│ │ │ │ │ │ ├── legacy-custom-shortcut.ts
│ │ │ │ │ │ ├── legacy-tag-converter.ts
│ │ │ │ │ │ ├── legacy-tag-group.ts
│ │ │ │ │ │ ├── legacy-user-account.ts
│ │ │ │ │ │ └── legacy-website-data.ts
│ │ │ │ │ ├── transformers/
│ │ │ │ │ │ ├── implementations/
│ │ │ │ │ │ │ ├── bluesky-data-transformer.ts
│ │ │ │ │ │ │ ├── custom-data-transformer.ts
│ │ │ │ │ │ │ ├── discord-data-transformer.ts
│ │ │ │ │ │ │ ├── e621-data-transformer.ts
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── inkbunny-data-transformer.ts
│ │ │ │ │ │ │ ├── megalodon-data-transformer.ts
│ │ │ │ │ │ │ ├── telegram-data-transformer.ts
│ │ │ │ │ │ │ └── twitter-data-transformer.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── legacy-website-data-transformer.ts
│ │ │ │ │ │ └── website-data-transformer-registry.ts
│ │ │ │ │ └── utils/
│ │ │ │ │ ├── ndjson-parser.ts
│ │ │ │ │ └── website-name-mapper.ts
│ │ │ │ ├── logs/
│ │ │ │ │ ├── logs.controller.ts
│ │ │ │ │ ├── logs.module.ts
│ │ │ │ │ └── logs.service.ts
│ │ │ │ ├── notifications/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── create-notification.dto.ts
│ │ │ │ │ │ └── update-notification.dto.ts
│ │ │ │ │ ├── notification.events.ts
│ │ │ │ │ ├── notifications.controller.ts
│ │ │ │ │ ├── notifications.module.ts
│ │ │ │ │ ├── notifications.service.spec.ts
│ │ │ │ │ └── notifications.service.ts
│ │ │ │ ├── post/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── post-queue-action.dto.ts
│ │ │ │ │ │ └── queue-post-record.dto.ts
│ │ │ │ │ ├── errors/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── invalid-post-chain.error.ts
│ │ │ │ │ ├── models/
│ │ │ │ │ │ ├── cancellable-token.ts
│ │ │ │ │ │ ├── cancellation-error.ts
│ │ │ │ │ │ └── posting-file.ts
│ │ │ │ │ ├── post.controller.ts
│ │ │ │ │ ├── post.module.ts
│ │ │ │ │ ├── post.service.ts
│ │ │ │ │ └── services/
│ │ │ │ │ ├── post-file-resizer/
│ │ │ │ │ │ ├── post-file-resizer.service.spec.ts
│ │ │ │ │ │ └── post-file-resizer.service.ts
│ │ │ │ │ ├── post-manager/
│ │ │ │ │ │ └── post-manager.controller.ts
│ │ │ │ │ ├── post-manager-v2/
│ │ │ │ │ │ ├── base-post-manager.service.ts
│ │ │ │ │ │ ├── file-submission-post-manager.service.spec.ts
│ │ │ │ │ │ ├── file-submission-post-manager.service.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── message-submission-post-manager.service.spec.ts
│ │ │ │ │ │ ├── message-submission-post-manager.service.ts
│ │ │ │ │ │ └── post-manager-registry.service.ts
│ │ │ │ │ ├── post-queue/
│ │ │ │ │ │ ├── post-queue.controller.ts
│ │ │ │ │ │ ├── post-queue.service.spec.ts
│ │ │ │ │ │ └── post-queue.service.ts
│ │ │ │ │ └── post-record-factory/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── post-event.repository.ts
│ │ │ │ │ ├── post-record-factory.service.spec.ts
│ │ │ │ │ └── post-record-factory.service.ts
│ │ │ │ ├── post-parsers/
│ │ │ │ │ ├── models/
│ │ │ │ │ │ ├── description-node/
│ │ │ │ │ │ │ ├── converters/
│ │ │ │ │ │ │ │ ├── base-converter.ts
│ │ │ │ │ │ │ │ ├── bbcode-converter.ts
│ │ │ │ │ │ │ │ ├── custom-converter.ts
│ │ │ │ │ │ │ │ ├── html-converter.ts
│ │ │ │ │ │ │ │ ├── npf-converter.spec.ts
│ │ │ │ │ │ │ │ ├── npf-converter.ts
│ │ │ │ │ │ │ │ └── plaintext-converter.ts
│ │ │ │ │ │ │ ├── description-node-tree.ts
│ │ │ │ │ │ │ ├── description-node.base.ts
│ │ │ │ │ │ │ └── description-node.types.ts
│ │ │ │ │ │ └── description-node.spec.ts
│ │ │ │ │ ├── parsers/
│ │ │ │ │ │ ├── content-warning-parser.ts
│ │ │ │ │ │ ├── description-parser.service.spec.ts
│ │ │ │ │ │ ├── description-parser.service.ts
│ │ │ │ │ │ ├── rating-parser.spec.ts
│ │ │ │ │ │ ├── rating-parser.ts
│ │ │ │ │ │ ├── tag-parser.service.spec.ts
│ │ │ │ │ │ ├── tag-parser.service.ts
│ │ │ │ │ │ ├── title-parser.spec.ts
│ │ │ │ │ │ └── title-parser.ts
│ │ │ │ │ ├── post-parsers.module.ts
│ │ │ │ │ └── post-parsers.service.ts
│ │ │ │ ├── remote/
│ │ │ │ │ ├── models/
│ │ │ │ │ │ └── update-cookies-remote.dto.ts
│ │ │ │ │ ├── remote.controller.ts
│ │ │ │ │ ├── remote.middleware.ts
│ │ │ │ │ ├── remote.module.ts
│ │ │ │ │ └── remote.service.ts
│ │ │ │ ├── security-and-authentication/
│ │ │ │ │ └── ssl.ts
│ │ │ │ ├── settings/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── update-settings.dto.ts
│ │ │ │ │ │ └── update-startup-settings.dto.ts
│ │ │ │ │ ├── settings.controller.ts
│ │ │ │ │ ├── settings.events.ts
│ │ │ │ │ ├── settings.module.ts
│ │ │ │ │ ├── settings.service.spec.ts
│ │ │ │ │ └── settings.service.ts
│ │ │ │ ├── submission/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── apply-multi-submission.dto.ts
│ │ │ │ │ │ ├── apply-template-options.dto.ts
│ │ │ │ │ │ ├── create-submission.dto.ts
│ │ │ │ │ │ ├── reorder-submission-files.dto.ts
│ │ │ │ │ │ ├── reorder-submission.dto.ts
│ │ │ │ │ │ ├── template-option.dto.ts
│ │ │ │ │ │ ├── update-alt-file.dto.ts
│ │ │ │ │ │ ├── update-submission-template-name.dto.ts
│ │ │ │ │ │ └── update-submission.dto.ts
│ │ │ │ │ ├── file-submission.controller.ts
│ │ │ │ │ ├── services/
│ │ │ │ │ │ ├── file-submission.service.ts
│ │ │ │ │ │ ├── message-submission.service.ts
│ │ │ │ │ │ ├── submission-service.interface.ts
│ │ │ │ │ │ ├── submission.service.spec.ts
│ │ │ │ │ │ └── submission.service.ts
│ │ │ │ │ ├── submission.controller.ts
│ │ │ │ │ ├── submission.events.ts
│ │ │ │ │ └── submission.module.ts
│ │ │ │ ├── tag-converters/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── create-tag-converter.dto.ts
│ │ │ │ │ │ └── update-tag-converter.dto.ts
│ │ │ │ │ ├── tag-converter.events.ts
│ │ │ │ │ ├── tag-converters.controller.ts
│ │ │ │ │ ├── tag-converters.module.ts
│ │ │ │ │ ├── tag-converters.service.spec.ts
│ │ │ │ │ └── tag-converters.service.ts
│ │ │ │ ├── tag-groups/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── create-tag-group.dto.ts
│ │ │ │ │ │ └── update-tag-group.dto.ts
│ │ │ │ │ ├── tag-group.events.ts
│ │ │ │ │ ├── tag-groups.controller.ts
│ │ │ │ │ ├── tag-groups.module.ts
│ │ │ │ │ ├── tag-groups.service.spec.ts
│ │ │ │ │ └── tag-groups.service.ts
│ │ │ │ ├── update/
│ │ │ │ │ ├── update.controller.ts
│ │ │ │ │ ├── update.events.ts
│ │ │ │ │ ├── update.module.ts
│ │ │ │ │ └── update.service.ts
│ │ │ │ ├── user-converters/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── create-user-converter.dto.ts
│ │ │ │ │ │ └── update-user-converter.dto.ts
│ │ │ │ │ ├── user-converter.events.ts
│ │ │ │ │ ├── user-converters.controller.ts
│ │ │ │ │ ├── user-converters.module.ts
│ │ │ │ │ ├── user-converters.service.spec.ts
│ │ │ │ │ └── user-converters.service.ts
│ │ │ │ ├── user-specified-website-options/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── create-user-specified-website-options.dto.ts
│ │ │ │ │ │ └── update-user-specified-website-options.dto.ts
│ │ │ │ │ ├── user-specified-website-options.controller.ts
│ │ │ │ │ ├── user-specified-website-options.module.ts
│ │ │ │ │ ├── user-specified-website-options.service.spec.ts
│ │ │ │ │ └── user-specified-website-options.service.ts
│ │ │ │ ├── utils/
│ │ │ │ │ ├── blocknote-to-tiptap.ts
│ │ │ │ │ ├── coerce.util.ts
│ │ │ │ │ ├── filesize.util.ts
│ │ │ │ │ ├── html-parser.util.ts
│ │ │ │ │ ├── select-option.util.ts
│ │ │ │ │ └── wait.util.ts
│ │ │ │ ├── validation/
│ │ │ │ │ ├── validation.module.ts
│ │ │ │ │ ├── validation.service.spec.ts
│ │ │ │ │ ├── validation.service.ts
│ │ │ │ │ └── validators/
│ │ │ │ │ ├── common-field-validators.ts
│ │ │ │ │ ├── datetime-field-validators.ts
│ │ │ │ │ ├── description-validators.ts
│ │ │ │ │ ├── file-submission-validators.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── select-field-validators.ts
│ │ │ │ │ ├── tag-validators.ts
│ │ │ │ │ ├── title-validators.ts
│ │ │ │ │ └── validator.type.ts
│ │ │ │ ├── web-socket/
│ │ │ │ │ ├── models/
│ │ │ │ │ │ └── web-socket-event.ts
│ │ │ │ │ ├── web-socket-adapter.ts
│ │ │ │ │ ├── web-socket-gateway.ts
│ │ │ │ │ ├── web-socket.events.ts
│ │ │ │ │ └── web-socket.module.ts
│ │ │ │ ├── website-options/
│ │ │ │ │ ├── dtos/
│ │ │ │ │ │ ├── create-website-options.dto.ts
│ │ │ │ │ │ ├── preview-description.dto.ts
│ │ │ │ │ │ ├── update-submission-website-options.dto.ts
│ │ │ │ │ │ ├── update-website-options.dto.ts
│ │ │ │ │ │ └── validate-website-options.dto.ts
│ │ │ │ │ ├── website-options.controller.ts
│ │ │ │ │ ├── website-options.module.ts
│ │ │ │ │ ├── website-options.service.spec.ts
│ │ │ │ │ └── website-options.service.ts
│ │ │ │ └── websites/
│ │ │ │ ├── commons/
│ │ │ │ │ ├── post-builder.spec.ts
│ │ │ │ │ ├── post-builder.ts
│ │ │ │ │ ├── validator-passthru.ts
│ │ │ │ │ └── validator.ts
│ │ │ │ ├── decorators/
│ │ │ │ │ ├── disable-ads.decorator.ts
│ │ │ │ │ ├── login-flow.decorator.ts
│ │ │ │ │ ├── supports-files.decorator.ts
│ │ │ │ │ ├── supports-username-shortcut.decorator.ts
│ │ │ │ │ ├── website-decorator-props.ts
│ │ │ │ │ └── website-metadata.decorator.ts
│ │ │ │ ├── dtos/
│ │ │ │ │ └── oauth-website-request.dto.ts
│ │ │ │ ├── implementations/
│ │ │ │ │ ├── artconomy/
│ │ │ │ │ │ ├── artconomy.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── artconomy-account-data.ts
│ │ │ │ │ │ ├── artconomy-file-submission.ts
│ │ │ │ │ │ └── artconomy-message-submission.ts
│ │ │ │ │ ├── aryion/
│ │ │ │ │ │ ├── aryion.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── aryion-account-data.ts
│ │ │ │ │ │ └── aryion-file-submission.ts
│ │ │ │ │ ├── bluesky/
│ │ │ │ │ │ ├── bluesky.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── bluesky-file-submission.ts
│ │ │ │ │ │ └── bluesky-message-submission.ts
│ │ │ │ │ ├── cara/
│ │ │ │ │ │ ├── cara.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── cara-account-data.ts
│ │ │ │ │ │ ├── cara-file-submission.ts
│ │ │ │ │ │ └── cara-message-submission.ts
│ │ │ │ │ ├── custom/
│ │ │ │ │ │ ├── custom.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── custom-file-submission.ts
│ │ │ │ │ │ └── custom-message-submission.ts
│ │ │ │ │ ├── default/
│ │ │ │ │ │ └── default.website.ts
│ │ │ │ │ ├── derpibooru/
│ │ │ │ │ │ ├── derpibooru.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ └── derpibooru-file-submission.ts
│ │ │ │ │ ├── deviant-art/
│ │ │ │ │ │ ├── deviant-art-description-converter.ts
│ │ │ │ │ │ ├── deviant-art.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── deviant-art-account-data.ts
│ │ │ │ │ │ ├── deviant-art-file-submission.ts
│ │ │ │ │ │ └── deviant-art-message-submission.ts
│ │ │ │ │ ├── discord/
│ │ │ │ │ │ ├── discord.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── discord-file-submission.ts
│ │ │ │ │ │ └── discord-message-submission.ts
│ │ │ │ │ ├── e621/
│ │ │ │ │ │ ├── e621.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ └── e621-file-submission.ts
│ │ │ │ │ ├── firefish/
│ │ │ │ │ │ └── firefish.website.ts
│ │ │ │ │ ├── friendica/
│ │ │ │ │ │ └── friendica.website.ts
│ │ │ │ │ ├── fur-affinity/
│ │ │ │ │ │ ├── fur-affinity.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── fur-affinity-account-data.ts
│ │ │ │ │ │ ├── fur-affinity-categories.ts
│ │ │ │ │ │ ├── fur-affinity-file-submission.ts
│ │ │ │ │ │ ├── fur-affinity-message-submission.ts
│ │ │ │ │ │ ├── fur-affinity-species-options.ts
│ │ │ │ │ │ └── fur-affinity-themes.ts
│ │ │ │ │ ├── furbooru/
│ │ │ │ │ │ ├── furbooru.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ └── furbooru-file-submission.ts
│ │ │ │ │ ├── gotosocial/
│ │ │ │ │ │ └── gotosocial.website.ts
│ │ │ │ │ ├── hentai-foundry/
│ │ │ │ │ │ ├── hentai-foundry.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── hentai-foundry-account-data.ts
│ │ │ │ │ │ ├── hentai-foundry-categories.ts
│ │ │ │ │ │ ├── hentai-foundry-file-submission.ts
│ │ │ │ │ │ └── hentai-foundry-message-submission.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── inkbunny/
│ │ │ │ │ │ ├── inkbunny.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ └── inkbunny-file-submission.ts
│ │ │ │ │ ├── instagram/
│ │ │ │ │ │ ├── instagram-api-service/
│ │ │ │ │ │ │ └── instagram-api-service.ts
│ │ │ │ │ │ ├── instagram-blob-service/
│ │ │ │ │ │ │ └── instagram-blob-service.ts
│ │ │ │ │ │ ├── instagram.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ └── instagram-file-submission.ts
│ │ │ │ │ ├── itaku/
│ │ │ │ │ │ ├── itaku.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── itaku-account-data.ts
│ │ │ │ │ │ ├── itaku-file-submission.ts
│ │ │ │ │ │ ├── itaku-message-submission.ts
│ │ │ │ │ │ └── itaku-user-info.ts
│ │ │ │ │ ├── ko-fi/
│ │ │ │ │ │ ├── ko-fi.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── ko-fi-account-data.ts
│ │ │ │ │ │ ├── ko-fi-file-submission.ts
│ │ │ │ │ │ └── ko-fi-message-submission.ts
│ │ │ │ │ ├── manebooru/
│ │ │ │ │ │ ├── manebooru.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ └── manebooru-file-submission.ts
│ │ │ │ │ ├── mastodon/
│ │ │ │ │ │ └── mastodon.website.ts
│ │ │ │ │ ├── megalodon/
│ │ │ │ │ │ ├── megalodon-api-service.ts
│ │ │ │ │ │ ├── megalodon.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── megalodon-file-submission.ts
│ │ │ │ │ │ └── megalodon-message-submission.ts
│ │ │ │ │ ├── misskey/
│ │ │ │ │ │ ├── misskey-api-service.ts
│ │ │ │ │ │ ├── misskey.website.ts
│ │ │ │ │ │ └── models/
│ │ │ │ │ │ ├── misskey-file-submission.ts
│ │ │ │ │ │ └── misskey-message-submission.ts
│ │ │ │ │ ├── newgrounds/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── newgrounds-account-data.ts
│ │ │ │ │ │ │ ├── newgrounds-base-submission.ts
│ │ │ │ │ │ │ ├── newgrounds-file-submission.ts
│ │ │ │ │ │ │ └── newgrounds-message-submission.ts
│ │ │ │ │ │ └── newgrounds.website.ts
│ │ │ │ │ ├── patreon/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── patreon-account-data.ts
│ │ │ │ │ │ │ ├── patreon-campaign-types.ts
│ │ │ │ │ │ │ ├── patreon-collection-types.ts
│ │ │ │ │ │ │ ├── patreon-file-submission.ts
│ │ │ │ │ │ │ ├── patreon-media-upload-types.ts
│ │ │ │ │ │ │ ├── patreon-message-submission.ts
│ │ │ │ │ │ │ └── patreon-post-types.ts
│ │ │ │ │ │ ├── patreon-description-converter.ts
│ │ │ │ │ │ └── patreon.website.ts
│ │ │ │ │ ├── philomena/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── philomena-account-data.ts
│ │ │ │ │ │ │ └── philomena-file-submission.ts
│ │ │ │ │ │ └── philomena.website.ts
│ │ │ │ │ ├── picarto/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── picarto-account-data.ts
│ │ │ │ │ │ │ ├── picarto-categories.ts
│ │ │ │ │ │ │ ├── picarto-file-submission.ts
│ │ │ │ │ │ │ └── picarto-software.ts
│ │ │ │ │ │ └── picarto.website.ts
│ │ │ │ │ ├── piczel/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── piczel-account-data.ts
│ │ │ │ │ │ │ └── piczel-file-submission.ts
│ │ │ │ │ │ └── piczel.website.ts
│ │ │ │ │ ├── pillowfort/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── pillowfort-account-data.ts
│ │ │ │ │ │ │ ├── pillowfort-file-submission.ts
│ │ │ │ │ │ │ └── pillowfort-message-submission.ts
│ │ │ │ │ │ └── pillowfort.website.ts
│ │ │ │ │ ├── pixelfed/
│ │ │ │ │ │ └── pixelfed.website.ts
│ │ │ │ │ ├── pixiv/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── pixiv-account-data.ts
│ │ │ │ │ │ │ └── pixiv-file-submission.ts
│ │ │ │ │ │ └── pixiv.website.ts
│ │ │ │ │ ├── pleroma/
│ │ │ │ │ │ └── pleroma.website.ts
│ │ │ │ │ ├── provider.ts
│ │ │ │ │ ├── sofurry/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── sofurry-account-data.ts
│ │ │ │ │ │ │ ├── sofurry-categories.ts
│ │ │ │ │ │ │ └── sofurry-file-submission.ts
│ │ │ │ │ │ └── sofurry.website.ts
│ │ │ │ │ ├── subscribe-star/
│ │ │ │ │ │ ├── base-subscribe-star.website.ts
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── subscribe-star-account-data.ts
│ │ │ │ │ │ │ ├── subscribe-star-file-submission.ts
│ │ │ │ │ │ │ └── subscribe-star-message-submission.ts
│ │ │ │ │ │ ├── subscribe-star-adult.website.ts
│ │ │ │ │ │ └── subscribe-star.website.ts
│ │ │ │ │ ├── telegram/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── telegram-file-submission.ts
│ │ │ │ │ │ │ └── telegram-message-submission.ts
│ │ │ │ │ │ └── telegram.website.ts
│ │ │ │ │ ├── test/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── test-file-submission.ts
│ │ │ │ │ │ │ └── test-message-submission.ts
│ │ │ │ │ │ └── test.website.ts
│ │ │ │ │ ├── toyhouse/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── toyhouse-account-data.ts
│ │ │ │ │ │ │ └── toyhouse-file-submission.ts
│ │ │ │ │ │ └── toyhouse.website.ts
│ │ │ │ │ ├── tumblr/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── tumblr-account-data.ts
│ │ │ │ │ │ │ ├── tumblr-file-submission.ts
│ │ │ │ │ │ │ └── tumblr-message-submission.ts
│ │ │ │ │ │ └── tumblr.website.ts
│ │ │ │ │ ├── twitter/
│ │ │ │ │ │ ├── models/
│ │ │ │ │ │ │ ├── twitter-file-submission.ts
│ │ │ │ │ │ │ └── twitter-message-submission.ts
│ │ │ │ │ │ ├── twitter-api-service/
│ │ │ │ │ │ │ └── twitter-api-service.ts
│ │ │ │ │ │ └── twitter.website.ts
│ │ │ │ │ └── weasyl/
│ │ │ │ │ ├── models/
│ │ │ │ │ │ ├── weasyl-account-data.ts
│ │ │ │ │ │ ├── weasyl-categories.ts
│ │ │ │ │ │ ├── weasyl-file-submission.ts
│ │ │ │ │ │ └── weasyl-message-submission.ts
│ │ │ │ │ └── weasyl.website.ts
│ │ │ │ ├── models/
│ │ │ │ │ ├── base-website-options.spec.ts
│ │ │ │ │ ├── base-website-options.ts
│ │ │ │ │ ├── data-property-accessibility.ts
│ │ │ │ │ ├── default-website-options.ts
│ │ │ │ │ └── website-modifiers/
│ │ │ │ │ ├── file-website.ts
│ │ │ │ │ ├── message-website.ts
│ │ │ │ │ ├── oauth-website.ts
│ │ │ │ │ ├── with-custom-description-parser.ts
│ │ │ │ │ ├── with-dynamic-file-size-limits.ts
│ │ │ │ │ └── with-runtime-description-parser.ts
│ │ │ │ ├── website-data-manager.spec.ts
│ │ │ │ ├── website-data-manager.ts
│ │ │ │ ├── website-registry.service.spec.ts
│ │ │ │ ├── website-registry.service.ts
│ │ │ │ ├── website.events.ts
│ │ │ │ ├── website.spec.ts
│ │ │ │ ├── website.ts
│ │ │ │ ├── websites.controller.ts
│ │ │ │ └── websites.module.ts
│ │ │ ├── assets/
│ │ │ │ ├── .gitkeep
│ │ │ │ └── sharp-worker.js
│ │ │ ├── environments/
│ │ │ │ ├── environment.prod.ts
│ │ │ │ └── environment.ts
│ │ │ ├── main.ts
│ │ │ └── test-files/
│ │ │ └── README.md
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.spec.json
│ ├── postybirb/
│ │ ├── .eslintrc.json
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── app/
│ │ │ │ ├── api/
│ │ │ │ │ └── preload.ts
│ │ │ │ ├── app.ts
│ │ │ │ ├── constants.ts
│ │ │ │ ├── events/
│ │ │ │ │ └── electron.events.ts
│ │ │ │ └── loader/
│ │ │ │ ├── css/
│ │ │ │ │ └── style.css
│ │ │ │ ├── fonts/
│ │ │ │ │ └── Mylodon-Light.otf
│ │ │ │ ├── loader.html
│ │ │ │ └── loader.js
│ │ │ ├── environments/
│ │ │ │ ├── environment.base.ts
│ │ │ │ ├── environment.prod.ts
│ │ │ │ └── environment.ts
│ │ │ ├── main.ts
│ │ │ └── migrations/
│ │ │ ├── 0000_tough_ken_ellis.sql
│ │ │ ├── 0001_noisy_kate_bishop.sql
│ │ │ ├── 0002_pretty_sunfire.sql
│ │ │ ├── 0003_glamorous_power_pack.sql
│ │ │ ├── 0004_fuzzy_rafael_vega.sql
│ │ │ ├── 0005_exotic_nebula.sql
│ │ │ ├── 0006_cooing_songbird.sql
│ │ │ └── meta/
│ │ │ ├── 0000_snapshot.json
│ │ │ ├── 0001_snapshot.json
│ │ │ ├── 0002_snapshot.json
│ │ │ ├── 0003_snapshot.json
│ │ │ ├── 0004_snapshot.json
│ │ │ ├── 0005_snapshot.json
│ │ │ ├── 0006_snapshot.json
│ │ │ └── _journal.json
│ │ ├── tsconfig.app.json
│ │ ├── tsconfig.json
│ │ └── tsconfig.spec.json
│ ├── postybirb-cloud-server/
│ │ ├── .gitignore
│ │ ├── host.json
│ │ ├── local.settings.json
│ │ ├── package.json
│ │ ├── src/
│ │ │ └── functions/
│ │ │ └── upload.ts
│ │ └── tsconfig.json
│ └── postybirb-ui/
│ ├── .eslintrc.json
│ ├── index.html
│ ├── jest.config.ts
│ ├── postcss.config.js
│ ├── project.json
│ ├── src/
│ │ ├── README.md
│ │ ├── api/
│ │ │ ├── account.api.ts
│ │ │ ├── base.api.ts
│ │ │ ├── custom-shortcut.api.ts
│ │ │ ├── directory-watchers.api.ts
│ │ │ ├── file-submission.api.ts
│ │ │ ├── form-generator.api.ts
│ │ │ ├── legacy-database-importer.api.ts
│ │ │ ├── notification.api.ts
│ │ │ ├── post-manager.api.ts
│ │ │ ├── post-queue.api.ts
│ │ │ ├── post.api.ts
│ │ │ ├── remote.api.ts
│ │ │ ├── settings.api.ts
│ │ │ ├── submission.api.ts
│ │ │ ├── tag-converters.api.ts
│ │ │ ├── tag-groups.api.ts
│ │ │ ├── update.api.ts
│ │ │ ├── user-converters.api.ts
│ │ │ ├── user-specified-website-options.api.ts
│ │ │ ├── website-options.api.ts
│ │ │ └── websites.api.ts
│ │ ├── app-insights-ui.ts
│ │ ├── blocknote-locales.d.ts
│ │ ├── components/
│ │ │ ├── confirm-action-modal/
│ │ │ │ ├── confirm-action-modal.tsx
│ │ │ │ └── index.ts
│ │ │ ├── dialogs/
│ │ │ │ └── settings-dialog/
│ │ │ │ ├── sections/
│ │ │ │ │ ├── app-settings-section.tsx
│ │ │ │ │ ├── appearance-settings-section.tsx
│ │ │ │ │ ├── data-settings-section.tsx
│ │ │ │ │ ├── description-settings-section.tsx
│ │ │ │ │ ├── import-settings-section.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── notifications-settings-section.tsx
│ │ │ │ │ ├── remote-settings-section.tsx
│ │ │ │ │ ├── spellchecker-settings-section.tsx
│ │ │ │ │ └── tags-settings-section.tsx
│ │ │ │ ├── settings-dialog.module.css
│ │ │ │ └── settings-dialog.tsx
│ │ │ ├── disclaimer/
│ │ │ │ └── disclaimer.tsx
│ │ │ ├── drawers/
│ │ │ │ ├── converter-drawer/
│ │ │ │ │ ├── converter-drawer.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── custom-shortcuts-drawer/
│ │ │ │ │ ├── custom-shortcuts-drawer.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── drawers.tsx
│ │ │ │ ├── file-watcher-drawer/
│ │ │ │ │ ├── file-watcher-drawer.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── notifications-drawer/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── notifications-drawer.tsx
│ │ │ │ ├── schedule-drawer/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── schedule-calendar.tsx
│ │ │ │ │ ├── schedule-drawer.css
│ │ │ │ │ ├── schedule-drawer.tsx
│ │ │ │ │ └── submission-list.tsx
│ │ │ │ ├── section-drawer.tsx
│ │ │ │ ├── tag-converter-drawer/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── tag-converter-drawer.tsx
│ │ │ │ ├── tag-group-drawer/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── tag-group-drawer.tsx
│ │ │ │ └── user-converter-drawer/
│ │ │ │ ├── index.ts
│ │ │ │ └── user-converter-drawer.tsx
│ │ │ ├── empty-state/
│ │ │ │ ├── empty-state.tsx
│ │ │ │ └── index.ts
│ │ │ ├── error-boundary/
│ │ │ │ ├── error-boundary.tsx
│ │ │ │ ├── index.ts
│ │ │ │ └── specialized-error-boundaries.tsx
│ │ │ ├── hold-to-confirm/
│ │ │ │ ├── hold-to-confirm.tsx
│ │ │ │ └── index.ts
│ │ │ ├── language-picker/
│ │ │ │ ├── index.ts
│ │ │ │ ├── language-picker.css
│ │ │ │ └── language-picker.tsx
│ │ │ ├── layout/
│ │ │ │ ├── content-area.tsx
│ │ │ │ ├── content-navbar.tsx
│ │ │ │ ├── layout.tsx
│ │ │ │ ├── primary-content.tsx
│ │ │ │ ├── section-panel.tsx
│ │ │ │ └── side-nav.tsx
│ │ │ ├── onboarding-tour/
│ │ │ │ ├── index.ts
│ │ │ │ ├── mantine-tooltip.tsx
│ │ │ │ ├── tour-provider.tsx
│ │ │ │ └── tours/
│ │ │ │ ├── accounts-tour.tsx
│ │ │ │ ├── custom-shortcuts-tour.tsx
│ │ │ │ ├── file-watchers-tour.tsx
│ │ │ │ ├── home-tour.tsx
│ │ │ │ ├── layout-tour.tsx
│ │ │ │ ├── notifications-tour.tsx
│ │ │ │ ├── schedule-tour.tsx
│ │ │ │ ├── submission-edit-tour.tsx
│ │ │ │ ├── submissions-tour.tsx
│ │ │ │ ├── tag-converters-tour.tsx
│ │ │ │ ├── tag-groups-tour.tsx
│ │ │ │ ├── templates-tour.tsx
│ │ │ │ └── user-converters-tour.tsx
│ │ │ ├── sections/
│ │ │ │ ├── accounts-section/
│ │ │ │ │ ├── account-section-header.tsx
│ │ │ │ │ ├── accounts-content.tsx
│ │ │ │ │ ├── accounts-section.tsx
│ │ │ │ │ ├── context/
│ │ │ │ │ │ ├── accounts-context.tsx
│ │ │ │ │ │ └── index.ts
│ │ │ │ │ ├── custom-login-placeholder.tsx
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── use-account-actions.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── login-webview.tsx
│ │ │ │ │ ├── website-account-card.tsx
│ │ │ │ │ ├── website-visibility-picker.tsx
│ │ │ │ │ └── webview-tag.ts
│ │ │ │ ├── file-submissions-section/
│ │ │ │ │ └── hooks/
│ │ │ │ │ └── use-file-submissions.ts
│ │ │ │ ├── home-section/
│ │ │ │ │ ├── account-health-panel.tsx
│ │ │ │ │ ├── home-content.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── queue-control-card.tsx
│ │ │ │ │ ├── recent-activity-panel.tsx
│ │ │ │ │ ├── schedule-calendar-panel.tsx
│ │ │ │ │ ├── stat-card.tsx
│ │ │ │ │ ├── upcoming-posts-panel.tsx
│ │ │ │ │ └── validation-issues-panel.tsx
│ │ │ │ ├── index.ts
│ │ │ │ ├── submissions-section/
│ │ │ │ │ ├── archived-submission-list.tsx
│ │ │ │ │ ├── context/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── submissions-context.tsx
│ │ │ │ │ ├── file-submission-modal/
│ │ │ │ │ │ ├── file-dropzone.tsx
│ │ │ │ │ │ ├── file-list.tsx
│ │ │ │ │ │ ├── file-preview.tsx
│ │ │ │ │ │ ├── file-submission-modal.css
│ │ │ │ │ │ ├── file-submission-modal.tsx
│ │ │ │ │ │ ├── file-submission-modal.utils.ts
│ │ │ │ │ │ ├── image-editor.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── submission-options.tsx
│ │ │ │ │ ├── hooks/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── use-global-dropzone.ts
│ │ │ │ │ │ ├── use-submission-actions.ts
│ │ │ │ │ │ ├── use-submission-create.ts
│ │ │ │ │ │ ├── use-submission-delete.ts
│ │ │ │ │ │ ├── use-submission-handlers.ts
│ │ │ │ │ │ ├── use-submission-post.ts
│ │ │ │ │ │ ├── use-submission-selection.ts
│ │ │ │ │ │ ├── use-submission-update.ts
│ │ │ │ │ │ └── use-submissions.ts
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── post-confirm-modal/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── post-confirm-modal.tsx
│ │ │ │ │ ├── resume-mode-modal/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ └── resume-mode-modal.tsx
│ │ │ │ │ ├── submission-card/
│ │ │ │ │ │ ├── archived-submission-card.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── sortable-submission-card.tsx
│ │ │ │ │ │ ├── submission-actions.tsx
│ │ │ │ │ │ ├── submission-badges.tsx
│ │ │ │ │ │ ├── submission-card.tsx
│ │ │ │ │ │ ├── submission-quick-edit-actions.tsx
│ │ │ │ │ │ ├── submission-thumbnail.tsx
│ │ │ │ │ │ ├── submission-title.tsx
│ │ │ │ │ │ ├── types.ts
│ │ │ │ │ │ └── utils.ts
│ │ │ │ │ ├── submission-edit-card/
│ │ │ │ │ │ ├── account-selection/
│ │ │ │ │ │ │ ├── account-option-row.tsx
│ │ │ │ │ │ │ ├── account-select.tsx
│ │ │ │ │ │ │ ├── account-selection-form.tsx
│ │ │ │ │ │ │ ├── account-selection.css
│ │ │ │ │ │ │ ├── form/
│ │ │ │ │ │ │ │ ├── fields/
│ │ │ │ │ │ │ │ │ ├── boolean-field.tsx
│ │ │ │ │ │ │ │ │ ├── datetime-field.tsx
│ │ │ │ │ │ │ │ │ ├── description-field.tsx
│ │ │ │ │ │ │ │ │ ├── description-preview-panel.tsx
│ │ │ │ │ │ │ │ │ ├── field-copy-button.tsx
│ │ │ │ │ │ │ │ │ ├── field-label.tsx
│ │ │ │ │ │ │ │ │ ├── field.css
│ │ │ │ │ │ │ │ │ ├── form-field.type.ts
│ │ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ │ ├── input-field.tsx
│ │ │ │ │ │ │ │ │ ├── radio-field.tsx
│ │ │ │ │ │ │ │ │ ├── select-field.tsx
│ │ │ │ │ │ │ │ │ ├── select-utils.ts
│ │ │ │ │ │ │ │ │ ├── tag-field.tsx
│ │ │ │ │ │ │ │ │ └── tree-select.tsx
│ │ │ │ │ │ │ │ ├── form-field.tsx
│ │ │ │ │ │ │ │ ├── form-fields-context.tsx
│ │ │ │ │ │ │ │ ├── hooks/
│ │ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ │ ├── use-default-option.tsx
│ │ │ │ │ │ │ │ │ └── use-validations.tsx
│ │ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ │ ├── save-defaults-popover.tsx
│ │ │ │ │ │ │ │ ├── section-layout.css
│ │ │ │ │ │ │ │ ├── section-layout.tsx
│ │ │ │ │ │ │ │ └── validation-alerts.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── selected-accounts-forms.tsx
│ │ │ │ │ │ ├── actions/
│ │ │ │ │ │ │ ├── apply-template-action.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── save-to-many-action.tsx
│ │ │ │ │ │ │ └── submission-edit-card-actions.tsx
│ │ │ │ │ │ ├── body/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── submission-edit-card-body.tsx
│ │ │ │ │ │ ├── context/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── submission-edit-card-context.tsx
│ │ │ │ │ │ ├── defaults-form/
│ │ │ │ │ │ │ ├── defaults-form.css
│ │ │ │ │ │ │ ├── defaults-form.tsx
│ │ │ │ │ │ │ └── index.ts
│ │ │ │ │ │ ├── file-management/
│ │ │ │ │ │ │ ├── bulk-file-editor.tsx
│ │ │ │ │ │ │ ├── file-actions.tsx
│ │ │ │ │ │ │ ├── file-alt-text-editor.tsx
│ │ │ │ │ │ │ ├── file-management.css
│ │ │ │ │ │ │ ├── file-metadata.tsx
│ │ │ │ │ │ │ ├── file-preview.tsx
│ │ │ │ │ │ │ ├── file-uploader.tsx
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ ├── submission-file-card.tsx
│ │ │ │ │ │ │ ├── submission-file-manager.tsx
│ │ │ │ │ │ │ └── use-submission-accounts.ts
│ │ │ │ │ │ ├── header/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── submission-edit-card-header.tsx
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── schedule-form/
│ │ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ │ └── schedule-form.tsx
│ │ │ │ │ │ ├── submission-edit-card.css
│ │ │ │ │ │ └── submission-edit-card.tsx
│ │ │ │ │ ├── submission-history/
│ │ │ │ │ │ ├── history-utils.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── post-history-content.tsx
│ │ │ │ │ │ └── post-record-card.tsx
│ │ │ │ │ ├── submission-history-drawer.tsx
│ │ │ │ │ ├── submission-list.tsx
│ │ │ │ │ ├── submission-section-header.tsx
│ │ │ │ │ ├── submissions-content.tsx
│ │ │ │ │ ├── submissions-section.css
│ │ │ │ │ ├── submissions-section.tsx
│ │ │ │ │ └── types.ts
│ │ │ │ └── templates-section/
│ │ │ │ ├── index.ts
│ │ │ │ ├── template-card.tsx
│ │ │ │ ├── templates-content.tsx
│ │ │ │ ├── templates-section.css
│ │ │ │ └── templates-section.tsx
│ │ │ ├── shared/
│ │ │ │ ├── account-picker/
│ │ │ │ │ ├── account-picker.css
│ │ │ │ │ ├── account-picker.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── basic-website-select/
│ │ │ │ │ └── basic-website-select.tsx
│ │ │ │ ├── copy-to-clipboard/
│ │ │ │ │ ├── copy-to-clipboard.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── description-editor/
│ │ │ │ │ ├── components/
│ │ │ │ │ │ ├── bubble-toolbar.tsx
│ │ │ │ │ │ ├── description-toolbar.tsx
│ │ │ │ │ │ ├── html-edit-modal.tsx
│ │ │ │ │ │ ├── insert-media-modal.tsx
│ │ │ │ │ │ └── suggestion-menu.tsx
│ │ │ │ │ ├── custom-blocks/
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── shortcut.css
│ │ │ │ │ │ └── website-only-selector.tsx
│ │ │ │ │ ├── description-editor.css
│ │ │ │ │ ├── description-editor.tsx
│ │ │ │ │ ├── extensions/
│ │ │ │ │ │ ├── content-warning-shortcut.tsx
│ │ │ │ │ │ ├── custom-shortcut.tsx
│ │ │ │ │ │ ├── default-shortcut.tsx
│ │ │ │ │ │ ├── indent.ts
│ │ │ │ │ │ ├── index.ts
│ │ │ │ │ │ ├── resizable-image.tsx
│ │ │ │ │ │ ├── tags-shortcut.tsx
│ │ │ │ │ │ ├── title-shortcut.tsx
│ │ │ │ │ │ └── username-shortcut.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── types.ts
│ │ │ │ ├── external-link/
│ │ │ │ │ ├── external-link.tsx
│ │ │ │ │ └── index.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── multi-scheduler-modal/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── multi-scheduler-modal.css
│ │ │ │ │ └── multi-scheduler-modal.tsx
│ │ │ │ ├── rating-input/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── rating-input.tsx
│ │ │ │ ├── reorderable-submission-list/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── reorderable-submission-list.css
│ │ │ │ │ └── reorderable-submission-list.tsx
│ │ │ │ ├── schedule-popover/
│ │ │ │ │ ├── cron-picker.tsx
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── schedule-popover.tsx
│ │ │ │ ├── search-input.tsx
│ │ │ │ ├── simple-tag-input/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── simple-tag-input.tsx
│ │ │ │ ├── submission-picker/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── submission-picker-modal.tsx
│ │ │ │ │ └── submission-picker.tsx
│ │ │ │ └── template-picker/
│ │ │ │ ├── index.ts
│ │ │ │ ├── template-picker-modal.tsx
│ │ │ │ └── template-picker.tsx
│ │ │ ├── theme-picker/
│ │ │ │ ├── index.ts
│ │ │ │ └── theme-picker.tsx
│ │ │ ├── update-button/
│ │ │ │ ├── index.ts
│ │ │ │ ├── update-button.tsx
│ │ │ │ └── update-modal.tsx
│ │ │ ├── website-components/
│ │ │ │ ├── bluesky/
│ │ │ │ │ └── description-preview.tsx
│ │ │ │ ├── e621/
│ │ │ │ │ ├── e621-dtext-renderer.tsx
│ │ │ │ │ └── e621-tag-search-provider.tsx
│ │ │ │ ├── furaffinity/
│ │ │ │ │ ├── furaffinity-bbcode-renderer.tsx
│ │ │ │ │ └── furaffinity-bbcode.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── telegram/
│ │ │ │ │ └── telegram-format-renderer.tsx
│ │ │ │ └── tumblr/
│ │ │ │ └── tumblr-npf-renderer.tsx
│ │ │ └── website-login-views/
│ │ │ ├── bluesky/
│ │ │ │ ├── bluesky-login-view.tsx
│ │ │ │ └── index.ts
│ │ │ ├── custom/
│ │ │ │ ├── custom-login-view.tsx
│ │ │ │ └── index.ts
│ │ │ ├── discord/
│ │ │ │ ├── discord-login-view.tsx
│ │ │ │ └── index.ts
│ │ │ ├── e621/
│ │ │ │ ├── e621-login-view.tsx
│ │ │ │ └── index.ts
│ │ │ ├── helpers.tsx
│ │ │ ├── index.ts
│ │ │ ├── inkbunny/
│ │ │ │ ├── index.ts
│ │ │ │ └── inkbunny-login-view.tsx
│ │ │ ├── instagram/
│ │ │ │ ├── index.ts
│ │ │ │ ├── instagram-login-view.tsx
│ │ │ │ └── instagram-setup-guide.tsx
│ │ │ ├── login-view-container.tsx
│ │ │ ├── megalodon/
│ │ │ │ ├── index.ts
│ │ │ │ └── megalodon-login-view.tsx
│ │ │ ├── misskey/
│ │ │ │ ├── index.ts
│ │ │ │ └── misskey-login-view.tsx
│ │ │ ├── telegram/
│ │ │ │ ├── index.ts
│ │ │ │ └── telegram-login-view.tsx
│ │ │ ├── twitter/
│ │ │ │ ├── index.ts
│ │ │ │ └── twitter-login-view.tsx
│ │ │ └── types.ts
│ │ ├── config/
│ │ │ ├── keybindings.ts
│ │ │ └── nav-items.tsx
│ │ ├── environments/
│ │ │ ├── environment.prod.ts
│ │ │ └── environment.ts
│ │ ├── hooks/
│ │ │ ├── index.ts
│ │ │ ├── tag-search/
│ │ │ │ ├── index.ts
│ │ │ │ ├── tag-search-provider.ts
│ │ │ │ ├── tag-search-providers.ts
│ │ │ │ └── use-tag-search.ts
│ │ │ ├── use-keybindings.ts
│ │ │ └── use-locale.ts
│ │ ├── i18n/
│ │ │ ├── languages.tsx
│ │ │ └── validation-translation.tsx
│ │ ├── index.tsx
│ │ ├── main.tsx
│ │ ├── models/
│ │ │ └── http-error-response.ts
│ │ ├── providers/
│ │ │ └── i18n-provider.tsx
│ │ ├── shared/
│ │ │ └── platform-utils.ts
│ │ ├── stores/
│ │ │ ├── create-entity-store.ts
│ │ │ ├── create-typed-store.ts
│ │ │ ├── entity/
│ │ │ │ ├── account-store.ts
│ │ │ │ ├── custom-shortcut-store.ts
│ │ │ │ ├── directory-watcher-store.ts
│ │ │ │ ├── notification-store.ts
│ │ │ │ ├── settings-store.ts
│ │ │ │ ├── submission-store.ts
│ │ │ │ ├── tag-converter-store.ts
│ │ │ │ ├── tag-group-store.ts
│ │ │ │ ├── user-converter-store.ts
│ │ │ │ └── website-store.ts
│ │ │ ├── index.ts
│ │ │ ├── records/
│ │ │ │ ├── account-record.ts
│ │ │ │ ├── base-record.ts
│ │ │ │ ├── custom-shortcut-record.ts
│ │ │ │ ├── directory-watcher-record.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── notification-record.ts
│ │ │ │ ├── settings-record.ts
│ │ │ │ ├── submission-record.ts
│ │ │ │ ├── tag-converter-record.ts
│ │ │ │ ├── tag-group-record.ts
│ │ │ │ ├── user-converter-record.ts
│ │ │ │ └── website-record.ts
│ │ │ ├── store-init.ts
│ │ │ └── ui/
│ │ │ ├── accounts-ui-store.ts
│ │ │ ├── appearance-store.ts
│ │ │ ├── drawer-store.ts
│ │ │ ├── locale-store.ts
│ │ │ ├── navigation-store.ts
│ │ │ ├── submissions-ui-store.ts
│ │ │ ├── templates-ui-store.ts
│ │ │ └── tour-store.ts
│ │ ├── styles/
│ │ │ └── layout.css
│ │ ├── styles.css
│ │ ├── theme/
│ │ │ ├── css-variable-resolver.ts
│ │ │ ├── theme-styles.css
│ │ │ └── theme.ts
│ │ ├── transports/
│ │ │ ├── http-client.ts
│ │ │ └── websocket.ts
│ │ ├── types/
│ │ │ ├── account-filters.ts
│ │ │ ├── navigation.ts
│ │ │ └── view-state.ts
│ │ └── utils/
│ │ ├── class-names.ts
│ │ ├── environment.ts
│ │ ├── index.ts
│ │ ├── notifications.tsx
│ │ └── open-url.ts
│ ├── tailwind.config.js
│ ├── tsconfig.app.json
│ ├── tsconfig.json
│ ├── tsconfig.spec.json
│ └── vite.config.ts
├── babel.config.js
├── commitlint.config.js
├── compose.yml
├── docs/
│ ├── DOCKER.md
│ ├── POST_QUEUE_FLOWS.md
│ ├── app-insights/
│ │ ├── APP_INSIGHTS_QUERIES.md
│ │ └── APP_INSIGHTS_SETUP.md
│ └── contributing/
│ └── add-a-website/
│ ├── README.md
│ └── sections/
│ ├── authenticate-a-user.md
│ ├── defining-submission-data.md
│ ├── description-parsing.md
│ ├── file-website.md
│ ├── message-website.md
│ └── validation.md
├── drizzle.config.ts
├── electron-builder.yml
├── entrypoint.sh
├── jest.config.ts
├── jest.preset.js
├── jest.reporter.js
├── jest.setup.ts
├── lang/
│ ├── de.po
│ ├── en.po
│ ├── es.po
│ ├── lt.po
│ ├── nl.po
│ ├── pt_BR.po
│ ├── ru.po
│ └── ta.po
├── libs/
│ ├── database/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── database.ts
│ │ │ ├── helper-types.ts
│ │ │ ├── relations/
│ │ │ │ ├── index.ts
│ │ │ │ └── relations.ts
│ │ │ └── schemas/
│ │ │ ├── account.schema.ts
│ │ │ ├── common.schema.ts
│ │ │ ├── custom-shortcut.schema.ts
│ │ │ ├── directory-watcher.schema.ts
│ │ │ ├── file-buffer.schema.ts
│ │ │ ├── index.ts
│ │ │ ├── notification.schema.ts
│ │ │ ├── post-event.schema.ts
│ │ │ ├── post-queue-record.schema.ts
│ │ │ ├── post-record.schema.ts
│ │ │ ├── settings.schema.ts
│ │ │ ├── submission-file.schema.ts
│ │ │ ├── submission.schema.ts
│ │ │ ├── tag-converter.schema.ts
│ │ │ ├── tag-group.schema.ts
│ │ │ ├── user-converter.schema.ts
│ │ │ ├── user-specified-website-options.schema.ts
│ │ │ ├── website-data.schema.ts
│ │ │ └── website-options.schema.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ └── tsconfig.spec.json
│ ├── form-builder/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── constants.ts
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── decorators/
│ │ │ │ ├── boolean-field.decorator.ts
│ │ │ │ ├── date-time-field.decorator.ts
│ │ │ │ ├── description-field.decorator.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── radio-field.decorator.ts
│ │ │ │ ├── rating.decorator.ts
│ │ │ │ ├── select-field.decorator.ts
│ │ │ │ ├── tag-field.decorator.ts
│ │ │ │ ├── text-field.decorator.ts
│ │ │ │ └── title-field.decorator.ts
│ │ │ ├── form-builder.spec.ts
│ │ │ ├── form-builder.ts
│ │ │ ├── types/
│ │ │ │ ├── field-aggregate.ts
│ │ │ │ ├── field.ts
│ │ │ │ ├── form-builder-metadata.ts
│ │ │ │ ├── index.ts
│ │ │ │ └── primitive-record.ts
│ │ │ └── utils/
│ │ │ └── assign-metadata.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ └── tsconfig.spec.json
│ ├── fs/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── directories.ts
│ │ │ ├── fs.spec.ts
│ │ │ └── fs.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ └── tsconfig.spec.json
│ ├── http/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── form-file.ts
│ │ │ ├── http.spec.ts
│ │ │ ├── http.ts
│ │ │ └── proxy.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ └── tsconfig.spec.json
│ ├── logger/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── app-insights.ts
│ │ │ ├── logger.ts
│ │ │ ├── serialize-log.ts
│ │ │ └── winston-appinsights-transport.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ └── tsconfig.spec.json
│ ├── socket-events/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ └── socket-events.ts
│ │ ├── tsconfig.json
│ │ └── tsconfig.lib.json
│ ├── translations/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ └── field-translations.ts
│ │ ├── tsconfig.json
│ │ └── tsconfig.lib.json
│ ├── types/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── dtos/
│ │ │ │ ├── account/
│ │ │ │ │ ├── account.dto.ts
│ │ │ │ │ ├── create-account.dto.ts
│ │ │ │ │ └── update-account.dto.ts
│ │ │ │ ├── custom-shortcut/
│ │ │ │ │ ├── create-custom-shortcut.dto.ts
│ │ │ │ │ ├── custom-shortcut.dto.ts
│ │ │ │ │ └── update-custom-shortcut.dto.ts
│ │ │ │ ├── database/
│ │ │ │ │ └── entity.dto.ts
│ │ │ │ ├── directory-watcher/
│ │ │ │ │ ├── create-directory-watcher.dto.ts
│ │ │ │ │ ├── directory-watcher.dto.ts
│ │ │ │ │ └── update-directory-watcher.dto.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── notification/
│ │ │ │ │ ├── create-notification.dto.ts
│ │ │ │ │ └── update-notification.dto.ts
│ │ │ │ ├── post/
│ │ │ │ │ ├── post-event.dto.ts
│ │ │ │ │ ├── post-queue-action.dto.ts
│ │ │ │ │ ├── post-queue-record.dto.ts
│ │ │ │ │ ├── post-record.dto.ts
│ │ │ │ │ └── queue-post-record-request.dto.ts
│ │ │ │ ├── settings/
│ │ │ │ │ ├── settings.dto.ts
│ │ │ │ │ └── update-settings.dto.ts
│ │ │ │ ├── submission/
│ │ │ │ │ ├── apply-multi-submission.dto.ts
│ │ │ │ │ ├── create-submission.dto.ts
│ │ │ │ │ ├── file-buffer.dto.ts
│ │ │ │ │ ├── reorder-submission-files.dto.ts
│ │ │ │ │ ├── submission-file.dto.ts
│ │ │ │ │ ├── submission.dto.ts
│ │ │ │ │ ├── update-alt-file.dto.ts
│ │ │ │ │ ├── update-submission-template-name.dto.ts
│ │ │ │ │ └── update-submission.dto.ts
│ │ │ │ ├── tag/
│ │ │ │ │ ├── create-tag-converter.dto.ts
│ │ │ │ │ ├── create-tag-group.dto.ts
│ │ │ │ │ ├── tag-converter.dto.ts
│ │ │ │ │ ├── tag-group.dto.ts
│ │ │ │ │ ├── update-tag-converter.dto.ts
│ │ │ │ │ └── update-tag-group.dto.ts
│ │ │ │ ├── user/
│ │ │ │ │ ├── create-user-converter.dto.ts
│ │ │ │ │ ├── update-user-converter.dto.ts
│ │ │ │ │ └── user-converter.dto.ts
│ │ │ │ ├── website/
│ │ │ │ │ ├── custom-website-route.dto.ts
│ │ │ │ │ ├── form-generation-request.dto.ts
│ │ │ │ │ ├── oauth-website-request.dto.ts
│ │ │ │ │ ├── set-website-data-request.dto.ts
│ │ │ │ │ ├── website-data.dto.ts
│ │ │ │ │ └── website-info.dto.ts
│ │ │ │ └── website-options/
│ │ │ │ ├── create-user-specified-website-options.dto.ts
│ │ │ │ ├── create-website-options.dto.ts
│ │ │ │ ├── preview-description.dto.ts
│ │ │ │ ├── update-submission-website-options.dto.ts
│ │ │ │ ├── update-user-specified-website-options.dto.ts
│ │ │ │ ├── update-website-options.dto.ts
│ │ │ │ ├── user-specified-website-options.dto.ts
│ │ │ │ ├── validate-website-options.dto.ts
│ │ │ │ └── website-options.dto.ts
│ │ │ ├── enums/
│ │ │ │ ├── description-types.enum.ts
│ │ │ │ ├── directory-watcher-import-action.enum.ts
│ │ │ │ ├── file-type.enum.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── post-event-type.enum.ts
│ │ │ │ ├── post-record-resume-mode.enum.ts
│ │ │ │ ├── post-record-state.enum.ts
│ │ │ │ ├── schedule-type.enum.ts
│ │ │ │ ├── submission-rating.enum.ts
│ │ │ │ └── submission-type.enum.ts
│ │ │ ├── index.ts
│ │ │ ├── models/
│ │ │ │ ├── account/
│ │ │ │ │ └── account.interface.ts
│ │ │ │ ├── common/
│ │ │ │ │ └── dynamic-object.ts
│ │ │ │ ├── custom-shortcut/
│ │ │ │ │ └── custom-shortcut.interface.ts
│ │ │ │ ├── database/
│ │ │ │ │ ├── entity-primitive.type.ts
│ │ │ │ │ └── entity.interface.ts
│ │ │ │ ├── directory-watcher/
│ │ │ │ │ └── directory-watcher.interface.ts
│ │ │ │ ├── file/
│ │ │ │ │ ├── file-buffer.interface.ts
│ │ │ │ │ └── file-dimensions.interface.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── notification/
│ │ │ │ │ └── notification.interface.ts
│ │ │ │ ├── post/
│ │ │ │ │ ├── post-event.interface.ts
│ │ │ │ │ ├── post-queue-record.interface.ts
│ │ │ │ │ ├── post-record.interface.ts
│ │ │ │ │ └── post-response.type.ts
│ │ │ │ ├── remote/
│ │ │ │ │ └── update-cookies-remote.type.ts
│ │ │ │ ├── settings/
│ │ │ │ │ ├── settings-options.interface.ts
│ │ │ │ │ ├── settings.constants.ts
│ │ │ │ │ └── settings.interface.ts
│ │ │ │ ├── submission/
│ │ │ │ │ ├── default-submission-file-props.ts
│ │ │ │ │ ├── description-value.type.ts
│ │ │ │ │ ├── file-submission/
│ │ │ │ │ │ ├── file-submission-metadata.type.ts
│ │ │ │ │ │ ├── file-submission.ts
│ │ │ │ │ │ └── modified-file-dimension.type.ts
│ │ │ │ │ ├── message-submission/
│ │ │ │ │ │ └── message-submission.type.ts
│ │ │ │ │ ├── npf-description.type.ts
│ │ │ │ │ ├── submission-file-props.interface.ts
│ │ │ │ │ ├── submission-file.interface.ts
│ │ │ │ │ ├── submission-metadata.interface.ts
│ │ │ │ │ ├── submission-metadata.type.ts
│ │ │ │ │ ├── submission-schedule-info.interface.ts
│ │ │ │ │ ├── submission.interface.ts
│ │ │ │ │ ├── validation-result.type.ts
│ │ │ │ │ └── website-form-fields.interface.ts
│ │ │ │ ├── tag/
│ │ │ │ │ ├── default-tag-value.ts
│ │ │ │ │ ├── tag-converter.interface.ts
│ │ │ │ │ ├── tag-group.interface.ts
│ │ │ │ │ ├── tag-value.type.ts
│ │ │ │ │ └── tag.type.ts
│ │ │ │ ├── update/
│ │ │ │ │ └── update.type.ts
│ │ │ │ ├── user/
│ │ │ │ │ └── user-converter.interface.ts
│ │ │ │ ├── website/
│ │ │ │ │ ├── file-website-form-fields.interface.ts
│ │ │ │ │ ├── folder.type.ts
│ │ │ │ │ ├── image-resize-props.ts
│ │ │ │ │ ├── login-request-data.type.ts
│ │ │ │ │ ├── login-response.interface.ts
│ │ │ │ │ ├── login-state.class.ts
│ │ │ │ │ ├── login-state.interface.ts
│ │ │ │ │ ├── post-data.type.ts
│ │ │ │ │ ├── website-data.interface.ts
│ │ │ │ │ ├── website-info.interface.ts
│ │ │ │ │ ├── website-login-type.ts
│ │ │ │ │ └── website.type.ts
│ │ │ │ └── website-options/
│ │ │ │ ├── user-specified-website-options.interface.ts
│ │ │ │ └── website-options.interface.ts
│ │ │ ├── website-modifiers/
│ │ │ │ ├── index.ts
│ │ │ │ ├── oauth-routes.ts
│ │ │ │ ├── username-shortcut.ts
│ │ │ │ ├── website-file-options.ts
│ │ │ │ └── website-metadata.ts
│ │ │ └── website-public/
│ │ │ ├── README.md
│ │ │ ├── bluesky-account-data.ts
│ │ │ ├── custom-account-data.ts
│ │ │ ├── discord-account-data.ts
│ │ │ ├── e621-account-data.ts
│ │ │ ├── index.ts
│ │ │ ├── inkbunny-account-data.ts
│ │ │ ├── instagram-account-data.ts
│ │ │ ├── megalodon-account-data.ts
│ │ │ ├── misskey-account-data.ts
│ │ │ ├── telegram-account-data.ts
│ │ │ └── twitter-account-data.ts
│ │ ├── tsconfig.json
│ │ └── tsconfig.lib.json
│ └── utils/
│ ├── electron/
│ │ ├── .eslintrc.json
│ │ ├── README.md
│ │ ├── jest.config.ts
│ │ ├── project.json
│ │ ├── src/
│ │ │ ├── index.ts
│ │ │ └── lib/
│ │ │ ├── browser-window-utils.ts
│ │ │ ├── postybirb-env-config.ts
│ │ │ ├── remote-utils.ts
│ │ │ ├── startup-options-electron.ts
│ │ │ ├── utils-electron.spec.ts
│ │ │ ├── utils-electron.ts
│ │ │ └── utils-test.ts
│ │ ├── tsconfig.json
│ │ ├── tsconfig.lib.json
│ │ └── tsconfig.spec.json
│ └── file-type/
│ ├── .eslintrc.json
│ ├── README.md
│ ├── project.json
│ ├── src/
│ │ ├── index.ts
│ │ └── lib/
│ │ ├── calculate-image-resize.ts
│ │ ├── get-file-type.ts
│ │ ├── is-audio.ts
│ │ ├── is-image.ts
│ │ ├── is-text.ts
│ │ ├── is-video.ts
│ │ └── mime-helper.ts
│ ├── tsconfig.json
│ └── tsconfig.lib.json
├── lingui.config.ts
├── nx.json
├── package.json
├── packaging-resources/
│ ├── entitlements.mac.plist
│ ├── icons/
│ │ └── icon.icns
│ └── installer.nsh
├── scripts/
│ ├── add-website/
│ │ ├── create-file-from-template.js
│ │ ├── create-website.js
│ │ ├── parse-add-website-input.js
│ │ └── templates/
│ │ ├── account-data.hbs
│ │ ├── file-submission.hbs
│ │ ├── message-submission.hbs
│ │ └── website.hbs
│ ├── add-website.js
│ ├── inject-app-insights.js
│ ├── package.json
│ ├── tsconfig.json
│ └── windows-signing/
│ ├── hasher.cjs
│ └── pull-and-sign.ps1
└── tsconfig.base.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false
================================================
FILE: .eslintignore
================================================
# Add files here to ignore them in eslint
node_modules
*.js
*.jsx
*.spec.ts
*.spec.tsx
================================================
FILE: .eslintrc.js
================================================
// @ts-check
/** @type {import('eslint').ESLint.ConfigData} */
const config = {
root: true,
ignorePatterns: ['**/*'],
extends: [
'airbnb',
'airbnb-typescript',
'plugin:jest/recommended',
'plugin:@nrwl/nx/typescript',
'eslint-config-prettier',
],
plugins: ['@nrwl/nx', 'jest'],
parserOptions: { project: './tsconfig.base.json' },
overrides: [
{
files: ['*.tsx'],
plugins: ['testing-library'],
extends: ['plugin:testing-library/react', 'airbnb/hooks'],
},
],
rules: {
'@nrwl/nx/enforce-module-boundaries': [
'error',
{
enforceBuildableLibDependency: true,
// We allow it because we need to use import from
// postybirb-ui to make jump-to definition in the
// FieldType.label
allow: [
'@postybirb/form-builder',
'@postybirb/translations',
'@postybirb/types',
],
depConstraints: [{ sourceTag: '*', onlyDependOnLibsWithTags: ['*'] }],
},
],
'@typescript-eslint/lines-between-class-members': 'off',
'no-plusplus': 'off',
'no-nested-ternary': 'off',
'no-continue': 'off',
'no-await-in-loop': 'off',
'no-restricted-syntax': 'off',
'class-methods-use-this': 'off',
'import/export': 'off',
'import/no-cycle': 'off',
'import/prefer-default-export': 'off',
'react/jsx-props-no-spreading': 'off',
'react/react-in-jsx-scope': 'off',
'react/no-unescaped-entities': 'off',
'react/sort-comp': 'off',
'react/prop-types': 'off',
'@typescript-eslint/ban-types': 'warn',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-use-before-define': 'off',
'import/extension': 'off',
'import/no-extraneous-dependencies': [
'warn',
{
// These dependencies will be considered
// as bundled and no warning will be shown
bundledDependencies: ['electron'],
},
],
'@typescript-eslint/naming-convention': [
'error',
{
selector: 'variable',
format: ['camelCase', 'PascalCase', 'UPPER_CASE'],
// Allow const { _ } = useLingui()
leadingUnderscore: 'allow',
},
{
selector: 'function',
format: ['camelCase', 'PascalCase'],
},
{
selector: 'typeLike',
format: ['PascalCase'],
},
],
},
};
module.exports = config;
================================================
FILE: .github/workflows/build.yml
================================================
name: Build/Release
on:
workflow_dispatch:
jobs:
build:
name: ${{ matrix.name }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: macOS (x64)
os: macos-latest
platform: mac
arch: x64
- name: macOS (arm64)
os: macos-latest
platform: mac
arch: arm64
- name: Linux (x64)
os: ubuntu-latest
platform: linux
arch: x64
- name: Linux (arm64)
os: ubuntu-24.04-arm
platform: linux
arch: arm64
- name: Windows (x64)
os: windows-latest
platform: win
arch: x64
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "22"
- name: Enable corepack to use Yarn v4
run: |
corepack enable
corepack prepare --activate
- name: Cache Electron binaries
uses: actions/cache@v4
with:
path: |
~/.cache/electron
~/.cache/electron-builder
node_modules/.cache/electron
key: ${{ runner.os }}-${{ matrix.arch }}-electron-cache-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.arch }}-electron-cache-
- name: Install dependencies
run: yarn install --immutable
env:
CYPRESS_INSTALL_BINARY: 0
ELECTRON_CACHE: ~/.cache/electron
ELECTRON_BUILDER_CACHE: ~/.cache/electron-builder
# This is needed to prevent ids showing instead of non translated i18n texts in production builds
- name: Extract messages
run: yarn lingui:extract
- name: Patch electron-builder.yml for fork repositories
if: github.event.repository.fork
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const yaml = require('js-yaml');
const configPath = './electron-builder.yml';
const configContent = fs.readFileSync(configPath, 'utf8');
let config = yaml.load(configContent);
config.publish = {
provider: 'github',
owner: context.repo.owner,
repo: context.repo.repo
};
if (config.linux && config.linux.target) {
config.linux.target = [{ target:"AppImage" }]
}
// Remove snap configuration entirely for forks
delete config.snap;
// Remove deb and rpm configurations for forks
delete config.deb;
delete config.rpm;
// Write the modified config back
fs.writeFileSync(configPath, yaml.dump(config));
console.log('✅ Patched electron-builder.yml for fork repository');
console.log(`📦 Publish target: ${context.repo.owner}/${context.repo.repo}`);
console.log(yaml.dump(config))
- name: Install snapcraft
if: matrix.platform == 'linux' && !github.event.repository.fork
run: |
sudo apt-get update
sudo apt-get install -y snapd
sudo snap install snapcraft --classic
- name: Install Ruby + fpm (system) for Linux packaging
if: matrix.platform == 'linux' && !github.event.repository.fork
run: |
sudo apt-get update
sudo apt-get install -y ruby ruby-dev build-essential rpm
sudo gem install --no-document fpm
# Ensure gem bindir is on PATH for subsequent steps
echo "$(ruby -e 'print Gem.bindir')" >> "$GITHUB_PATH"
echo "Ruby:"; ruby -v
echo "fpm:"; fpm --version
- name: Inject Application Insights connection string
env:
APP_INSIGHTS_KEY: ${{ secrets.APP_INSIGHTS_KEY }}
run: corepack yarn inject:app-insights
- name: Build application
run: corepack yarn build:prod
env:
NODE_ENV: production
- name: Build Electron app (fork)
if: github.event.repository.fork
run: corepack yarn electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish=always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Electron app
if: (!github.event.repository.fork)
run: corepack yarn electron-builder --${{ matrix.platform }} --${{ matrix.arch }} --publish=always
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
CSC_IDENTITY_AUTO_DISCOVERY: true
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
SNAPCRAFT_BUILD_ENVIRONMENT: host
# ✅ Use system fpm for Linux only
USE_SYSTEM_FPM: ${{ matrix.platform == 'linux' && 'true' || '' }}
- name: Extract version from package.json for docker tag
uses: actions/github-script@v8
id: packageVersion
with:
script: |
const fs = require('fs');
const pkg = JSON.parse(fs.readFileSync("package.json", "utf8"))
return pkg.version
- name: Build and publish a Docker image for '${{github.repository}}'
uses: macbre/push-to-ghcr@master
if: matrix.platform == 'linux' && matrix.arch == 'x64'
with:
image_name: "${{github.repository}}"
extra_args: --tag ghcr.io/${{github.repository}}:${{steps.packageVersion.outputs.result}}
github_token: ${{ secrets.GITHUB_TOKEN }}
dockerfile: "./Dockerfile"
================================================
FILE: .github/workflows/ci.yml
================================================
name: CI
on:
workflow_dispatch:
pull_request:
push:
branches:
- main
# Needed for nx-set-shas when run on the main branch
permissions:
actions: read
contents: read
jobs:
ci:
name: ${{ matrix.ci.name }}
runs-on: ubuntu-latest
if: github.head_ref != 'weblate-postybirb-postybirb'
strategy:
matrix:
ci:
- name: Lint
command: "yarn nx affected -t lint --cache --cache-strategy content
--cache-location .eslint"
- name: TypeCheck
command: "yarn nx affected -t typecheck"
# xvfb-run is needed because electron still tries to get $DISPLAY env even if it does not launches the window
# so to run tests on electron runner we need to add this command
# grep filters out useless spam in console about not able to access display.
# pipefail needed because otherwise grep returns exit code 0 and failed tests don't display in ci as failed
- name: Test
command: "set -o pipefail && xvfb-run --auto-servernum --server-args=\"-screen 0
1280x960x24\" -- yarn nx affected -t test | grep -v -E
\"ERROR:viz_main_impl\\.cc\\(183\\)|ERROR:object_proxy\\.cc\\(576\
\\)|ERROR:bus\\.cc\\(408\\)|ERROR:gles2_cmd_decoder_passthrough\\\
.cc\\(1094\\)|ERROR:gl_utils\\.cc\\(431\\)|ERROR:browser_main_loop\
\\.cc\\(276\\)\""
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
# We need to fetch all branches and commits so that Nx affected has a base to compare against.
fetch-depth: 0
- name: Use eslint, jest and tsc cache
uses: actions/cache@v4
with:
# out-tsc needed because we use incremental in typecheck which uses files from tsc output
path: |
.eslint
.jest
dist/out-tsc
key: ${{ runner.os }}-ci
# This is needed to be run before node setup, because node setup uses yarn to get cache dir
- name: Enable corepack to use Yarn v4
run: corepack enable
- name: Install Node.js and Yarn
uses: actions/setup-node@v4
with:
node-version: "22"
cache: yarn
# These flags needed to speed up ci
- name: Install dependencies
run: yarn install --immutable
env:
CYPRESS_INSTALL_BINARY: 0 # Skip downloading Cypress binary (not needed for CI)
# Used to calculate affected
- name: Setup SHAs
uses: nrwl/nx-set-shas@v4
- name: Run ${{ matrix.ci.name }}
run: ${{ matrix.ci.command }}
timeout-minutes: 30
================================================
FILE: .github/workflows/i18n.yml.disabled
================================================
name: i18n CI
on:
push:
branches:
- main
permissions:
actions: read
contents: write
checks: write
pull-requests: write
jobs:
main:
runs-on: ubuntu-latest
if: github.head_ref != 'weblate-postybirb-postybirb'
steps:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v4
with:
cache: yarn
# Note: These flags needed to speed up ci
- name: Install dependencies
run: yarn install --frozen-lockfile --prefer-offline
# Extracts i18n strings from code
- name: Extract i18n keys
run: yarn lingui:extract
# It commits extracted i18n string and eslint/prettier fixes
- name: Commit changes
run: |
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
# pull only from main branch so it will not fetch all tags
git pull origin main
git add lang/*
# ignore when nothing to commit
yarn exitzero "git commit -m 'ci: extract i18n messages'"
- name: Push changes
uses: ad-m/github-push-action@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: ${{ github.ref }}
================================================
FILE: .gitignore
================================================
# See http://help.github.com/ignore-files/ for more about ignoring files.
# yarn
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
# compiled output
/release
/dist
/tmp
/out-tsc
/test
/.swc
/.jest
/.eslint
.jest
.swc
# dependencies
/node_modules
scripts/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
yarn-error.log
testem.log
/typings
# System Files
.DS_Store
Thumbs.db
# Public UI dependencies
.nx/cache
.nx/workspace-data
================================================
FILE: .husky/commit-msg
================================================
npx --no-install commitlint --edit "$1"
================================================
FILE: .husky/post-merge
================================================
yarn
================================================
FILE: .prettierignore
================================================
# Add files here to ignore them from prettier formatting
/dist/**
/coverage/**
/.nx/cache/**
/.nx/workspace-data/**
*.hbs
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true,
"endOfLine": "crlf"
}
================================================
FILE: .vscode/extensions.json
================================================
{
"recommendations": [
"nrwl.angular-console",
"esbenp.prettier-vscode",
"firsttris.vscode-jest-runner",
"dbaeumer.vscode-eslint"
]
}
================================================
FILE: .vscode/launch.json
================================================
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug Node App",
"type": "node",
"request": "attach",
"port": 9229
},
{
"name": "Debug Jest Tests",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--runInBand",
"--no-coverage",
"--no-cache",
"${relativeFile}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
},
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/**/*.js",
"!**/node_modules/**"
],
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
],
"env": {
"NODE_ENV": "test"
}
},
{
"name": "Debug Current Jest Test",
"type": "node",
"request": "launch",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--runInBand",
"--no-coverage",
"--no-cache",
"--testNamePattern=${input:testNamePattern}",
"${relativeFile}"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",
"disableOptimisticBPs": true,
"windows": {
"program": "${workspaceFolder}/node_modules/jest/bin/jest"
},
"sourceMaps": true,
"outFiles": [
"${workspaceFolder}/**/*.js",
"!**/node_modules/**"
],
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
],
"env": {
"NODE_ENV": "test"
}
}
],
"inputs": [
{
"id": "testNamePattern",
"description": "Test name pattern",
"default": ".*",
"type": "promptString"
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"editor.codeActionsOnSave": {
"source.organizeImports": "always"
}
}
================================================
FILE: .yarn/patches/@handlewithcare-prosemirror-inputrules-npm-0.1.3-897e37b56f.patch
================================================
diff --git a/package.json b/package.json
index 20bc41090c166b7b27756b8d8e0fb67525d48f38..a539a8e91499f831bd77b4fa43e98f3970ea57e8 100644
--- a/package.json
+++ b/package.json
@@ -9,6 +9,7 @@
"exports": {
".": {
"types": "./dist/index.d.ts",
+ "require": "./dist/index.js",
"import": "./dist/index.js"
}
},
================================================
FILE: .yarn/patches/@tiptap-html-npm-3.15.3-a9641901db.patch
================================================
diff --git a/package.json b/package.json
index 1d1aa65a70e7f3e898be608610693a9b98152b8c..e27af24e756e7c6b76651d504304f28e7de4e084 100644
--- a/package.json
+++ b/package.json
@@ -12,28 +12,6 @@
"type": "github",
"url": "https://github.com/sponsors/ueberdosis"
},
- "exports": {
- ".": {
- "types": {
- "import": "./dist/index.d.ts",
- "require": "./dist/index.d.cts"
- },
- "import": {
- "browser": "./dist/index.js",
- "node": "./dist/server/index.js",
- "default": "./dist/index.js"
- },
- "require": "./dist/index.cjs"
- },
- "./server": {
- "types": {
- "import": "./dist/server/index.d.ts",
- "require": "./dist/server/index.d.cts"
- },
- "import": "./dist/server/index.js",
- "require": "./dist/server/index.cjs"
- }
- },
"main": "dist/index.cjs",
"module": "dist/index.js",
"types": "dist/index.d.ts",
================================================
FILE: .yarn/patches/jest-snapshot-npm-29.7.0-15ef0a4ad6.patch
================================================
diff --git a/build/InlineSnapshots.js b/build/InlineSnapshots.js
index 3481ad99885c847156afdef148d3075dcc9c68ca..44c91da106e6111f95c75b37bc41e5cadebcc145 100644
--- a/build/InlineSnapshots.js
+++ b/build/InlineSnapshots.js
@@ -149,6 +149,7 @@ const saveSnapshotsForFile = (snapshots, sourceFilePath, rootDir, prettier) => {
filename: sourceFilePath,
plugins,
presets,
+ configFile: path.join(process.cwd(),'./babel.config.js'),
root: rootDir
});
} catch (error) {
================================================
FILE: .yarn/patches/strong-log-transformer-npm-2.1.0-45addd9278.patch
================================================
diff --git a/lib/logger.js b/lib/logger.js
index 69218a870e6022289a73c27cb2d4f166bf1691d9..8d6554bce9dd8892c945ef0740b0e9d6ba56798c 100644
--- a/lib/logger.js
+++ b/lib/logger.js
@@ -29,7 +29,7 @@ var formatters = {
function Logger(options) {
var defaults = JSON.parse(JSON.stringify(Logger.DEFAULTS));
- options = util._extend(defaults, options || {});
+ options = Object.assign(defaults, options || {});
var catcher = deLiner();
var emitter = catcher;
var transforms = [
================================================
FILE: .yarnrc.yml
================================================
logFilters:
# NX packages peer dependencies make a lot of warnings that does not affect code in any way
- code: YN0086
level: discard
# This warning doesn't make sense. This package is not referenced in code at all and wasn't updated in 2 years
- code: YN0002
level: discard
text: doesn't provide @mantine/utils (p2df71b), requested by @blocknote/mantine.
nodeLinker: node-modules
supportedArchitectures:
cpu:
- current
- x64
- arm64
================================================
FILE: Dockerfile
================================================
# Multi stage build to make image smaller
FROM node:24-bookworm-slim AS builder
# For ca-certificates
RUN apt-get update && apt-get install -y curl
WORKDIR /source
COPY . .
# Conditional build - only build if release/linux-unpacked doesn't exist
RUN if [ -d "./release/linux-unpacked" ]; then \
echo "Found existing build, copying..."; \
cp -r ./release/linux-unpacked/ /app;\
else \
echo "Building from source..."; rm -rf .nx && \
CYPRESS_INSTALL_BINARY=0 corepack yarn install --inline-builds && \
corepack yarn dist:linux --dir && \
cp -r ./release/linux-unpacked/ /app;\
fi
FROM node:24-bookworm-slim
COPY --from=builder /app /app
# Install dependencies for Electron and headless display
RUN apt-get update && apt-get install -y \
libgtk-3-0 \
libnss3 \
libasound2 \
libxss1 \
libgbm-dev \
libxshmfence-dev \
libdrm-dev \
# For ca-certificates and healthcheck
curl \
xvfb \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Contains database, submissions, tags etc
VOLUME [ "/root/PostyBirb" ]
# Contains startup options, remote config, partitions etc
VOLUME [ "/root/.config/postybirb" ]
ENV DISPLAY=:99
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=5 \
CMD curl http://127.0.0.1:8080 || [ $? -eq 52 ] && exit 0 || exit 1
COPY ./entrypoint.sh .
ENTRYPOINT [ "bash" ]
CMD [ "entrypoint.sh" ]
================================================
FILE: LICENSE
================================================
BSD 3-Clause License
Copyright (c) 2024, Michael DiCarlo
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
================================================
FILE: README.md
================================================
# Postybirb
<div style='flex: 1'>
<a href="https://discord.com/invite/FUdN7JCr2f">
<img alt="Static Badge" src="https://img.shields.io/badge/discord-%2323272a?logo=discord">
</a>
<a href="https://github.com/mvdicarlo/postybirb/releases/latest">
<img alt="GitHub Downloads (all assets, latest release)" src="https://img.shields.io/github/downloads/mvdicarlo/postybirb/latest/total">
</a>
<a href="https://hosted.weblate.org/engage/postybirb/">
<img src="https://hosted.weblate.org/widget/postybirb/svg-badge.svg" alt="Translation status" />
</a>
<img alt="GitHub Actions Workflow Status" src="https://img.shields.io/github/actions/workflow/status/mvdicarlo/postybirb/build.yml">
</div>
## About
PostyBirb is an application that helps artists post art and other multimedia to
multiple websites more quickly.The overall goal of PostyBirb is to cut down on
the time it takes to post submissions to multiple websites.
## V4 Initiative
v4 sets out to be more flexible for adding new features and updates not easily
supported on v3. It also aims to be more contributor friendly and ease the
implementation of websites where possible.
## Looking for v3 (PostyBirb+)?
You can find v3 [here](https://github.com/mvdicarlo/postybirb-plus).
## Translation

PostyBirb uses [Weblate](https://hosted.weblate.org/projects/postybirb/postybirb/) as transltion service
Learn more: [Translation guide](./TRANSLATION.md)
## Project Setup
1. Ensure your NodeJS version is 24.6.0 or higher
2. Clone project using git
3. `corepack enable` Make NodeJS use the yarn version specific to the project (from package.json)
4. `yarn install` Installs dependencies
5. If it fails with `➤ YN0009: │ better-sqlite3@npm:11.8.0 couldn't be built successfully`: <summary>
<details>
If you're on windows run
```
winget install -e --id Microsoft.VisualStudio.2022.BuildTools --override "--passive --wait --add Microsoft.VisualStudio.Workload.VCTools;includeRecommended"
```
If you're on linux or other OS please create an issue with log from the unsucessfull build. It will have instructions on which packages are required and we will add them there. But generally it should work out of box if you have C++ compiler installed
</details>
</summary>
6. `yarn run setup` Installs hooks/husky
7. `yarn start` Starts app
### Recommended Plugins (VSCode)
- Nx Console
- Jest Runner
- Prettier
## Contributing
Please write clean code.
Follow [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/)
To add new website [see this guide](./contributing/add-a-website)
## Primary Modules (/apps)
### Client-Server
The "Back end" of the application. This houses all data models, user settings,
posting logic, etc.
#### Primary Technologies Used
- NestJS
- Drizzle (sqlite3)
### Postybirb
The Electron part of the application that contains initialization logic and
app setup.
#### Primary Technologies Used
- Electron
### PostyBirb-UI
The user interface for the application that talks with Client-Server through
web-socket and https.
#### Primary Technologies Used
- React
- Blocknote/TipTap (Text editor)
---
This project was generated using [Nx](https://nx.dev).
================================================
FILE: TRANSLATION.md
================================================
# Translation guide
## How to contribute to translation
Go to [PostyBirb site](https://hosted.weblate.org/projects/postybirb/postybirb/), create account and suggest translation!
## Add new language
To add new language you need to add [language 2-letter code](https://www.loc.gov/standards/iso639-2/php/code_list.php) to these files:
- [lingui.config.ts](./lingui.config.ts)
- [languages.tsx](./apps/postybirb-ui/src/app/languages.tsx)
================================================
FILE: apps/client-server/.eslintrc.json
================================================
{
"extends": ["../../.eslintrc.js"],
"ignorePatterns": ["!**/*"],
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"rules": {}
},
{
"files": ["*.ts", "*.tsx"],
"rules": {}
},
{
"files": ["*.js", "*.jsx"],
"rules": {}
}
]
}
================================================
FILE: apps/client-server/jest.config.ts
================================================
/* eslint-disable */
export default {
displayName: 'client-server',
preset: '../../jest.preset.js',
globals: {},
testEnvironment: 'node',
moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/client-server',
runner: '@kayahr/jest-electron-runner/main',
};
================================================
FILE: apps/client-server/project.json
================================================
{
"name": "client-server",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "apps/client-server/src",
"projectType": "application",
"targets": {
"build": {
"executor": "nx-electron:build",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/client-server",
"main": "apps/client-server/src/main.ts",
"tsConfig": "apps/client-server/tsconfig.app.json",
"assets": ["apps/client-server/src/assets"]
},
"configurations": {
"production": {
"optimization": true,
"extractLicenses": true,
"inspect": false,
"fileReplacements": [
{
"replace": "apps/client-server/src/environments/environment.ts",
"with": "apps/client-server/src/environments/environment.prod.ts"
}
]
}
}
},
"serve": {
"executor": "@nx/js:node",
"options": {
"buildTarget": "client-server:build",
"inspect": true,
"port": 9229
}
},
"lint": {
"executor": "@nx/eslint:lint",
"outputs": ["{options.outputFile}"]
},
"test": {
"executor": "@nx/jest:jest",
"outputs": ["{workspaceRoot}/coverage/apps/client-server"],
"options": {
"jestConfig": "apps/client-server/jest.config.ts"
}
},
"typecheck": {
"executor": "nx:run-commands",
"options": {
"command": "tsc -b {projectRoot}/tsconfig.json --incremental --pretty"
}
}
},
"tags": []
}
================================================
FILE: apps/client-server/src/app/account/account.controller.ts
================================================
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiTags,
} from '@nestjs/swagger';
import { AccountId } from '@postybirb/types';
import { PostyBirbController } from '../common/controller/postybirb-controller';
import { AccountService } from './account.service';
import { CreateAccountDto } from './dtos/create-account.dto';
import { SetWebsiteDataRequestDto } from './dtos/set-website-data-request.dto';
import { UpdateAccountDto } from './dtos/update-account.dto';
/**
* CRUD operations on Account data.
* @class AccountController
*/
@ApiTags('account')
@Controller('account')
export class AccountController extends PostyBirbController<'AccountSchema'> {
constructor(readonly service: AccountService) {
super(service);
}
@Post()
@ApiOkResponse({ description: 'Account created.' })
@ApiBadRequestResponse({ description: 'Bad request made.' })
create(@Body() createAccountDto: CreateAccountDto) {
return this.service
.create(createAccountDto)
.then((account) => account.toDTO());
}
@Post('/clear/:id')
@ApiOkResponse({ description: 'Account data cleared.' })
@ApiBadRequestResponse({ description: 'Bad request made.' })
async clear(@Param('id') id: AccountId) {
await this.service.clearAccountData(id);
try {
this.service.manuallyExecuteOnLogin(id);
} catch {
// For some reason throws error that crashes app when deleting account
}
}
@Get('/refresh/:id')
@ApiOkResponse({ description: 'Account login check queued.' })
async refresh(@Param('id') id: AccountId) {
this.service.manuallyExecuteOnLogin(id);
}
@Patch(':id')
@ApiOkResponse({ description: 'Account updated.', type: Boolean })
@ApiNotFoundResponse({ description: 'Account Id not found.' })
update(
@Body() updateAccountDto: UpdateAccountDto,
@Param('id') id: AccountId,
) {
return this.service
.update(id, updateAccountDto)
.then((account) => account.toDTO());
}
@Post('/account-data')
@ApiOkResponse({ description: 'Account data set.' })
setWebsiteData(@Body() oauthRequestDto: SetWebsiteDataRequestDto) {
return this.service.setAccountData(oauthRequestDto);
}
}
================================================
FILE: apps/client-server/src/app/account/account.events.ts
================================================
import { ACCOUNT_UPDATES } from '@postybirb/socket-events';
import { IAccountDto } from '@postybirb/types';
import { WebsocketEvent } from '../web-socket/models/web-socket-event';
export type AccountEventTypes = AccountUpdateEvent;
class AccountUpdateEvent implements WebsocketEvent<IAccountDto[]> {
event: string = ACCOUNT_UPDATES;
data: IAccountDto[];
}
================================================
FILE: apps/client-server/src/app/account/account.module.ts
================================================
import { Module } from '@nestjs/common';
import { WebsitesModule } from '../websites/websites.module';
import { AccountController } from './account.controller';
import { AccountService } from './account.service';
@Module({
imports: [WebsitesModule],
providers: [AccountService],
controllers: [AccountController],
exports: [AccountService],
})
export class AccountModule {}
================================================
FILE: apps/client-server/src/app/account/account.service.spec.ts
================================================
import { Test, TestingModule } from '@nestjs/testing';
import { clearDatabase } from '@postybirb/database';
import { NULL_ACCOUNT_ID } from '@postybirb/types';
import { Account } from '../drizzle/models';
import { waitUntil } from '../utils/wait.util';
import { WebsiteImplProvider } from '../websites/implementations/provider';
import { WebsiteRegistryService } from '../websites/website-registry.service';
import { AccountService } from './account.service';
import { CreateAccountDto } from './dtos/create-account.dto';
describe('AccountsService', () => {
let service: AccountService;
let registryService: WebsiteRegistryService;
let module: TestingModule;
// Mock objects for deleteUnregisteredAccounts tests
let mockRepository: any;
let mockWebsiteRegistry: any;
let mockLogger: any;
const mockRegisteredAccount = {
id: 'account-1',
name: 'Test Account 1',
website: 'registered-website',
withWebsiteInstance(websiteInstance) {
return this;
},
toDTO: () => {},
} as Account;
const mockUnregisteredAccount = {
id: 'account-2',
name: 'Test Account 2',
website: 'unregistered-website',
withWebsiteInstance(websiteInstance) {
return this;
},
toDTO: () => {},
} as Account;
const mockAnotherUnregisteredAccount = {
id: 'account-3',
name: 'Test Account 3',
website: 'another-unregistered-website',
withWebsiteInstance(websiteInstance) {
return this;
},
toDTO: () => {},
} as Account;
beforeEach(async () => {
clearDatabase();
module = await Test.createTestingModule({
providers: [AccountService, WebsiteRegistryService, WebsiteImplProvider],
}).compile();
service = module.get<AccountService>(AccountService);
registryService = module.get<WebsiteRegistryService>(
WebsiteRegistryService,
);
await service.onModuleInit();
});
afterAll(async () => {
await module.close();
});
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should set and clear account data', async () => {
const dto = new CreateAccountDto();
dto.groups = ['test'];
dto.name = 'test';
dto.website = 'test';
const record = await service.create(dto);
const instance = registryService.findInstance(record);
expect(instance).toBeDefined();
await instance.login();
const websiteData = instance.getWebsiteData();
expect(websiteData).toEqual({
test: 'test-mode',
});
await service.setAccountData({
id: record.id,
data: { test: 'test-mode-2' },
});
expect(instance.getWebsiteData()).toEqual({
test: 'test-mode-2',
});
await service.clearAccountData(record.id);
expect(instance.getWebsiteData()).toEqual({});
}, 10000);
it('should create entities', async () => {
const dto = new CreateAccountDto();
dto.groups = ['test'];
dto.name = 'test';
dto.website = 'test';
const record = await service.create(dto);
expect(registryService.findInstance(record)).toBeDefined();
const groups = await service.findAll();
await waitUntil(() => !record.websiteInstance?.getLoginState().pending, 50);
expect(groups).toHaveLength(1);
expect(groups[0].name).toEqual(dto.name);
expect(groups[0].website).toEqual(dto.website);
expect(groups[0].groups).toEqual(dto.groups);
const recordDto = record.toDTO();
expect(recordDto).toEqual({
groups: dto.groups,
name: dto.name,
website: dto.website,
id: record.id,
createdAt: record.createdAt,
updatedAt: record.updatedAt,
state: {
isLoggedIn: true,
pending: false,
username: 'TestUser',
lastUpdated: expect.any(String),
},
data: {
test: 'test-mode',
},
websiteInfo: {
supports: ['MESSAGE', 'FILE'],
websiteDisplayName: 'Test',
},
});
}, 10000);
it('should support crud operations', async () => {
const createAccount: CreateAccountDto = new CreateAccountDto();
createAccount.name = 'test';
createAccount.website = 'test';
// Create
const account = await service.create(createAccount);
expect(account).toBeDefined();
expect(await service.findAll()).toHaveLength(1);
expect(await service.findById(account.id)).toBeDefined();
// Update
await service.update(account.id, { name: 'Updated', groups: [] });
expect(await (await service.findById(account.id)).name).toEqual('Updated');
// Remove
await service.remove(account.id);
expect(await service.findAll()).toHaveLength(0);
});
describe('deleteUnregisteredAccounts', () => {
beforeEach(() => {
// Setup mock objects for testing private method
mockRepository = {
find: jest.fn(),
deleteById: jest.fn(),
schemaEntity: { id: 'id' },
};
mockWebsiteRegistry = {
canCreate: jest.fn(),
create: jest.fn(),
findInstance: jest.fn(),
getAvailableWebsites: () => [],
markAsInitialized: jest.fn(),
emit: jest.fn(),
};
mockLogger = {
withMetadata: jest.fn().mockReturnThis(),
withError: jest.fn().mockReturnThis(),
warn: jest.fn(),
error: jest.fn(),
};
// Replace service dependencies with mocks
(service as any).repository = mockRepository;
(service as any).websiteRegistry = mockWebsiteRegistry;
(service as any).logger = mockLogger;
// Setup default mock behavior
mockRepository.find.mockResolvedValue([
mockRegisteredAccount,
mockUnregisteredAccount,
mockAnotherUnregisteredAccount,
]);
mockWebsiteRegistry.canCreate.mockImplementation((website: string) => {
return website === 'registered-website';
});
mockRepository.deleteById.mockResolvedValue(undefined);
});
it('should delete accounts for unregistered websites', async () => {
await (service as any).deleteUnregisteredAccounts();
// Verify that find was called to get all accounts except NULL_ACCOUNT_ID
expect(mockRepository.find).toHaveBeenCalledWith({
where: expect.any(Object), // ne(schemaEntity.id, NULL_ACCOUNT_ID)
});
// Verify canCreate was called for each account's website
expect(mockWebsiteRegistry.canCreate).toHaveBeenCalledWith(
'registered-website',
);
expect(mockWebsiteRegistry.canCreate).toHaveBeenCalledWith(
'unregistered-website',
);
expect(mockWebsiteRegistry.canCreate).toHaveBeenCalledWith(
'another-unregistered-website',
);
expect(mockWebsiteRegistry.canCreate).toHaveBeenCalledTimes(3);
// Verify deleteById was called for unregistered accounts only
expect(mockRepository.deleteById).toHaveBeenCalledWith(['account-2']);
expect(mockRepository.deleteById).toHaveBeenCalledWith(['account-3']);
expect(mockRepository.deleteById).toHaveBeenCalledTimes(2);
// Verify logging
expect(mockLogger.withMetadata).toHaveBeenCalledWith(
mockUnregisteredAccount,
);
expect(mockLogger.withMetadata).toHaveBeenCalledWith(
mockAnotherUnregisteredAccount,
);
expect(mockLogger.warn).toHaveBeenCalledWith(
'Deleting unregistered account: account-2 (Test Account 2)',
);
expect(mockLogger.warn).toHaveBeenCalledWith(
'Deleting unregistered account: account-3 (Test Account 3)',
);
});
it('should not delete accounts for registered websites', async () => {
await (service as any).deleteUnregisteredAccounts();
// Verify the registered account was not deleted
expect(mockRepository.deleteById).not.toHaveBeenCalledWith(['account-1']);
});
it('should handle deletion errors gracefully', async () => {
const deleteError = new Error('Database deletion failed');
mockRepository.deleteById
.mockResolvedValueOnce(undefined) // First deletion succeeds
.mockRejectedValueOnce(deleteError); // Second deletion fails
await (service as any).deleteUnregisteredAccounts();
// Verify both deletions were attempted
expect(mockRepository.deleteById).toHaveBeenCalledTimes(2);
// Verify error was logged for the failed deletion
expect(mockLogger.withError).toHaveBeenCalledWith(deleteError);
expect(mockLogger.error).toHaveBeenCalledWith(
'Failed to delete unregistered account: account-3',
);
});
it('should handle empty accounts list', async () => {
mockRepository.find.mockResolvedValue([]);
await (service as any).deleteUnregisteredAccounts();
expect(mockWebsiteRegistry.canCreate).not.toHaveBeenCalled();
expect(mockRepository.deleteById).not.toHaveBeenCalled();
expect(mockLogger.warn).not.toHaveBeenCalled();
});
it('should handle case where all accounts are registered', async () => {
mockRepository.find.mockResolvedValue([mockRegisteredAccount]);
await (service as any).deleteUnregisteredAccounts();
expect(mockWebsiteRegistry.canCreate).toHaveBeenCalledWith(
'registered-website',
);
expect(mockRepository.deleteById).not.toHaveBeenCalled();
expect(mockLogger.warn).not.toHaveBeenCalled();
});
it('should exclude NULL_ACCOUNT_ID from deletion consideration', async () => {
const nullAccount = {
id: NULL_ACCOUNT_ID,
name: 'Null Account',
website: 'null',
} as Account;
// Mock the repository.find to only return non-NULL accounts (simulating the database query)
// The actual service uses ne(this.repository.schemaEntity.id, NULL_ACCOUNT_ID) to exclude it
mockRepository.find.mockResolvedValue([
mockUnregisteredAccount, // Only return the unregistered account, not the null account
]);
// Even if null website is not registered, it shouldn't be considered for deletion
mockWebsiteRegistry.canCreate.mockImplementation((website: string) => {
return website !== 'null' && website !== 'unregistered-website';
});
await (service as any).deleteUnregisteredAccounts();
// Verify the query excludes NULL_ACCOUNT_ID (this is tested by the repository mock)
expect(mockRepository.find).toHaveBeenCalledWith({
where: expect.any(Object),
});
// Only the unregistered account should be deleted, not the null account
expect(mockRepository.deleteById).toHaveBeenCalledWith(['account-2']);
expect(mockRepository.deleteById).toHaveBeenCalledTimes(1);
});
});
});
================================================
FILE: apps/client-server/src/app/account/account.service.ts
================================================
import {
BadRequestException,
Injectable,
OnModuleInit,
Optional,
} from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { ACCOUNT_UPDATES } from '@postybirb/socket-events';
import {
AccountId,
IWebsiteMetadata,
NULL_ACCOUNT_ID,
NullAccount,
} from '@postybirb/types';
import { IsTestEnvironment } from '@postybirb/utils/electron';
import { ne } from 'drizzle-orm';
import { Class } from 'type-fest';
import { PostyBirbService } from '../common/service/postybirb-service';
import { Account } from '../drizzle/models';
import { FindOptions } from '../drizzle/postybirb-database/find-options.type';
import { WSGateway } from '../web-socket/web-socket-gateway';
import { UnknownWebsite } from '../websites/website';
import { WebsiteRegistryService } from '../websites/website-registry.service';
import { CreateAccountDto } from './dtos/create-account.dto';
import { SetWebsiteDataRequestDto } from './dtos/set-website-data-request.dto';
import { UpdateAccountDto } from './dtos/update-account.dto';
import { LoginStatePoller } from './login-state-poller';
/**
* Service responsible for returning Account data.
* Also stores login refresh timers for initiating login checks.
*/
@Injectable()
export class AccountService
extends PostyBirbService<'AccountSchema'>
implements OnModuleInit
{
private readonly loginRefreshTimers: Record<
string,
{
timer: NodeJS.Timeout;
websites: Class<UnknownWebsite>[];
}
> = {};
private readonly loginStatePoller: LoginStatePoller;
constructor(
private readonly websiteRegistry: WebsiteRegistryService,
@Optional() webSocket?: WSGateway,
) {
super('AccountSchema', webSocket);
this.repository.subscribe('AccountSchema', () => this.emit());
this.loginStatePoller = new LoginStatePoller(
this.websiteRegistry,
() => {
this.emit();
this.websiteRegistry.emit();
},
);
}
/**
* Initializes all website login timers and creates instances for known accounts.
* Heavy operations are deferred to avoid blocking application startup.
*/
async onModuleInit() {
// Critical path: only populate null account to ensure database is ready
await this.populateNullAccount();
// Defer heavy operations to avoid blocking NestJS initialization
setImmediate(async () => {
await this.deleteUnregisteredAccounts();
await this.initWebsiteRegistry();
this.websiteRegistry.markAsInitialized();
this.initWebsiteLoginRefreshTimers();
this.emit();
Object.keys(this.loginRefreshTimers).forEach((interval) =>
this.executeOnLoginForInterval(interval),
);
});
}
/**
* CRON-driven poll for login state changes.
* Compares cached login states against live values and emits to UI on change.
*/
@Cron(CronExpression.EVERY_SECOND)
private pollLoginStates() {
if (!IsTestEnvironment()) {
this.loginStatePoller.checkForChanges();
}
}
private async deleteUnregisteredAccounts() {
const accounts = await this.repository.find({
where: ne(this.repository.schemaEntity.id, NULL_ACCOUNT_ID),
});
const unregisteredAccounts = accounts.filter(
(account) => !this.websiteRegistry.canCreate(account.website),
);
for (const account of unregisteredAccounts) {
try {
this.logger
.withMetadata(account)
.warn(
`Deleting unregistered account: ${account.id} (${account.name})`,
);
await this.repository.deleteById([account.id]);
} catch (err) {
this.logger
.withError(err)
.withMetadata(account)
.error(`Failed to delete unregistered account: ${account.id}`);
}
}
}
/**
* Create the Nullable typed account.
*/
private async populateNullAccount(): Promise<void> {
if (!(await this.repository.findById(NULL_ACCOUNT_ID))) {
await this.repository.insert(new NullAccount());
}
}
/**
* Loads accounts into website registry.
*/
private async initWebsiteRegistry(): Promise<void> {
const accounts = await this.repository.find({
where: ne(this.repository.schemaEntity.id, NULL_ACCOUNT_ID),
});
await Promise.all(
accounts.map((account) => this.websiteRegistry.create(account)),
).catch((err) => {
this.logger.error(err, 'onModuleInit');
});
}
/**
* Creates website login check timers.
*/
private initWebsiteLoginRefreshTimers(): void {
const availableWebsites = this.websiteRegistry.getAvailableWebsites();
availableWebsites.forEach((website) => {
const interval: number =
(website.prototype.decoratedProps.metadata as IWebsiteMetadata)
.refreshInterval ?? 60_000 * 60;
if (!this.loginRefreshTimers[interval]) {
this.loginRefreshTimers[interval] = {
websites: [],
timer: setInterval(() => {
this.executeOnLoginForInterval(interval);
}, interval),
};
}
this.loginRefreshTimers[interval].websites.push(website);
});
}
public async emit() {
const dtos = await this.findAll().then((accounts) =>
accounts.map((a) => this.injectWebsiteInstance(a)),
);
super.emit({
event: ACCOUNT_UPDATES,
data: dtos.map((dto) => dto.toDTO()),
});
}
/**
* Runs login on all created website instances within a specific interval.
* The mutex inside website.login() ensures only one login runs at a time
* per instance; concurrent callers simply wait and get the fresh state.
*
* @param {string} interval
*/
private async executeOnLoginForInterval(interval: string | number) {
const { websites } = this.loginRefreshTimers[interval];
websites.forEach((website) => {
this.websiteRegistry.getInstancesOf(website).forEach((instance) => {
// Fire-and-forget — the poller will detect state changes
instance.login().catch((e) => {
this.logger.withError(e).error(`Login failed for ${instance.id}`);
});
});
});
}
/**
* Logic that needs to be run after an account is created.
*
* @param {Account} account
* @param {UnknownWebsite} website
*/
private afterCreate(account: Account, website: UnknownWebsite) {
// Fire-and-forget — poller picks up the state change
website.login().catch((e) => {
this.logger.withError(e).error(`Initial login failed for ${website.id}`);
});
}
/**
* Executes a login refresh initiated from an external source.
* Waits for the result so the caller knows when it's done.
*
* @param {AccountId} id
*/
async manuallyExecuteOnLogin(id: AccountId): Promise<void> {
const account = await this.findById(id);
if (account) {
const instance = this.websiteRegistry.findInstance(account);
if (instance) {
await instance.login();
// Force an immediate UI update for this instance
this.loginStatePoller.checkInstance(instance);
}
}
}
/**
* Creates an Account.
* @param {CreateAccountDto} createDto
* @return {*} {Promise<Account>}
*/
async create(createDto: CreateAccountDto): Promise<Account> {
this.logger
.withMetadata(createDto)
.info(`Creating Account '${createDto.name}:${createDto.website}`);
if (!this.websiteRegistry.canCreate(createDto.website)) {
throw new BadRequestException(
`Website ${createDto.website} is not supported.`,
);
}
const account = await this.repository.insert(new Account(createDto));
const instance = await this.websiteRegistry.create(account);
this.afterCreate(account, instance);
return account.withWebsiteInstance(instance);
}
public findById(id: AccountId, options?: FindOptions) {
return this.repository
.findById(id, options)
.then((account) => this.injectWebsiteInstance(account));
}
public async findAll() {
return this.repository
.find({
where: ne(this.repository.schemaEntity.id, NULL_ACCOUNT_ID),
})
.then((accounts) =>
accounts.map((account) => this.injectWebsiteInstance(account)),
);
}
async update(id: AccountId, update: UpdateAccountDto) {
this.logger.withMetadata(update).info(`Updating Account '${id}'`);
return this.repository
.update(id, update)
.then((account) => this.injectWebsiteInstance(account));
}
async remove(id: AccountId) {
const account = await this.findById(id);
if (account) {
this.websiteRegistry.remove(account);
}
return super.remove(id);
}
/**
* Clears the data and login state associated with an account.
*
* @param {string} id
*/
async clearAccountData(id: AccountId) {
this.logger.info(`Clearing Account data for '${id}'`);
const account = await this.findById(id);
if (account) {
const instance = this.websiteRegistry.findInstance(account);
await instance.clearLoginStateAndData();
}
}
/**
* Sets the data saved to an account's website.
*
* @param {SetWebsiteDataRequestDto} setWebsiteDataRequestDto
*/
async setAccountData(setWebsiteDataRequestDto: SetWebsiteDataRequestDto) {
this.logger.info(
`Setting Account data for '${setWebsiteDataRequestDto.id}'`,
);
const account = await this.repository.findById(
setWebsiteDataRequestDto.id,
{ failOnMissing: true },
);
const instance = this.websiteRegistry.findInstance(account);
await instance.setWebsiteData(setWebsiteDataRequestDto.data);
}
private injectWebsiteInstance(account?: Account): Account | null {
if (!account) {
return null;
}
return account.withWebsiteInstance(
this.websiteRegistry.findInstance(account),
);
}
}
================================================
FILE: apps/client-server/src/app/account/dtos/create-account.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { ICreateAccountDto } from '@postybirb/types';
import { IsArray, IsString, Length } from 'class-validator';
/**
* Account creation request object.
*/
export class CreateAccountDto implements ICreateAccountDto {
@ApiProperty()
@IsString()
@Length(1, 64)
name: string;
@ApiProperty()
@IsString()
@Length(1)
website: string;
@ApiProperty()
@IsArray()
groups: string[];
}
================================================
FILE: apps/client-server/src/app/account/dtos/set-website-data-request.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import {
AccountId,
DynamicObject,
ISetWebsiteDataRequestDto,
} from '@postybirb/types';
import { IsObject, IsString } from 'class-validator';
export class SetWebsiteDataRequestDto
implements ISetWebsiteDataRequestDto<DynamicObject>
{
@ApiProperty()
@IsString()
id: AccountId;
@ApiProperty({
type: Object,
})
@IsObject()
data: DynamicObject;
}
================================================
FILE: apps/client-server/src/app/account/dtos/update-account.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { IUpdateAccountDto } from '@postybirb/types';
import { IsArray, IsString } from 'class-validator';
/**
* Account update request object.
*/
export class UpdateAccountDto implements IUpdateAccountDto {
@ApiProperty()
@IsString()
name: string;
@ApiProperty()
@IsArray()
groups: string[];
}
================================================
FILE: apps/client-server/src/app/account/login-state-poller.ts
================================================
import { Logger } from '@postybirb/logger';
import { AccountId, ILoginState } from '@postybirb/types';
import { UnknownWebsite } from '../websites/website';
import { WebsiteRegistryService } from '../websites/website-registry.service';
/**
* Compares all website instances' login states against a cached snapshot
* and triggers a callback when any state has changed.
*
* Designed to be driven externally (e.g. by a @Cron job) rather than
* managing its own polling interval.
*/
export class LoginStatePoller {
private readonly logger = Logger(LoginStatePoller.name);
/**
* Cached snapshot of the last-known login state per account.
* Used for diffing to detect changes.
*/
private lastKnownStates: Record<AccountId, ILoginState> = {};
constructor(
private readonly websiteRegistry: WebsiteRegistryService,
private readonly onStateChange: () => void,
) {}
/**
* Compare current login states against the cached snapshot.
* If any account's state has changed, update the cache and fire the callback.
*/
checkForChanges(): void {
try {
const instances = this.websiteRegistry.getAll();
let hasChanged = false;
const currentStates: Record<AccountId, ILoginState> = {};
for (const instance of instances) {
const { accountId } = instance;
const state = instance.getLoginState();
currentStates[accountId] = state;
const previous = this.lastKnownStates[accountId];
if (!previous || !this.statesEqual(previous, state)) {
hasChanged = true;
}
}
// Detect removed accounts
for (const accountId of Object.keys(this.lastKnownStates)) {
if (!(accountId in currentStates)) {
hasChanged = true;
}
}
if (hasChanged) {
this.lastKnownStates = currentStates;
this.onStateChange();
}
} catch (e) {
this.logger.withError(e).error('Error during login state poll');
}
}
/**
* Check a single website instance for login state changes.
* More efficient than checkForChanges() when you know exactly which instance changed.
*
* @param {UnknownWebsite} instance - The website instance to check
*/
checkInstance(instance: UnknownWebsite): void {
try {
const { accountId } = instance;
const state = instance.getLoginState();
const previous = this.lastKnownStates[accountId];
if (!previous || !this.statesEqual(previous, state)) {
this.lastKnownStates[accountId] = state;
this.onStateChange();
}
} catch (e) {
this.logger.withError(e).error('Error during single instance login state check');
}
}
/**
* Shallow equality check for two ILoginState objects.
*/
private statesEqual(a: ILoginState, b: ILoginState): boolean {
return (
a.isLoggedIn === b.isLoggedIn &&
a.pending === b.pending &&
a.username === b.username &&
a.lastUpdated === b.lastUpdated
);
}
}
================================================
FILE: apps/client-server/src/app/app.controller.ts
================================================
import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Get()
getData() {
return this.appService.getData();
}
}
================================================
FILE: apps/client-server/src/app/app.module.ts
================================================
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { ScheduleModule } from '@nestjs/schedule';
import { ServeStaticModule } from '@nestjs/serve-static';
import { join } from 'path';
import { AccountModule } from './account/account.module';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { CustomShortcutsModule } from './custom-shortcuts/custom-shortcuts.module';
import { DirectoryWatchersModule } from './directory-watchers/directory-watchers.module';
import { FileConverterModule } from './file-converter/file-converter.module';
import { FileModule } from './file/file.module';
import { FormGeneratorModule } from './form-generator/form-generator.module';
import { ImageProcessingModule } from './image-processing/image-processing.module';
import { LegacyDatabaseImporterModule } from './legacy-database-importer/legacy-database-importer.module';
import { LogsModule } from './logs/logs.module';
import { NotificationsModule } from './notifications/notifications.module';
import { PostParsersModule } from './post-parsers/post-parsers.module';
import { PostModule } from './post/post.module';
import { RemotePasswordMiddleware } from './remote/remote.middleware';
import { RemoteModule } from './remote/remote.module';
import { SettingsModule } from './settings/settings.module';
import { SubmissionModule } from './submission/submission.module';
import { TagConvertersModule } from './tag-converters/tag-converters.module';
import { TagGroupsModule } from './tag-groups/tag-groups.module';
import { UpdateModule } from './update/update.module';
import { UserConvertersModule } from './user-converters/user-converters.module';
import { UserSpecifiedWebsiteOptionsModule } from './user-specified-website-options/user-specified-website-options.module';
import { ValidationModule } from './validation/validation.module';
import { WebSocketModule } from './web-socket/web-socket.module';
import { WebsiteOptionsModule } from './website-options/website-options.module';
import { WebsitesModule } from './websites/websites.module';
@Module({
imports: [
ScheduleModule.forRoot(),
ImageProcessingModule,
AccountModule,
WebSocketModule,
WebsitesModule,
FileModule,
SubmissionModule,
SettingsModule,
ServeStaticModule.forRoot({
rootPath: join(__dirname, '..', 'postybirb-ui'),
exclude: ['/api*'],
}),
FormGeneratorModule,
WebsiteOptionsModule,
TagGroupsModule,
TagConvertersModule,
UserConvertersModule,
DirectoryWatchersModule,
UserSpecifiedWebsiteOptionsModule,
UpdateModule,
PostModule,
PostParsersModule,
ValidationModule,
FileConverterModule,
NotificationsModule,
RemoteModule,
CustomShortcutsModule,
LegacyDatabaseImporterModule,
LogsModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(RemotePasswordMiddleware).forRoutes('*');
}
}
================================================
FILE: apps/client-server/src/app/app.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { app } from 'electron';
@Injectable()
export class AppService {
getData(): Record<string, string> {
return {
message: 'pong',
version: app.getVersion(),
location: app.getPath('userData'),
};
}
}
================================================
FILE: apps/client-server/src/app/common/controller/postybirb-controller.ts
================================================
import { Delete, Get, Param, Query } from '@nestjs/common';
import { ApiOkResponse } from '@nestjs/swagger';
import { SchemaKey } from '@postybirb/database';
import { EntityId } from '@postybirb/types';
import { PostyBirbService } from '../service/postybirb-service';
/**
* Base PostyBirb controller logic that should be good for most rest calls.
*
* @class PostyBirbController
*/
export abstract class PostyBirbController<T extends SchemaKey> {
constructor(protected readonly service: PostyBirbService<T>) {}
@Get(':id')
@ApiOkResponse({ description: 'Record by Id.' })
findOne(@Param('id') id: EntityId) {
return this.service
.findById(id, { failOnMissing: true })
.then((record) => record.toDTO());
}
@Get()
@ApiOkResponse({ description: 'A list of all records.' })
findAll() {
return this.service
.findAll()
.then((records) => records.map((record) => record.toDTO()));
}
@Delete()
@ApiOkResponse({
description: 'Records removed.',
})
async remove(@Query('ids') ids: EntityId | EntityId[]) {
return Promise.all(
(Array.isArray(ids) ? ids : [ids]).map((id) => this.service.remove(id)),
).then(() => ({
success: true,
}));
}
}
================================================
FILE: apps/client-server/src/app/common/service/postybirb-service.ts
================================================
import { BadRequestException, Injectable } from '@nestjs/common';
import { SchemaKey } from '@postybirb/database';
import { Logger } from '@postybirb/logger';
import { EntityId } from '@postybirb/types';
import { SQL } from 'drizzle-orm';
import { FindOptions } from '../../drizzle/postybirb-database/find-options.type';
import { PostyBirbDatabase } from '../../drizzle/postybirb-database/postybirb-database';
import { WSGateway } from '../../web-socket/web-socket-gateway';
import { WebSocketEvents } from '../../web-socket/web-socket.events';
/**
* Base class that implements simple CRUD logic
*
* @class PostyBirbService
*/
@Injectable()
export abstract class PostyBirbService<TSchemaKey extends SchemaKey> {
protected readonly logger = Logger(this.constructor.name);
protected readonly repository: PostyBirbDatabase<TSchemaKey>;
constructor(
private readonly table: TSchemaKey | PostyBirbDatabase<TSchemaKey>,
private readonly webSocket?: WSGateway,
) {
if (typeof table === 'string') {
this.repository = new PostyBirbDatabase(table);
} else {
this.repository = table;
}
}
/**
* Emits events onto the websocket
*
* @protected
* @param {WebSocketEvents} event
*/
protected async emit(event: WebSocketEvents) {
try {
if (this.webSocket) {
this.webSocket.emit(event);
}
} catch (err) {
this.logger.error(`Error emitting websocket event: ${event.event}`, err);
}
}
protected get schema() {
return this.repository.schemaEntity;
}
/**
* Throws exception if a record matching the query already exists.
*
* @protected
* @param {FilterQuery<T>} where
*/
protected async throwIfExists(where: SQL) {
const exists = await this.repository.select(where);
if (exists.length) {
this.logger
.withMetadata(exists)
.error(`A duplicate entity already exists`);
throw new BadRequestException(`A duplicate entity already exists`);
}
}
// Repository Wrappers
public findById(id: EntityId, options?: FindOptions) {
return this.repository.findById(id, options);
}
public findAll() {
return this.repository.findAll();
}
public remove(id: EntityId) {
this.logger.withMetadata({ id }).info(`Removing entity '${id}'`);
return this.repository.deleteById([id]);
}
// END Repository Wrappers
}
================================================
FILE: apps/client-server/src/app/constants.ts
================================================
// Constant variables
export const WEBSITE_IMPLEMENTATIONS = 'WEBSITE_IMPLEMENTATIONS';
================================================
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcut.events.ts
================================================
import { CUSTOM_SHORTCUT_UPDATES } from '@postybirb/socket-events';
import { ICustomShortcut } from '@postybirb/types';
import { WebsocketEvent } from '../web-socket/models/web-socket-event';
export type CustomShortcutEventTypes = CustomShortcutEvent;
class CustomShortcutEvent implements WebsocketEvent<ICustomShortcut[]> {
event: string = CUSTOM_SHORTCUT_UPDATES;
data: ICustomShortcut[];
}
================================================
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcuts.controller.ts
================================================
import { Body, Controller, Param, Patch, Post } from '@nestjs/common';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { PostyBirbController } from '../common/controller/postybirb-controller';
import { CustomShortcutsService } from './custom-shortcuts.service';
import { CreateCustomShortcutDto } from './dtos/create-custom-shortcut.dto';
import { UpdateCustomShortcutDto } from './dtos/update-custom-shortcut.dto';
@ApiTags('custom-shortcut')
@Controller('custom-shortcut')
export class CustomShortcutsController extends PostyBirbController<'CustomShortcutSchema'> {
constructor(readonly service: CustomShortcutsService) {
super(service);
}
@Post()
@ApiOkResponse({ description: 'Custom shortcut created' })
async create(@Body() createCustomShortcutDto: CreateCustomShortcutDto) {
return this.service
.create(createCustomShortcutDto)
.then((shortcut) => shortcut.toDTO());
}
@Patch(':id')
@ApiOkResponse({ description: 'Custom shortcut updated' })
async update(
@Body() updateCustomShortcutDto: UpdateCustomShortcutDto,
@Param('id') id: string,
) {
return this.service
.update(id, updateCustomShortcutDto)
.then((shortcut) => shortcut.toDTO());
}
}
================================================
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcuts.module.ts
================================================
import { Module } from '@nestjs/common';
import { CustomShortcutsController } from './custom-shortcuts.controller';
import { CustomShortcutsService } from './custom-shortcuts.service';
@Module({
controllers: [CustomShortcutsController],
providers: [CustomShortcutsService],
exports: [CustomShortcutsService],
})
export class CustomShortcutsModule {}
================================================
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcuts.service.ts
================================================
import { Injectable, Optional } from '@nestjs/common';
import { CUSTOM_SHORTCUT_UPDATES } from '@postybirb/socket-events';
import { EntityId } from '@postybirb/types';
import { eq } from 'drizzle-orm';
import { PostyBirbService } from '../common/service/postybirb-service';
import { CustomShortcut } from '../drizzle/models/custom-shortcut.entity';
import { WSGateway } from '../web-socket/web-socket-gateway';
import { CreateCustomShortcutDto } from './dtos/create-custom-shortcut.dto';
import { UpdateCustomShortcutDto } from './dtos/update-custom-shortcut.dto';
@Injectable()
export class CustomShortcutsService extends PostyBirbService<'CustomShortcutSchema'> {
constructor(@Optional() webSocket?: WSGateway) {
super('CustomShortcutSchema', webSocket);
this.repository.subscribe('CustomShortcutSchema', () => this.emit());
}
public async emit() {
const dtos = await this.findAll();
super.emit({
event: CUSTOM_SHORTCUT_UPDATES,
data: dtos.map((dto) => dto.toDTO()),
});
}
public async create(
createCustomShortcutDto: CreateCustomShortcutDto,
): Promise<CustomShortcut> {
this.logger
.withMetadata(createCustomShortcutDto)
.info('Creating custom shortcut');
await this.throwIfExists(
eq(this.schema.name, createCustomShortcutDto.name),
);
return this.repository.insert(createCustomShortcutDto);
}
public async update(
id: string,
updateCustomShortcutDto: UpdateCustomShortcutDto,
): Promise<CustomShortcut> {
this.logger
.withMetadata(updateCustomShortcutDto)
.info('Updating custom shortcut');
const existing = await this.repository.findById(id, {
failOnMissing: true,
});
return this.repository.update(id, updateCustomShortcutDto);
}
public async remove(id: EntityId) {
const existing = await this.repository.findById(id, {
failOnMissing: true,
});
return super.remove(id);
}
}
================================================
FILE: apps/client-server/src/app/custom-shortcuts/dtos/create-custom-shortcut.dto.ts
================================================
import { ICreateCustomShortcutDto } from '@postybirb/types';
import { IsString } from 'class-validator';
export class CreateCustomShortcutDto implements ICreateCustomShortcutDto {
@IsString()
name: string;
}
================================================
FILE: apps/client-server/src/app/custom-shortcuts/dtos/update-custom-shortcut.dto.ts
================================================
import { Description, IUpdateCustomShortcutDto } from '@postybirb/types';
import { IsObject, IsString } from 'class-validator';
export class UpdateCustomShortcutDto implements IUpdateCustomShortcutDto {
@IsString()
name: string;
@IsObject()
shortcut: Description;
}
================================================
FILE: apps/client-server/src/app/directory-watchers/directory-watcher.events.ts
================================================
import { DIRECTORY_WATCHER_UPDATES } from '@postybirb/socket-events';
import { DirectoryWatcherDto } from '@postybirb/types';
import { WebsocketEvent } from '../web-socket/models/web-socket-event';
export type DirectoryWatcherEventTypes = DirectoryWatcherUpdateEvent;
class DirectoryWatcherUpdateEvent
implements WebsocketEvent<DirectoryWatcherDto[]>
{
event: string = DIRECTORY_WATCHER_UPDATES;
data: DirectoryWatcherDto[];
}
================================================
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.controller.ts
================================================
import { Body, Controller, Param, Patch, Post } from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiNotFoundResponse,
ApiOkResponse,
ApiTags,
} from '@nestjs/swagger';
import { EntityId } from '@postybirb/types';
import { PostyBirbController } from '../common/controller/postybirb-controller';
import { DirectoryWatchersService } from './directory-watchers.service';
import { CheckPathDto } from './dtos/check-path.dto';
import { CreateDirectoryWatcherDto } from './dtos/create-directory-watcher.dto';
import { UpdateDirectoryWatcherDto } from './dtos/update-directory-watcher.dto';
/**
* CRUD operations on DirectoryWatchers.
* @class DirectoryWatchersController
*/
@ApiTags('directory-watchers')
@Controller('directory-watchers')
export class DirectoryWatchersController extends PostyBirbController<'DirectoryWatcherSchema'> {
constructor(readonly service: DirectoryWatchersService) {
super(service);
}
@Post()
@ApiOkResponse({ description: 'Entity created.' })
@ApiBadRequestResponse({ description: 'Bad request made.' })
create(@Body() createDto: CreateDirectoryWatcherDto) {
return this.service.create(createDto).then((entity) => entity.toDTO());
}
@Patch(':id')
@ApiOkResponse({ description: 'Entity updated.', type: Boolean })
@ApiNotFoundResponse({ description: 'Entity not found.' })
update(
@Body() updateDto: UpdateDirectoryWatcherDto,
@Param('id') id: EntityId,
) {
return this.service.update(id, updateDto).then((entity) => entity.toDTO());
}
@Post('check-path')
@ApiOkResponse({ description: 'Path check result.' })
@ApiBadRequestResponse({ description: 'Bad request made.' })
checkPath(@Body() checkPathDto: CheckPathDto) {
return this.service.checkPath(checkPathDto.path);
}
}
================================================
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.module.ts
================================================
import { Module } from '@nestjs/common';
import { NotificationsModule } from '../notifications/notifications.module';
import { SubmissionModule } from '../submission/submission.module';
import { DirectoryWatchersController } from './directory-watchers.controller';
import { DirectoryWatchersService } from './directory-watchers.service';
@Module({
imports: [SubmissionModule, NotificationsModule],
controllers: [DirectoryWatchersController],
providers: [DirectoryWatchersService],
})
export class DirectoryWatchersModule {}
================================================
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.service.spec.ts
================================================
import { Test, TestingModule } from '@nestjs/testing';
import { clearDatabase } from '@postybirb/database';
import { DirectoryWatcherImportAction, SubmissionType } from '@postybirb/types';
import { mkdir, readdir, rename, writeFile } from 'fs/promises';
import { join } from 'path';
import { AccountService } from '../account/account.service';
import { NotificationsModule } from '../notifications/notifications.module';
import { CreateSubmissionDto } from '../submission/dtos/create-submission.dto';
import { SubmissionService } from '../submission/services/submission.service';
import { SubmissionModule } from '../submission/submission.module';
import { DirectoryWatchersService } from './directory-watchers.service';
import { CreateDirectoryWatcherDto } from './dtos/create-directory-watcher.dto';
import { UpdateDirectoryWatcherDto } from './dtos/update-directory-watcher.dto';
// Mock fs/promises
jest.mock('fs/promises', () => ({
readdir: jest.fn(),
mkdir: jest.fn(),
rename: jest.fn(),
writeFile: jest.fn(),
}));
describe('DirectoryWatchersService', () => {
let service: DirectoryWatchersService;
let submissionService: SubmissionService;
let accountService: AccountService;
let module: TestingModule;
beforeEach(async () => {
clearDatabase();
// Setup mocks
(readdir as jest.Mock).mockResolvedValue([]);
(mkdir as jest.Mock).mockResolvedValue(undefined);
(rename as jest.Mock).mockResolvedValue(undefined);
(writeFile as jest.Mock).mockResolvedValue(undefined);
module = await Test.createTestingModule({
imports: [SubmissionModule, NotificationsModule],
providers: [DirectoryWatchersService],
}).compile();
service = module.get<DirectoryWatchersService>(DirectoryWatchersService);
submissionService = module.get<SubmissionService>(SubmissionService);
accountService = module.get<AccountService>(AccountService);
await accountService.onModuleInit();
});
async function createSubmission() {
const dto = new CreateSubmissionDto();
dto.name = 'test';
dto.type = SubmissionType.MESSAGE;
dto.isTemplate = true;
const record = await submissionService.create(dto);
return record;
}
afterEach(() => {
jest.clearAllMocks();
});
afterAll(async () => {
await module.close();
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(module).toBeDefined();
});
it('should create entities and setup directory structure', async () => {
const dto = new CreateDirectoryWatcherDto();
dto.importAction = DirectoryWatcherImportAction.NEW_SUBMISSION;
dto.path = 'path';
await service.create(dto);
// Verify directory structure was created
expect(mkdir).toHaveBeenCalledWith(join('path', 'processing'), {
recursive: true,
});
expect(mkdir).toHaveBeenCalledWith(join('path', 'completed'), {
recursive: true,
});
expect(mkdir).toHaveBeenCalledWith(join('path', 'failed'), {
recursive: true,
});
const entities = await service.findAll();
const record = entities[0];
expect(record.path).toBe(dto.path);
expect(record.importAction).toBe(dto.importAction);
});
it('should update entities', async () => {
const dto = new CreateDirectoryWatcherDto();
dto.importAction = DirectoryWatcherImportAction.NEW_SUBMISSION;
dto.path = 'path';
const record = await service.create(dto);
expect(record.path).toBe(dto.path);
expect(record.importAction).toBe(dto.importAction);
const updateDto = new UpdateDirectoryWatcherDto();
updateDto.path = 'updated-path';
const updatedRecord = await service.update(record.id, updateDto);
expect(updatedRecord.path).toBe(updateDto.path);
// Verify directory structure was created for new path
expect(mkdir).toHaveBeenCalledWith(join('updated-path', 'processing'), {
recursive: true,
});
});
it('should support templates', async () => {
const template = await createSubmission();
const dto = new CreateDirectoryWatcherDto();
dto.importAction = DirectoryWatcherImportAction.NEW_SUBMISSION;
dto.path = 'path';
const record = await service.create(dto);
expect(record.path).toBe(dto.path);
const updateDto = new UpdateDirectoryWatcherDto();
updateDto.templateId = template.id;
const updatedRecord = await service.update(record.id, updateDto);
expect(updatedRecord.templateId).toBe(template.id);
expect(updatedRecord.toDTO()).toEqual({
createdAt: updatedRecord.createdAt,
updatedAt: updatedRecord.updatedAt,
id: updatedRecord.id,
importAction: DirectoryWatcherImportAction.NEW_SUBMISSION,
path: 'path',
templateId: template.id,
});
await submissionService.remove(template.id);
const rec = await service.findById(updatedRecord.id);
expect(rec.templateId).toBe(null);
});
it('should throw error if path does not exist', async () => {
(readdir as jest.Mock).mockRejectedValue(new Error('Path not found'));
const dto = new CreateDirectoryWatcherDto();
dto.importAction = DirectoryWatcherImportAction.NEW_SUBMISSION;
dto.path = 'non-existent-path';
await expect(service.create(dto)).rejects.toThrow(
"Path 'non-existent-path' does not exist or is not accessible",
);
});
it('should create entity without path (UI flow)', async () => {
const dto = new CreateDirectoryWatcherDto();
dto.importAction = DirectoryWatcherImportAction.NEW_SUBMISSION;
// No path provided
const record = await service.create(dto);
// Verify entity was created without path
expect(record.importAction).toBe(dto.importAction);
expect(record.path).toBeNull();
// Verify no directory structure was created
expect(mkdir).not.toHaveBeenCalled();
});
it('should create directory structure when path is first set via update', async () => {
// Create without path
const dto = new CreateDirectoryWatcherDto();
dto.importAction = DirectoryWatcherImportAction.NEW_SUBMISSION;
const record = await service.create(dto);
expect(record.path).toBeNull();
// Clear mocks from create
jest.clearAllMocks();
// Update with path
const updateDto = new UpdateDirectoryWatcherDto();
updateDto.path = 'new-path';
const updatedRecord = await service.update(record.id, updateDto);
expect(updatedRecord.path).toBe('new-path');
// Verify directory structure was created
expect(mkdir).toHaveBeenCalledWith(join('new-path', 'processing'), {
recursive: true,
});
expect(mkdir).toHaveBeenCalledWith(join('new-path', 'completed'), {
recursive: true,
});
expect(mkdir).toHaveBeenCalledWith(join('new-path', 'failed'), {
recursive: true,
});
});
});
================================================
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.service.ts
================================================
import { BadRequestException, Injectable, Optional } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { DIRECTORY_WATCHER_UPDATES } from '@postybirb/socket-events';
import {
DirectoryWatcherImportAction,
EntityId,
SubmissionType,
} from '@postybirb/types';
import { IsTestEnvironment } from '@postybirb/utils/electron';
import { mkdir, readdir, rename, writeFile } from 'fs/promises';
import { getType } from 'mime';
import { join } from 'path';
import { PostyBirbService } from '../common/service/postybirb-service';
import { DirectoryWatcher } from '../drizzle/models';
import { MulterFileInfo } from '../file/models/multer-file-info';
import { NotificationsService } from '../notifications/notifications.service';
import { SubmissionService } from '../submission/services/submission.service';
import { WSGateway } from '../web-socket/web-socket-gateway';
import { CreateDirectoryWatcherDto } from './dtos/create-directory-watcher.dto';
import { UpdateDirectoryWatcherDto } from './dtos/update-directory-watcher.dto';
/**
* Directory structure for file processing:
*
* {watch-path}/ <- Users drop files here
* ├── processing/ <- Files being actively processed
* ├── completed/ <- Successfully processed files
* └── failed/ <- Files that failed processing (with .error.txt)
*/
/**
* A directory watcher service that reads created watchers and checks
* for new files added to the folder.
*
* Files are moved through different folders based on processing status,
* eliminating the need for metadata tracking.
*
* @class DirectoryWatchersService
* @extends {PostyBirbService<DirectoryWatcher>}
*/
/**
* Threshold for warning users about folders with many files.
* If a folder contains more than this number of files, a confirmation will be required.
*/
export const FILE_COUNT_WARNING_THRESHOLD = 10;
/**
* Result of checking a directory path for file watcher usage.
*/
export interface CheckPathResult {
/** Whether the path is valid and accessible */
valid: boolean;
/** Number of files in the directory (excluding subfolders) */
count: number;
/** List of file names in the directory */
files: string[];
/** Error message if the path is invalid */
error?: string;
}
@Injectable()
export class DirectoryWatchersService extends PostyBirbService<'DirectoryWatcherSchema'> {
private runningWatchers = new Set<EntityId>();
private recoveredWatchers = new Set<EntityId>();
private readonly SUBFOLDER_PROCESSING = 'processing';
private readonly SUBFOLDER_COMPLETED = 'completed';
private readonly SUBFOLDER_FAILED = 'failed';
constructor(
private readonly submissionService: SubmissionService,
private readonly notificationService: NotificationsService,
@Optional() webSocket?: WSGateway,
) {
super('DirectoryWatcherSchema', webSocket);
this.repository.subscribe('DirectoryWatcherSchema', () =>
this.emitUpdates(),
);
}
protected async emitUpdates() {
super.emit({
event: DIRECTORY_WATCHER_UPDATES,
data: (await this.repository.findAll()).map((entity) => entity.toDTO()),
});
}
/**
* CRON run read of paths.
*/
@Cron(CronExpression.EVERY_30_SECONDS)
private async run() {
if (!IsTestEnvironment()) {
const entities = await this.repository.findAll();
entities
.filter((e) => !!e.path)
.forEach((e) => {
// Recover orphaned files on first run
if (!this.recoveredWatchers.has(e.id)) {
this.recoverOrphanedFiles(e);
this.recoveredWatchers.add(e.id);
}
// Process new files if not already running
if (!this.runningWatchers.has(e.id)) {
this.runningWatchers.add(e.id);
this.read(e).finally(() => this.runningWatchers.delete(e.id));
}
});
}
}
/**
* Ensures all required subdirectories exist.
* Skips if the base path doesn't exist.
*
* @param {string} basePath
*/
private async ensureDirectoryStructure(basePath: string): Promise<void> {
// Check if base path exists, skip if it doesn't
try {
await readdir(basePath);
} catch {
return;
}
const subfolders = [
this.SUBFOLDER_PROCESSING,
this.SUBFOLDER_COMPLETED,
this.SUBFOLDER_FAILED,
];
for (const folder of subfolders) {
await mkdir(join(basePath, folder), { recursive: true });
}
}
/**
* Recovers files that were left in the processing folder due to app crash/restart.
* Moves them back to the main watch folder for reprocessing.
*
* @param {DirectoryWatcher} watcher
*/
private async recoverOrphanedFiles(watcher: DirectoryWatcher): Promise<void> {
try {
await this.ensureDirectoryStructure(watcher.path);
const processingPath = join(watcher.path, this.SUBFOLDER_PROCESSING);
const orphanedFiles = await readdir(processingPath);
if (orphanedFiles.length > 0) {
this.logger.info(
`Recovering ${orphanedFiles.length} orphaned files in ${watcher.path}`,
);
for (const file of orphanedFiles) {
const sourcePath = join(processingPath, file);
const targetPath = join(watcher.path, file);
try {
await rename(sourcePath, targetPath);
this.logger.info(`Recovered orphaned file: ${file}`);
} catch (err) {
this.logger.error(err, `Failed to recover orphaned file: ${file}`);
}
}
this.notificationService.create({
title: 'Directory Watcher Recovery',
message: `Recovered ${orphanedFiles.length} orphaned files in '${watcher.path}'`,
type: 'info',
tags: ['directory-watcher', 'recovery'],
data: {
recoveredFiles: orphanedFiles,
watcherId: watcher.id,
},
});
}
} catch (err) {
this.logger.error(
err,
`Failed to recover orphaned files for watcher ${watcher.id}`,
);
}
}
/**
* Reads directory for processable files.
*
* @param {DirectoryWatcher} watcher
*/
private async read(watcher: DirectoryWatcher) {
try {
// Ensure directory structure exists
await this.ensureDirectoryStructure(watcher.path);
const allFiles = await readdir(watcher.path);
const filesInDirectory = allFiles.filter(
(file) =>
file !== this.SUBFOLDER_PROCESSING &&
file !== this.SUBFOLDER_COMPLETED &&
file !== this.SUBFOLDER_FAILED,
);
// Only process and notify if there are files
if (filesInDirectory.length === 0) {
return;
}
const results = { success: [], failed: [] };
// Process files sequentially
for (const file of filesInDirectory) {
try {
await this.processFileWithMove(watcher, file);
results.success.push(file);
} catch (err) {
this.logger.error(err, `Failed to process file ${file}`);
results.failed.push({ file, error: err.message });
}
}
// Create notification with success/failure breakdown
this.notificationService.create({
title: 'Directory Watcher',
message: `Processed ${results.success.length} of ${filesInDirectory.length} files in '${watcher.path}'`,
type: results.failed.length > 0 ? 'warning' : 'info',
tags: ['directory-watcher'],
data: {
successCount: results.success.length,
failedCount: results.failed.length,
successFiles: results.success,
failedFiles: results.failed,
watcherId: watcher.id,
},
});
} catch (e) {
this.logger.error(e, `Failed to read directory ${watcher.path}`);
this.notificationService.create({
title: 'Directory Watcher Error',
message: `Failed to read directory ${watcher.path}`,
type: 'error',
tags: ['directory-watcher'],
data: {
error: e.message,
},
});
}
}
/**
* Processes a file using the move/archive pattern.
* Files are moved through: main folder -> processing -> completed/failed
*
* @param {DirectoryWatcher} watcher
* @param {string} fileName
*/
private async processFileWithMove(
watcher: DirectoryWatcher,
fileName: string,
): Promise<void> {
const sourcePath = join(watcher.path, fileName);
const processingPath = join(
watcher.path,
this.SUBFOLDER_PROCESSING,
fileName,
);
const completedPath = join(
watcher.path,
this.SUBFOLDER_COMPLETED,
fileName,
);
const failedPath = join(watcher.path, this.SUBFOLDER_FAILED, fileName);
let currentLocation = sourcePath;
let submissionId: EntityId | null = null;
try {
// Step 1: Move to processing folder (atomic operation)
await rename(sourcePath, processingPath);
currentLocation = processingPath;
this.logger.info(`Processing file ${fileName}`);
// Step 2: Process the file
const multerInfo: MulterFileInfo = {
fieldname: '',
origin: 'directory-watcher',
originalname: fileName,
encoding: '',
mimetype: getType(fileName),
size: 0,
destination: '',
filename: fileName,
path: processingPath, // Use processing path
};
switch (watcher.importAction) {
case DirectoryWatcherImportAction.NEW_SUBMISSION: {
const submission = await this.submissionService.create(
{
name: fileName,
type: SubmissionType.FILE,
},
multerInfo,
);
submissionId = submission.id;
if (watcher.template) {
await this.submissionService.applyOverridingTemplate(
submission.id,
watcher.template?.id,
);
}
break;
}
default:
break;
}
// Step 3: Move to completed folder
await rename(processingPath, completedPath);
this.logger.info(
`Successfully processed file ${fileName} (submission: ${submissionId})`,
);
} catch (err) {
this.logger.error(err, `Failed to process file ${fileName}`);
// Cleanup submission if it was created
if (submissionId) {
await this.submissionService
.remove(submissionId)
.catch((cleanupErr) => {
this.logger.error(
cleanupErr,
`Failed to cleanup submission ${submissionId}`,
);
});
}
// Move to failed folder and create error file
try {
await rename(currentLocation, failedPath);
// Create error details file
const errorFilePath = join(
watcher.path,
this.SUBFOLDER_FAILED,
`${fileName}.error.txt`,
);
const errorDetails = [
`File: ${fileName}`,
`Failed at: ${new Date().toISOString()}`,
`Error: ${err.message}`,
`Stack: ${err.stack || 'N/A'}`,
].join('\n');
await writeFile(errorFilePath, errorDetails);
} catch (moveErr) {
this.logger.error(
moveErr,
`Failed to move file to failed folder: ${fileName}`,
);
}
throw err;
}
}
async create(
createDto: CreateDirectoryWatcherDto,
): Promise<DirectoryWatcher> {
// Validate path exists and is accessible (only if path is provided)
if (createDto.path) {
try {
await readdir(createDto.path);
} catch (err) {
throw new BadRequestException(
`Path '${createDto.path}' does not exist or is not accessible`,
);
}
// Create directory structure
await this.ensureDirectoryStructure(createDto.path);
}
return this.repository.insert(createDto);
}
async update(id: EntityId, update: UpdateDirectoryWatcherDto) {
this.logger.withMetadata(update).info(`Updating DirectoryWatcher '${id}'`);
const entity = await this.repository.findById(id, { failOnMissing: true });
// Validate path if being updated
if (update.path && update.path !== entity.path) {
try {
await readdir(update.path);
} catch (err) {
throw new BadRequestException(
`Path '${update.path}' does not exist or is not accessible`,
);
}
// Create directory structure for new path
await this.ensureDirectoryStructure(update.path);
}
const template = update.templateId
? await this.submissionService.findById(update.templateId, {
failOnMissing: true,
})
: null;
if (template && !template.isTemplate) {
throw new BadRequestException('Template Id provided is not a template.');
}
const updatedEntity = await this.repository.update(id, {
importAction: update.importAction ?? entity.importAction,
path: update.path ?? entity.path,
templateId: update.templateId ?? entity.templateId,
});
// If this is the first time setting a path (from null/empty to valid path), ensure directory structure
if (!entity.path && updatedEntity.path) {
await this.ensureDirectoryStructure(updatedEntity.path);
}
return updatedEntity;
}
/**
* Checks a directory path for validity and returns file count information.
* Used to warn users before selecting folders with many files.
*
* @param {string} path - The directory path to check
* @returns {Promise<CheckPathResult>} Information about the directory
*/
async checkPath(path: string): Promise<CheckPathResult> {
try {
const allFiles = await readdir(path);
const files = allFiles.filter(
(file) =>
file !== this.SUBFOLDER_PROCESSING &&
file !== this.SUBFOLDER_COMPLETED &&
file !== this.SUBFOLDER_FAILED,
);
return {
valid: true,
count: files.length,
files,
};
} catch (err) {
return {
valid: false,
count: 0,
files: [],
error: `Path '${path}' does not exist or is not accessible`,
};
}
}
}
================================================
FILE: apps/client-server/src/app/directory-watchers/dtos/check-path.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
export class CheckPathDto {
@ApiProperty({ description: 'The directory path to check' })
@IsNotEmpty()
@IsString()
path: string;
}
================================================
FILE: apps/client-server/src/app/directory-watchers/dtos/create-directory-watcher.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import {
DirectoryWatcherImportAction,
ICreateDirectoryWatcherDto,
} from '@postybirb/types';
import { IsEnum, IsOptional, IsString } from 'class-validator';
export class CreateDirectoryWatcherDto implements ICreateDirectoryWatcherDto {
@ApiProperty()
@IsOptional()
@IsString()
path: string;
@ApiProperty({
enum: DirectoryWatcherImportAction,
})
@IsEnum(DirectoryWatcherImportAction)
importAction: DirectoryWatcherImportAction;
}
================================================
FILE: apps/client-server/src/app/directory-watchers/dtos/update-directory-watcher.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import {
DirectoryWatcherImportAction,
IUpdateDirectoryWatcherDto,
SubmissionId,
} from '@postybirb/types';
import { IsEnum, IsOptional, IsString } from 'class-validator';
export class UpdateDirectoryWatcherDto implements IUpdateDirectoryWatcherDto {
@ApiProperty({
enum: DirectoryWatcherImportAction,
required: false,
})
@IsOptional()
@IsEnum(DirectoryWatcherImportAction)
importAction?: DirectoryWatcherImportAction;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
path?: string;
@ApiProperty({ required: false })
@IsOptional()
@IsString()
templateId?: SubmissionId;
}
================================================
FILE: apps/client-server/src/app/drizzle/models/account.entity.ts
================================================
import { IAccount, IAccountDto } from '@postybirb/types';
import { Exclude, instanceToPlain, Type } from 'class-transformer';
import { UnknownWebsite } from '../../websites/website';
import { DatabaseEntity } from './database-entity';
import { WebsiteData } from './website-data.entity';
export class Account extends DatabaseEntity implements IAccount {
name: string;
website: string;
groups: string[] = [];
/**
* we don't want to pass this down to users unless filtered
* by the website instance.
*/
@Exclude()
@Type(() => WebsiteData)
websiteData: WebsiteData;
@Exclude()
websiteInstance?: UnknownWebsite;
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(entity: Partial<IAccount>) {
super(entity);
Object.assign(this, entity);
}
toObject(): IAccount {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IAccount;
}
toDTO(): IAccountDto {
const dto: IAccountDto = {
...(this.toObject() as unknown as IAccountDto),
data: this.websiteInstance?.getWebsiteData() ?? {},
state: this.websiteInstance?.getLoginState() ?? {
isLoggedIn: false,
username: '',
pending: false,
lastUpdated: null,
},
websiteInfo: {
websiteDisplayName:
this.websiteInstance?.decoratedProps.metadata.displayName ?? '',
supports: this.websiteInstance?.getSupportedTypes() ?? [],
},
};
return dto;
}
withWebsiteInstance(websiteInstance: UnknownWebsite): this {
this.websiteInstance = websiteInstance;
return this;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/custom-shortcut.entity.ts
================================================
import {
DefaultDescription,
Description,
ICustomShortcut,
ICustomShortcutDto,
} from '@postybirb/types';
import { instanceToPlain } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
export class CustomShortcut extends DatabaseEntity implements ICustomShortcut {
name: string;
shortcut: Description = DefaultDescription();
constructor(entity: Partial<ICustomShortcut>) {
super(entity);
Object.assign(this, entity);
}
public toObject(): ICustomShortcut {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as ICustomShortcut;
}
public toDTO(): ICustomShortcutDto {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as ICustomShortcutDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/database-entity.spec.ts
================================================
import { IEntity, IEntityDto } from '@postybirb/types';
import { instanceToPlain } from 'class-transformer';
import 'reflect-metadata';
import { DatabaseEntity, fromDatabaseRecord } from './database-entity';
class TestEntity extends DatabaseEntity {
public testField: string;
constructor(entity: Partial<TestEntity>) {
super(entity);
Object.assign(this, entity);
}
toObject(): IEntity {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as unknown as IEntity;
}
toDTO(): IEntityDto {
return this.toObject() as unknown as IEntityDto;
}
}
describe('DatabaseEntity', () => {
let entity: TestEntity;
beforeEach(() => {
entity = new TestEntity({
id: 'id',
testField: 'test',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
});
});
it('should create an instance', () => {
expect(entity).toBeTruthy();
});
it('should convert class to object', () => {
const obj = entity.toObject();
expect(obj).toBeTruthy();
});
it('should succeed fromDatabaseObject', () => {
const dbObj = {
id: 'id',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
testField: 'test',
};
const obj = fromDatabaseRecord(TestEntity, dbObj);
expect(obj).toBeTruthy();
expect(obj.id).toBe(dbObj.id);
expect(obj.createdAt).toEqual(dbObj.createdAt);
expect(obj.updatedAt).toEqual(dbObj.updatedAt);
expect(obj.testField).toBe(dbObj.testField);
});
it('should convert toObject', () => {
const obj = entity.toObject();
expect(obj).toBeTruthy();
expect(obj.id).toBe(entity.id);
expect(obj.createdAt).toBe(entity.createdAt);
expect(obj.updatedAt).toBe(entity.updatedAt);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
expect((obj as any).testField).toBe(entity.testField);
});
});
================================================
FILE: apps/client-server/src/app/drizzle/models/database-entity.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { EntityId, IEntity, IEntityDto } from '@postybirb/types';
import {
ClassConstructor,
plainToClass,
plainToInstance,
} from 'class-transformer';
import { v4 } from 'uuid';
export function fromDatabaseRecord<TEntity>(
entity: ClassConstructor<TEntity>,
record: any[],
): TEntity[];
export function fromDatabaseRecord<TEntity>(
entity: ClassConstructor<TEntity>,
record: any,
): TEntity;
export function fromDatabaseRecord<TEntity>(
entity: ClassConstructor<TEntity>,
record: any | any[],
): TEntity | TEntity[] {
if (Array.isArray(record)) {
return record.map((r) =>
plainToInstance(entity, r, { enableCircularCheck: true }),
) as TEntity[];
}
return plainToClass(entity, record, {
enableCircularCheck: true,
}) as TEntity;
}
export abstract class DatabaseEntity implements IEntity {
public readonly id: EntityId;
public createdAt: string;
public updatedAt: string;
constructor(entity: Partial<IEntity>) {
Object.assign(this, entity);
if (!this.id) {
this.id = v4();
}
}
public abstract toObject(): IEntity;
public abstract toDTO(): IEntityDto;
public toJSON(): string {
return JSON.stringify(this.toDTO());
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/directory-watcher.entity.ts
================================================
import {
DirectoryWatcherDto,
DirectoryWatcherImportAction,
IDirectoryWatcher,
SubmissionId,
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
import { Submission } from './submission.entity';
export class DirectoryWatcher
extends DatabaseEntity
implements IDirectoryWatcher
{
path?: string;
templateId: SubmissionId;
importAction: DirectoryWatcherImportAction;
@Type(() => Submission)
template: Submission;
toObject(): IDirectoryWatcher {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IDirectoryWatcher;
}
toDTO(): DirectoryWatcherDto {
return this.toObject() as unknown as DirectoryWatcherDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/file-buffer.entity.ts
================================================
import { EntityId, FileBufferDto, IFileBuffer } from '@postybirb/types';
import { Exclude, instanceToPlain, Type } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
export class FileBuffer extends DatabaseEntity implements IFileBuffer {
submissionFileId: EntityId;
@Type(() => Buffer)
@Exclude({ toPlainOnly: true })
buffer: Buffer;
fileName: string;
mimeType: string;
size: number;
width: number;
height: number;
constructor(entity: Partial<FileBuffer>) {
super(entity);
Object.assign(this, entity);
}
toObject(): IFileBuffer {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IFileBuffer;
}
toDTO(): FileBufferDto {
return this.toObject() as unknown as FileBufferDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/index.ts
================================================
export * from './account.entity';
export * from './database-entity';
export * from './directory-watcher.entity';
export * from './file-buffer.entity';
export * from './post-event.entity';
export * from './post-queue-record.entity';
export * from './post-record.entity';
export * from './settings.entity';
export * from './submission-file.entity';
export * from './submission.entity';
export * from './tag-converter.entity';
export * from './tag-group.entity';
export * from './user-converter.entity';
export * from './user-specified-website-options.entity';
export * from './website-data.entity';
export * from './website-options.entity';
================================================
FILE: apps/client-server/src/app/drizzle/models/notification.entity.ts
================================================
import { INotification } from '@postybirb/types';
import { instanceToPlain } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
export class Notification extends DatabaseEntity implements INotification {
title: string;
message: string;
tags: string[];
data: Record<string, unknown>;
isRead: boolean;
hasEmitted: boolean;
type: 'warning' | 'error' | 'info' | 'success';
toObject(): INotification {
return instanceToPlain(this, {}) as INotification;
}
toDTO(): INotification {
return this.toObject() as unknown as INotification;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/post-event.entity.ts
================================================
import {
AccountId,
EntityId,
IPostEvent,
IPostEventError,
IPostEventMetadata,
PostEventDto,
PostEventType,
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { Account } from './account.entity';
import { DatabaseEntity } from './database-entity';
import { PostRecord } from './post-record.entity';
export class PostEvent extends DatabaseEntity implements IPostEvent {
postRecordId: EntityId;
accountId?: AccountId;
eventType: PostEventType;
fileId?: EntityId;
sourceUrl?: string;
error?: IPostEventError;
metadata?: IPostEventMetadata;
@Type(() => PostRecord)
postRecord: PostRecord;
@Type(() => Account)
account?: Account;
constructor(entity: Partial<IPostEvent>) {
super(entity);
Object.assign(this, entity);
}
toObject(): IPostEvent {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IPostEvent;
}
toDTO(): PostEventDto {
const dto: PostEventDto = {
...this.toObject(),
account: this.account?.toDTO(),
};
return dto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/post-queue-record.entity.ts
================================================
import {
EntityId,
IPostQueueRecord,
PostQueueRecordDto,
SubmissionId
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
import { PostRecord } from './post-record.entity';
import { Submission } from './submission.entity';
export class PostQueueRecord
extends DatabaseEntity
implements IPostQueueRecord
{
postRecordId: EntityId;
submissionId: SubmissionId;
@Type(() => PostRecord)
postRecord: PostRecord;
@Type(() => Submission)
submission: Submission;
toObject(): IPostQueueRecord {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IPostQueueRecord;
}
toDTO(): PostQueueRecordDto {
return this.toObject() as unknown as PostQueueRecordDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/post-record.entity.ts
================================================
import {
EntityId,
IPostRecord,
PostRecordDto,
PostRecordResumeMode,
PostRecordState,
SubmissionId,
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
import { PostEvent } from './post-event.entity';
import { PostQueueRecord } from './post-queue-record.entity';
import { Submission } from './submission.entity';
export class PostRecord extends DatabaseEntity implements IPostRecord {
postQueueRecordId: EntityId;
submissionId: SubmissionId;
@Type(() => Submission)
submission: Submission;
/**
* Reference to the originating NEW PostRecord for this chain.
* null for NEW records (they ARE the origin).
*/
originPostRecordId?: EntityId;
/**
* The originating NEW PostRecord (resolved relation).
*/
@Type(() => PostRecord)
origin?: PostRecord;
/**
* All CONTINUE/RETRY PostRecords that chain to this origin.
*/
@Type(() => PostRecord)
chainedRecords?: PostRecord[];
completedAt?: string;
state: PostRecordState;
resumeMode: PostRecordResumeMode;
// eslint-disable-next-line @typescript-eslint/no-useless-constructor
constructor(entity: Partial<IPostRecord>) {
super(entity);
}
@Type(() => PostEvent)
events: PostEvent[];
@Type(() => PostQueueRecord)
postQueueRecord: PostQueueRecord;
toObject(): IPostRecord {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IPostRecord;
}
toDTO(): PostRecordDto {
const dto: PostRecordDto = {
...this.toObject(),
events: this.events?.map((event) => event.toDTO()),
postQueueRecord: this.postQueueRecord?.toDTO(),
};
return dto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/settings.entity.ts
================================================
import {
ISettings,
ISettingsOptions,
SettingsConstants,
SettingsDto,
} from '@postybirb/types';
import { instanceToPlain } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
export class Settings extends DatabaseEntity implements ISettings {
profile: string;
settings: ISettingsOptions = { ...SettingsConstants.DEFAULT_SETTINGS };
toObject(): ISettings {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as ISettings;
}
toDTO(): SettingsDto {
return this.toObject() as unknown as SettingsDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/submission-file.entity.ts
================================================
import {
EntityId,
FileSubmissionMetadata,
ISubmissionFile,
ISubmissionFileDto,
SubmissionFileMetadata,
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { PostyBirbDatabase } from '../postybirb-database/postybirb-database';
import { DatabaseEntity } from './database-entity';
import { FileBuffer } from './file-buffer.entity';
import { Submission } from './submission.entity';
export class SubmissionFile extends DatabaseEntity implements ISubmissionFile {
submissionId: EntityId;
primaryFileId: EntityId;
altFileId: EntityId;
thumbnailId: EntityId;
@Type(() => Submission)
submission: Submission<FileSubmissionMetadata>;
fileName: string;
hash: string;
mimeType: string;
@Type(() => FileBuffer)
file: FileBuffer;
@Type(() => FileBuffer)
thumbnail?: FileBuffer;
@Type(() => FileBuffer)
altFile?: FileBuffer;
hasThumbnail: boolean;
hasAltFile: boolean;
hasCustomThumbnail: boolean;
size: number;
width: number;
height: number;
metadata: SubmissionFileMetadata;
order: number;
constructor(entity: Partial<SubmissionFile>) {
super(entity);
Object.assign(this, entity);
}
toObject(): ISubmissionFile {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as ISubmissionFile;
}
toDTO(): ISubmissionFileDto {
return this.toObject() as unknown as ISubmissionFileDto;
}
/**
* Load the submission file from the database
* More of a workaround for the lack of proper ORM support with
* blob relations loading from nested with queries.
*/
public async load(fileTarget?: 'file' | 'thumbnail' | 'alt') {
const db = new PostyBirbDatabase('FileBufferSchema');
if (fileTarget) {
switch (fileTarget) {
case 'file':
this.file = await db.findById(this.primaryFileId);
break;
case 'thumbnail':
this.thumbnail = await db.findById(this.thumbnailId);
break;
case 'alt':
this.altFile = await db.findById(this.altFileId);
break;
default:
throw new Error('Invalid file target');
}
return;
}
this.file = await db.findById(this.primaryFileId);
if (this.thumbnailId) {
this.thumbnail = await db.findById(this.thumbnailId);
}
if (this.altFileId) {
this.altFile = await db.findById(this.altFileId);
}
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/submission.entity.ts
================================================
import {
ISubmission,
ISubmissionDto,
ISubmissionMetadata,
ISubmissionScheduleInfo,
ScheduleType,
SubmissionType,
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
import { PostQueueRecord } from './post-queue-record.entity';
import { PostRecord } from './post-record.entity';
import { SubmissionFile } from './submission-file.entity';
import { WebsiteOptions } from './website-options.entity';
export class Submission<T extends ISubmissionMetadata = ISubmissionMetadata>
extends DatabaseEntity
implements ISubmission<T>
{
type: SubmissionType;
@Type(() => WebsiteOptions)
options: WebsiteOptions[];
@Type(() => PostQueueRecord)
postQueueRecord?: PostQueueRecord;
isScheduled = false;
isTemplate = false;
isMultiSubmission = false;
isArchived = false;
isInitialized = false;
schedule: ISubmissionScheduleInfo = {
scheduleType: ScheduleType.NONE,
};
@Type(() => SubmissionFile)
files: SubmissionFile[];
metadata: T;
@Type(() => PostRecord)
posts: PostRecord[];
order: number;
constructor(entity: Partial<ISubmission<T>>) {
super(entity);
Object.assign(this, entity);
}
toObject(): ISubmission {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as ISubmission;
}
toDTO(): ISubmissionDto {
const dto: ISubmissionDto = {
...this.toObject(),
files: this.files?.map((file) => file.toDTO()),
options: this.options?.map((option) => option.toDTO()),
posts: this.posts?.map((post) => post.toDTO()),
postQueueRecord: this.postQueueRecord?.toDTO(),
validations: [],
};
return dto;
}
getSubmissionName(): string {
if (this.options?.length) {
return this.options.find((o) => o.isDefault)?.data.title;
}
return 'Unknown';
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/tag-converter.entity.ts
================================================
import { ITagConverter, TagConverterDto } from '@postybirb/types';
import { instanceToPlain } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
export class TagConverter extends DatabaseEntity implements ITagConverter {
public readonly tag: string;
public convertTo: Record<string, string> = {};
constructor(entity: ITagConverter) {
super(entity);
Object.assign(this, entity);
}
toObject(): ITagConverter {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as ITagConverter;
}
toDTO(): TagConverterDto {
return this.toObject() as unknown as TagConverterDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/tag-group.entity.ts
================================================
import { ITagGroup, TagGroupDto } from '@postybirb/types';
import { instanceToPlain } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
export class TagGroup extends DatabaseEntity implements ITagGroup {
name: string;
tags: string[] = [];
toObject(): ITagGroup {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as ITagGroup;
}
toDTO(): TagGroupDto {
return this.toObject() as unknown as TagGroupDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/user-converter.entity.ts
================================================
import { IUserConverter, UserConverterDto } from '@postybirb/types';
import { instanceToPlain } from 'class-transformer';
import { DatabaseEntity } from './database-entity';
export class UserConverter extends DatabaseEntity implements IUserConverter {
public readonly username: string;
public convertTo: Record<string, string> = {};
constructor(entity: IUserConverter) {
super(entity);
Object.assign(this, entity);
}
toObject(): IUserConverter {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IUserConverter;
}
toDTO(): UserConverterDto {
return this.toObject() as unknown as UserConverterDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/user-specified-website-options.entity.ts
================================================
import {
AccountId,
DynamicObject,
IUserSpecifiedWebsiteOptions,
SubmissionType,
UserSpecifiedWebsiteOptionsDto
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { Account } from './account.entity';
import { DatabaseEntity } from './database-entity';
export class UserSpecifiedWebsiteOptions
extends DatabaseEntity
implements IUserSpecifiedWebsiteOptions
{
accountId: AccountId;
@Type(() => Account)
account: Account;
type: SubmissionType;
options: DynamicObject;
toObject(): IUserSpecifiedWebsiteOptions {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IUserSpecifiedWebsiteOptions;
}
toDTO(): UserSpecifiedWebsiteOptionsDto {
return this.toObject() as unknown as UserSpecifiedWebsiteOptionsDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/website-data.entity.ts
================================================
import {
DynamicObject,
IWebsiteData,
IWebsiteDataDto
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { Account } from './account.entity';
import { DatabaseEntity } from './database-entity';
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export class WebsiteData<T extends DynamicObject = any>
extends DatabaseEntity
implements IWebsiteData<T>
{
data: T = {} as T;
@Type(() => Account)
account: Account;
toObject(): IWebsiteData {
return instanceToPlain(this) as IWebsiteData;
}
toDTO(): IWebsiteDataDto {
return this.toObject() as unknown as IWebsiteDataDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/models/website-options.entity.ts
================================================
import {
AccountId,
IWebsiteFormFields,
IWebsiteOptions,
SubmissionId,
WebsiteOptionsDto,
} from '@postybirb/types';
import { instanceToPlain, Type } from 'class-transformer';
import { BaseWebsiteOptions } from '../../websites/models/base-website-options';
import { Account } from './account.entity';
import { DatabaseEntity } from './database-entity';
import { Submission } from './submission.entity';
export class WebsiteOptions extends DatabaseEntity implements IWebsiteOptions {
accountId: AccountId;
@Type(() => Account)
account: Account;
submissionId: SubmissionId;
@Type(() => Submission)
submission: Submission;
data: IWebsiteFormFields = new BaseWebsiteOptions();
isDefault: boolean;
constructor(entity: Partial<WebsiteOptions>) {
super(entity);
Object.assign(this, entity);
}
toObject(): IWebsiteOptions {
return instanceToPlain(this, {
enableCircularCheck: true,
}) as IWebsiteOptions;
}
toDTO(): WebsiteOptionsDto {
return {
...this.toObject(),
submission: this.submission?.toDTO(),
} as unknown as WebsiteOptionsDto;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/postybirb-database/find-options.type.ts
================================================
export type FindOptions = {
failOnMissing?: boolean;
};
================================================
FILE: apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.spec.ts
================================================
import { clearDatabase, Schemas } from '@postybirb/database';
import { eq as equals } from 'drizzle-orm';
import 'reflect-metadata';
import { PostyBirbDatabase } from './postybirb-database';
describe('PostyBirbDatabase', () => {
let service: PostyBirbDatabase<'AccountSchema'>;
beforeEach(() => {
clearDatabase();
service = new PostyBirbDatabase('AccountSchema');
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should insert and delete', async () => {
const account = await service.insert({
name: 'test',
website: 'test',
groups: [],
});
const accounts = await service.findAll();
expect(accounts).toHaveLength(1);
expect(accounts[0].id).toBe(account.id);
expect(accounts[0].name).toBe('test');
expect(accounts[0].website).toBe('test');
await service.deleteById([account.id]);
const accountsAfterDelete = await service.findAll();
expect(accountsAfterDelete).toHaveLength(0);
});
it('should find by id', async () => {
const account = await service.insert({
name: 'test',
website: 'test',
groups: [],
});
const foundAccount = await service.findById(account.id, {
failOnMissing: true,
});
expect(foundAccount).toBeTruthy();
expect(foundAccount.id).toBe(account.id);
expect(foundAccount.name).toBe('test');
expect(foundAccount.website).toBe('test');
const notFoundAccount = await service.findById('not-found', {
failOnMissing: false,
});
expect(notFoundAccount).toBeNull();
});
it('should throw on find by id not found', async () => {
await expect(
service.findById('not-found', { failOnMissing: true }),
).rejects.toThrow('Record with id not-found not found');
});
it('should update', async () => {
const account = await service.insert({
name: 'test',
website: 'test',
groups: [],
});
await service.update(account.id, {
name: 'test2',
});
const updatedAccount = await service.findById(account.id, {
failOnMissing: true,
});
expect(updatedAccount).toBeTruthy();
expect(updatedAccount.id).toBe(account.id);
expect(updatedAccount.name).toBe('test2');
});
it('should find one', async () => {
await service.insert({
name: 'test',
website: 'test',
groups: [],
});
const foundAccount = await service.findOne({
where: (account, { eq }) => eq(account.name, 'test'),
});
expect(foundAccount).toBeTruthy();
expect(foundAccount?.name).toBe('test');
});
it('should find many', async () => {
await service.insert({
name: 'test',
website: 'test',
groups: [],
});
await service.insert({
name: 'test2',
website: 'test',
groups: [],
});
const foundAccounts = await service.find({
where: (account, { eq }) => eq(account.website, 'test'),
});
expect(foundAccounts).toHaveLength(2);
});
it('should return empty array on find many not found', async () => {
const foundAccounts = await service.find({
where: (account, { eq }) => eq(account.website, 'test'),
});
expect(foundAccounts).toHaveLength(0);
});
it('should notify subscribers on create', async () => {
const subscriber = jest.fn();
service.subscribe('AccountSchema', subscriber);
const entity = await service.insert({
name: 'test',
website: 'test',
groups: [],
});
expect(subscriber).toHaveBeenCalledWith([entity.id], 'insert');
});
it('should select', async () => {
await service.insert({
name: 'test',
website: 'test',
groups: [],
});
const foundAccounts = await service.select(
equals(Schemas.AccountSchema.name, 'test'),
);
expect(foundAccounts).toHaveLength(1);
});
});
================================================
FILE: apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { NotFoundException } from '@nestjs/common';
import {
getDatabase,
Insert,
PostyBirbDatabaseType,
SchemaKey,
Schemas,
Select,
} from '@postybirb/database';
import { EntityId, NULL_ACCOUNT_ID } from '@postybirb/types';
import { eq, inArray, KnownKeysOnly, SQL } from 'drizzle-orm';
import {
DBQueryConfig,
ExtractTablesWithRelations,
} from 'drizzle-orm/relations';
import { fromDatabaseRecord } from '../models';
import { FindOptions } from './find-options.type';
import {
DatabaseSchemaEntityMap,
DatabaseSchemaEntityMapConst,
} from './schema-entity-map';
type ExtractedRelations = ExtractTablesWithRelations<typeof Schemas>;
type Relation<TSchemaKey extends SchemaKey> =
PostyBirbDatabaseType['_']['schema'][TSchemaKey];
export type Action = 'delete' | 'insert' | 'update';
type SubscribeCallback = (ids: EntityId[], action: Action) => void;
export class PostyBirbDatabase<
TSchemaKey extends SchemaKey,
TEntityClass = DatabaseSchemaEntityMap[TSchemaKey],
> {
public readonly db: PostyBirbDatabaseType;
private static readonly subscribers: Record<
SchemaKey,
Array<SubscribeCallback>
> = {
AccountSchema: [],
DirectoryWatcherSchema: [],
FileBufferSchema: [],
PostEventSchema: [],
PostQueueRecordSchema: [],
PostRecordSchema: [],
SettingsSchema: [],
SubmissionFileSchema: [],
SubmissionSchema: [],
TagConverterSchema: [],
TagGroupSchema: [],
UserConverterSchema: [],
UserSpecifiedWebsiteOptionsSchema: [],
WebsiteDataSchema: [],
WebsiteOptionsSchema: [],
NotificationSchema: [],
CustomShortcutSchema: [],
};
constructor(
private readonly schemaKey: TSchemaKey,
private readonly load?: DBQueryConfig<
'many',
true,
ExtractedRelations,
Relation<TSchemaKey>
>['with'],
) {
this.db = getDatabase();
}
public subscribe(key: SchemaKey[], callback: SubscribeCallback): this;
public subscribe(key: SchemaKey, callback: SubscribeCallback): this;
public subscribe(
key: SchemaKey | SchemaKey[],
callback: SubscribeCallback,
): this {
if (Array.isArray(key)) {
key.forEach((k) => this.subscribe(k, callback));
return this;
}
if (!PostyBirbDatabase.subscribers[key]) {
PostyBirbDatabase.subscribers[key] = [];
}
PostyBirbDatabase.subscribers[key].push(callback);
return this;
}
private notify(ids: EntityId[], action: Action) {
PostyBirbDatabase.subscribers[this.schemaKey].forEach((callback) =>
callback(ids, action),
);
}
/**
* Forcefully calls notify.
* To be used where database updates occur outside of normal db calls
* such as during transaction and direct db mutations.
*
* @param {EntityId[]} ids
* @param {Action} action
*/
public forceNotify(ids: EntityId[], action: Action) {
this.notify(ids, action);
}
/**
* Static method to notify subscribers for a given schema.
* Used by TransactionContext to trigger notifications after commit.
*
* @param {SchemaKey} schemaKey - The schema key to notify subscribers for
* @param {EntityId[]} ids - The entity IDs that were affected
* @param {Action} action - The action that occurred (insert, update, delete)
*/
public static notifySubscribers(
schemaKey: SchemaKey,
ids: EntityId[],
action: Action,
) {
PostyBirbDatabase.subscribers[schemaKey]?.forEach((callback) =>
callback(ids, action),
);
}
public get EntityClass() {
return DatabaseSchemaEntityMapConst[this.schemaKey];
}
public get schemaEntity() {
return Schemas[this.schemaKey];
}
private classConverter(value: any[]): TEntityClass[];
private classConverter(value: any): TEntityClass;
private classConverter(value: any | any[]): TEntityClass | TEntityClass[] {
if (Array.isArray(value)) {
return fromDatabaseRecord(this.EntityClass, value);
}
return fromDatabaseRecord(this.EntityClass, value);
}
public async insert(value: Insert<TSchemaKey>): Promise<TEntityClass>;
public async insert(value: Insert<TSchemaKey>[]): Promise<TEntityClass[]>;
public async insert(
value: Insert<TSchemaKey> | Insert<TSchemaKey>[],
): Promise<TEntityClass | TEntityClass[]> {
const insertQuery = this.db
.insert(this.schemaEntity)
.values(value)
.returning();
// After calling .returning(), the result is always an array of the selected fields
const inserts = (await insertQuery) as Array<Select<TSchemaKey>>;
this.notify(
inserts.map((insert) => insert.id),
'insert',
);
const result = await Promise.all(
inserts.map((insert) => this.findById(insert.id)),
);
return Array.isArray(value) ? result : result[0];
}
public async deleteById(ids: EntityId[]) {
if (ids.find((id) => id === NULL_ACCOUNT_ID)) {
throw new Error('Cannot delete the null account');
}
const result = await this.db
.delete(this.schemaEntity)
.where(inArray(this.schemaEntity.id, ids));
this.notify(ids, 'delete');
return result;
}
public async findById(
id: EntityId,
options?: FindOptions,
load?: DBQueryConfig<
'many',
true,
ExtractedRelations,
Relation<TSchemaKey>
>['with'],
): Promise<TEntityClass | null> {
const record = await this.db.query[this.schemaKey].findFirst({
where: eq(this.schemaEntity.id, id),
with: {
...(load ?? this.load ?? {}),
},
});
if (!record && options?.failOnMissing) {
throw new NotFoundException(`Record with id ${id} not found`);
}
return record ? (this.classConverter(record) as TEntityClass) : null;
}
public async select(query: SQL): Promise<TEntityClass[]> {
const records = await this.db.select().from(this.schemaEntity).where(query);
return this.classConverter(records);
}
public async find<
TConfig extends DBQueryConfig<
'many',
true,
ExtractedRelations,
Relation<TSchemaKey>
>,
>(
query: KnownKeysOnly<
TConfig,
DBQueryConfig<'many', true, ExtractedRelations, Relation<TSchemaKey>>
>,
): Promise<TEntityClass[]> {
const record: any[] =
(await this.db.query[
this.schemaKey as keyof PostyBirbDatabaseType
].findMany({
...query,
with: query.with
? query.with
: {
...(this.load ?? {}),
},
})) ?? [];
return this.classConverter(record);
}
public async findOne<
TSelection extends Omit<
DBQueryConfig<'many', true, ExtractedRelations, Relation<TSchemaKey>>,
'limit'
>,
>(
query: KnownKeysOnly<
TSelection,
Omit<
DBQueryConfig<'many', true, ExtractedRelations, Relation<TSchemaKey>>,
'limit'
>
>,
): Promise<TEntityClass | null> {
const record = await this.db.query[
this.schemaKey as keyof PostyBirbDatabaseType
].findFirst({
...query,
with: query.with
? query.with
: {
...(this.load ?? {}),
},
});
return record ? this.classConverter(record) : null;
}
public async findAll(): Promise<TEntityClass[]> {
const records: object[] = await this.db.query[this.schemaKey].findMany({
with: {
...(this.load ?? {}),
},
});
return this.classConverter(records);
}
public async update(
id: EntityId,
set: Partial<Select<TSchemaKey>>,
): Promise<TEntityClass> {
await this.findById(id, { failOnMissing: true });
await this.db
// eslint-disable-next-line testing-library/await-async-query
.update(this.schemaEntity)
.set(set)
.where(eq(this.schemaEntity.id, id));
this.notify([id], 'update');
return this.findById(id);
}
public count(filter?: SQL): Promise<number> {
return this.db.$count(this.schemaEntity, filter);
}
}
================================================
FILE: apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.util.ts
================================================
/* eslint-disable no-param-reassign */
import { SchemaKey, Schemas } from '@postybirb/database';
import { DatabaseEntity } from '../models';
import { PostyBirbDatabase } from './postybirb-database';
export class PostyBirbDatabaseUtil {
static async saveFromEntity<T extends DatabaseEntity>(entity: T) {
const obj = entity.toObject();
let entitySchemaKey: SchemaKey | undefined;
for (const schemaKey of Object.keys(Schemas)) {
if (schemaKey === `${entity.constructor.name}Schema`) {
entitySchemaKey = schemaKey as SchemaKey;
break;
}
}
if (!entitySchemaKey) {
throw new Error(`Could not find schema for ${entity.constructor.name}`);
}
const db = new PostyBirbDatabase(entitySchemaKey);
const exists = await db.findById(entity.id);
if (exists) {
if (exists.updatedAt !== entity.updatedAt) {
throw new Error('Entity has been updated since last fetch');
}
const update = await db.update(entity.id, obj);
entity.updatedAt = update.updatedAt;
} else {
const insert = await db.insert(obj);
entity.createdAt = insert.createdAt;
entity.updatedAt = insert.updatedAt;
}
return entity;
}
}
================================================
FILE: apps/client-server/src/app/drizzle/postybirb-database/schema-entity-map.ts
================================================
import { SchemaKey } from '@postybirb/database';
import {
Account,
DirectoryWatcher,
FileBuffer,
PostEvent,
PostQueueRecord,
PostRecord,
Settings,
Submission,
SubmissionFile,
TagConverter,
TagGroup,
UserConverter,
UserSpecifiedWebsiteOptions,
WebsiteData,
WebsiteOptions,
} from '../models';
import { CustomShortcut } from '../models/custom-shortcut.entity';
import { Notification } from '../models/notification.entity';
export type DatabaseSchemaEntityMap = {
AccountSchema: InstanceType<typeof Account>;
DirectoryWatcherSchema: InstanceType<typeof DirectoryWatcher>;
FileBufferSchema: InstanceType<typeof FileBuffer>;
PostEventSchema: InstanceType<typeof PostEvent>;
PostQueueRecordSchema: InstanceType<typeof PostQueueRecord>;
PostRecordSchema: InstanceType<typeof PostRecord>;
SettingsSchema: InstanceType<typeof Settings>;
SubmissionFileSchema: InstanceType<typeof SubmissionFile>;
SubmissionSchema: InstanceType<typeof Submission>;
TagConverterSchema: InstanceType<typeof TagConverter>;
TagGroupSchema: InstanceType<typeof TagGroup>;
UserConverterSchema: InstanceType<typeof UserConverter>;
UserSpecifiedWebsiteOptionsSchema: InstanceType<
typeof UserSpecifiedWebsiteOptions
>;
WebsiteDataSchema: InstanceType<typeof WebsiteData>;
WebsiteOptionsSchema: InstanceType<typeof WebsiteOptions>;
NotificationSchema: InstanceType<typeof Notification>;
CustomShortcutSchema: InstanceType<typeof CustomShortcut>;
};
export const DatabaseSchemaEntityMapConst: Record<
SchemaKey,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
InstanceType<any>
> = {
AccountSchema: Account,
DirectoryWatcherSchema: DirectoryWatcher,
FileBufferSchema: FileBuffer,
PostEventSchema: PostEvent,
PostQueueRecordSchema: PostQueueRecord,
PostRecordSchema: PostRecord,
SettingsSchema: Settings,
SubmissionFileSchema: SubmissionFile,
SubmissionSchema: Submission,
TagConverterSchema: TagConverter,
TagGroupSchema: TagGroup,
UserConverterSchema: UserConverter,
UserSpecifiedWebsiteOptionsSchema: UserSpecifiedWebsiteOptions,
WebsiteDataSchema: WebsiteData,
WebsiteOptionsSchema: WebsiteOptions,
NotificationSchema: Notification,
CustomShortcutSchema: CustomShortcut,
};
================================================
FILE: apps/client-server/src/app/drizzle/transaction-context.ts
================================================
import type { PostyBirbDatabaseType, SchemaKey } from '@postybirb/database';
import { Schemas } from '@postybirb/database';
import { Logger } from '@postybirb/logger';
import { EntityId } from '@postybirb/types';
import { eq } from 'drizzle-orm';
import { PostyBirbDatabase } from './postybirb-database/postybirb-database';
interface TrackedEntity {
schemaKey: SchemaKey;
id: EntityId;
}
/**
* A transaction-like wrapper that tracks created entities and provides
* automatic cleanup on failure. This works around drizzle-orm's synchronous
* transaction requirement with better-sqlite3.
*/
export class TransactionContext {
private readonly logger = Logger();
private readonly createdEntities: TrackedEntity[] = [];
private readonly db: PostyBirbDatabaseType;
constructor(db: PostyBirbDatabaseType) {
this.db = db;
}
/**
* Track an entity that was created during this operation.
* If the operation fails, this entity will be deleted.
*/
track(schemaKey: SchemaKey, id: EntityId): void {
this.createdEntities.push({ schemaKey, id });
}
/**
* Track multiple entities that were created during this operation.
*/
trackMany(schemaKey: SchemaKey, ids: EntityId[]): void {
ids.forEach((id) => this.track(schemaKey, id));
}
/**
* Get the database instance for performing operations.
*/
getDb(): PostyBirbDatabaseType {
return this.db;
}
/**
* Cleanup all tracked entities in reverse order (LIFO).
* This is called automatically on failure.
*/
async cleanup(): Promise<void> {
this.logger.warn(
`Rolling back transaction: cleaning up ${this.createdEntities.length} entities`,
);
// Delete in reverse order (most recently created first)
const entities = [...this.createdEntities].reverse();
for (const { schemaKey, id } of entities) {
try {
const schema = Schemas[schemaKey];
await this.db.delete(schema).where(eq(schema.id, id));
this.logger.debug(`Cleaned up ${String(schemaKey)} entity: ${id}`);
} catch (err) {
this.logger.error(
`Failed to cleanup ${String(schemaKey)} entity ${id}: ${err.message}`,
err.stack,
);
}
}
}
/**
* Clear tracked entities and notify subscribers (called on successful completion).
*
* NOTE: Currently only tracks inserts. If update/delete tracking is needed in the future,
* add trackUpdate() and trackDelete() methods that store the action type alongside the entity.
*/
commit(): void {
// Group tracked entities by schemaKey
const bySchema = new Map<SchemaKey, EntityId[]>();
for (const { schemaKey, id } of this.createdEntities) {
const existing = bySchema.get(schemaKey);
if (existing) {
existing.push(id);
} else {
bySchema.set(schemaKey, [id]);
}
}
// Notify subscribers for each schema
for (const [schemaKey, ids] of bySchema) {
PostyBirbDatabase.notifySubscribers(schemaKey, ids, 'insert');
}
this.createdEntities.length = 0;
}
}
/**
* Execute an operation with automatic cleanup on failure.
*
* @example
* ```typescript
* const result = await withTransactionContext(
* this.fileRepository.db,
* async (ctx) => {
* const entity = await createEntity(...);
* ctx.track('SubmissionFileSchema', entity.id);
*
* const buffer = await createBuffer(...);
* ctx.track('FileBufferSchema', buffer.id);
*
* return entity;
* }
* );
* ```
*/
export async function withTransactionContext<T>(
db: PostyBirbDatabaseType,
operation: (ctx: TransactionContext) => Promise<T>,
): Promise<T> {
const ctx = new TransactionContext(db);
try {
const result = await operation(ctx);
ctx.commit();
return result;
} catch (err) {
await ctx.cleanup();
throw err;
}
}
================================================
FILE: apps/client-server/src/app/file/file.controller.ts
================================================
import {
BadRequestException,
Controller,
Get,
Param,
Res,
} from '@nestjs/common';
import { ApiNotFoundResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { EntityId, IFileBuffer } from '@postybirb/types';
import { SubmissionFile } from '../drizzle/models';
import { FileService } from './file.service';
@ApiTags('file')
@Controller('file')
export class FileController {
constructor(private readonly service: FileService) {}
@Get(':fileTarget/:id')
@ApiOkResponse()
@ApiNotFoundResponse()
async getThumbnail(
@Param('fileTarget') fileTarget: 'file' | 'thumbnail' | 'alt',
@Param('id') id: EntityId,
@Res() response,
) {
const submissionFile = await this.service.findFile(id);
await submissionFile.load(fileTarget);
const imageProvidingEntity = this.getFileBufferForTarget(
fileTarget,
submissionFile,
);
if (!imageProvidingEntity) {
throw new BadRequestException(`No ${fileTarget} found for file ${id}`);
}
response.contentType(imageProvidingEntity.mimeType);
response.send(imageProvidingEntity.buffer);
}
private getFileBufferForTarget(
fileTarget: 'file' | 'thumbnail' | 'alt',
submissionFile: SubmissionFile,
): IFileBuffer | undefined {
switch (fileTarget) {
case 'file':
return submissionFile.file;
case 'thumbnail':
return submissionFile.thumbnail;
case 'alt':
return submissionFile.altFile;
default:
throw new BadRequestException('Invalid file target');
}
}
}
================================================
FILE: apps/client-server/src/app/file/file.module.ts
================================================
import { Module } from '@nestjs/common';
import { ImageProcessingModule } from '../image-processing/image-processing.module';
import { FileController } from './file.controller';
import { FileService } from './file.service';
import { CreateFileService } from './services/create-file.service';
import { UpdateFileService } from './services/update-file.service';
@Module({
imports: [ImageProcessingModule],
controllers: [FileController],
providers: [FileService, CreateFileService, UpdateFileService],
exports: [FileService],
})
export class FileModule {}
================================================
FILE: apps/client-server/src/app/file/file.service.spec.ts
================================================
import { Test, TestingModule } from '@nestjs/testing';
import { clearDatabase } from '@postybirb/database';
import { PostyBirbDirectories, writeSync } from '@postybirb/fs';
import { FileSubmission, SubmissionType } from '@postybirb/types';
import { readFileSync } from 'fs';
import { join } from 'path';
import { AccountService } from '../account/account.service';
import { CustomShortcutsService } from '../custom-shortcuts/custom-shortcuts.service';
import { SubmissionFile } from '../drizzle/models';
import { PostyBirbDatabase } from '../drizzle/postybirb-database/postybirb-database';
import { FileConverterService } from '../file-converter/file-converter.service';
import { FormGeneratorService } from '../form-generator/form-generator.service';
import { DescriptionParserService } from '../post-parsers/parsers/description-parser.service';
import { TagParserService } from '../post-parsers/parsers/tag-parser.service';
import { TitleParser } from '../post-parsers/parsers/title-parser';
import { PostParsersService } from '../post-parsers/post-parsers.service';
import { SettingsService } from '../settings/settings.service';
import { CreateSubmissionDto } from '../submission/dtos/create-submission.dto';
import { FileSubmissionService } from '../submission/services/file-submission.service';
import { MessageSubmissionService } from '../submission/services/message-submission.service';
import { SubmissionService } from '../submission/services/submission.service';
import { TagConvertersService } from '../tag-converters/tag-converters.service';
import { UserConvertersService } from '../user-converters/user-converters.service';
import { UserSpecifiedWebsiteOptionsService } from '../user-specified-website-options/user-specified-website-options.service';
import { ValidationService } from '../validation/validation.service';
import { WebsiteOptionsService } from '../website-options/website-options.service';
import { WebsiteImplProvider } from '../websites/implementations/provider';
import { WebsiteRegistryService } from '../websites/website-registry.service';
import { FileService } from './file.service';
import { MulterFileInfo } from './models/multer-file-info';
import { CreateFileService } from './services/create-file.service';
import { UpdateFileService } from './services/update-file.service';
import { SharpInstanceManager } from '../image-processing/sharp-instance-manager';
describe('FileService', () => {
let testFile: Buffer | null = null;
let testFile2: Buffer | null = null;
let service: FileService;
let submissionService: SubmissionService;
let module: TestingModule;
let fileBufferRepository: PostyBirbDatabase<'FileBufferSchema'>;
async function createSubmission() {
const dto = new CreateSubmissionDto();
dto.name = 'test';
dto.type = SubmissionType.MESSAGE; // Use message submission just for the sake of insertion
const record = await submissionService.create(dto);
return record;
}
function createMulterData(path: string): MulterFileInfo {
return {
fieldname: 'file',
originalname: 'small_image.jpg',
encoding: '',
mimetype: 'image/jpeg',
size: testFile.length,
destination: '',
filename: 'small_image.jpg',
path,
origin: undefined,
};
}
function createMulterData2(path: string): MulterFileInfo {
return {
fieldname: 'file',
originalname: 'png_with_alpha.png',
encoding: '',
mimetype: 'image/png',
size: testFile2.length,
destination: '',
filename: 'png_with_alpha.jpg',
path,
origin: undefined,
};
}
function setup(): string[] {
const path = `${PostyBirbDirectories.DATA_DIRECTORY}/${Date.now()}.jpg`;
const path2 = `${PostyBirbDirectories.DATA_DIRECTORY}/${Date.now()}.png`;
writeSync(path, testFile);
writeSync(path2, testFile2);
return [path, path2];
}
beforeAll(() => {
testFile = readFileSync(
join(__dirname, '../../test-files/small_image.jpg'),
);
testFile2 = readFileSync(
join(__dirname, '../../test-files/png_with_alpha.png'),
);
});
beforeEach(async () => {
clearDatabase();
module = await Test.createTestingModule({
providers: [
UserSpecifiedWebsiteOptionsService,
SubmissionService,
CreateFileService,
UpdateFileService,
SharpInstanceManager,
FileService,
ValidationService,
SubmissionService,
FileSubmissionService,
MessageSubmissionService,
AccountService,
WebsiteRegistryService,
WebsiteOptionsService,
WebsiteImplProvider,
PostParsersService,
TagParserService,
DescriptionParserService,
TitleParser,
TagConvertersService,
SettingsService,
FormGeneratorService,
FileConverterService,
CustomShortcutsService,
UserConvertersService,
],
}).compile();
fileBufferRepository = new PostyBirbDatabase('FileBufferSchema');
service = module.get<FileService>(FileService);
submissionService = module.get<SubmissionService>(SubmissionService);
const accountService = module.get<AccountService>(AccountService);
await accountService.onModuleInit();
});
async function loadBuffers(rec: SubmissionFile) {
// !bug - https://github.com/drizzle-team/drizzle-orm/issues/3497
// eslint-disable-next-line no-param-reassign
rec.file = await fileBufferRepository.findById(rec.primaryFileId);
// eslint-disable-next-line no-param-reassign
rec.thumbnail = rec.thumbnailId
? await fileBufferRepository.findById(rec.thumbnailId)
: undefined;
// eslint-disable-next-line no-param-reassign
rec.altFile = rec.altFileId
? await fileBufferRepository.findById(rec.altFileId)
: undefined;
}
it('should be defined', () => {
expect(service).toBeDefined();
});
it('should create submission file', async () => {
const path = setup();
const submission = await createSubmission();
const fileInfo = createMulterData(path[0]);
const file = await service.create(
fileInfo,
submission as unknown as FileSubmission,
);
await loadBuffers(file);
expect(file.file).toBeDefined();
expect(file.thumbnail).toBeDefined();
expect(file.thumbnail.fileName.startsWith('thumbnail_')).toBe(true);
expect(file.fileName).toBe(fileInfo.originalname);
expect(file.size).toBe(fileInfo.size);
expect(file.hasThumbnail).toBe(true);
expect(file.hasCustomThumbnail).toBe(false);
expect(file.height).toBe(202);
expect(file.width).toBe(138);
expect(file.file.size).toBe(fileInfo.size);
expect(file.file.height).toBe(202);
expect(file.file.width).toBe(138);
expect(file.file.submissionFileId).toEqual(file.id);
expect(file.file.mimeType).toEqual(fileInfo.mimetype);
expect(file.file.buffer).toEqual(testFile);
});
it('should not update submission file when hash is same', async () => {
const path = setup();
const submission = await createSubmission();
const fileInfo = createMulterData(path[0]);
const file = await service.create(
fileInfo,
submission as unknown as FileSubmission,
);
await loadBuffers(file);
expect(file.file).toBeDefined();
const path2 = setup();
const updateFileInfo: MulterFileInfo = {
fieldname: 'file',
originalname: 'small_image.jpg',
encoding: '',
mimetype: 'image/png',
size: testFile.length,
destination: '',
filename: 'small_image.jpg',
path: path2[0],
origin: undefined,
};
const updatedFile = await service.update(updateFileInfo, file.id, false);
await loadBuffers(updatedFile);
expect(updatedFile.file).toBeDefined();
expect(updatedFile.thumbnail).toBeDefined();
expect(updatedFile.fileName).toBe(updateFileInfo.originalname);
expect(updatedFile.size).toBe(updateFileInfo.size);
expect(updatedFile.hasThumbnail).toBe(true);
expect(updatedFile.hasCustomThumbnail).toBe(false);
expect(updatedFile.height).toBe(202);
expect(updatedFile.width).toBe(138);
expect(updatedFile.file.size).toBe(updateFileInfo.size);
expect(updatedFile.file.height).toBe(202);
expect(updatedFile.file.width).toBe(138);
expect(updatedFile.file.submissionFileId).toEqual(file.id);
expect(updatedFile.file.mimeType).not.toEqual(updateFileInfo.mimetype);
expect(updatedFile.file.buffer).toEqual(testFile);
});
it('should update submission primary file', async () => {
const path = setup();
const submission = await createSubmission();
const fileInfo = createMulterData(path[0]);
const file = await service.create(
fileInfo,
submission as unknown as FileSubmission,
);
await loadBuffers(file);
expect(file.file).toBeDefined();
const path2 = setup();
const updateFileInfo = createMulterData2(path2[1]);
const updatedFile = await service.update(updateFileInfo, file.id, false);
await loadBuffers(updatedFile);
expect(updatedFile.file).toBeDefined();
expect(updatedFile.thumbnail).toBeDefined();
expect(updatedFile.fileName).toBe(updateFileInfo.filename);
expect(updatedFile.size).toBe(updateFileInfo.size);
expect(updatedFile.hasThumbnail).toBe(true);
expect(updatedFile.hasCustomThumbnail).toBe(false);
expect(updatedFile.height).toBe(600);
expect(updatedFile.width).toBe(600);
expect(updatedFile.file.size).toBe(updateFileInfo.size);
expect(updatedFile.file.height).toBe(600);
expect(updatedFile.file.width).toBe(600);
expect(updatedFile.file.submissionFileId).toEqual(file.id);
expect(updatedFile.file.mimeType).toEqual(updateFileInfo.mimetype);
expect(updatedFile.file.buffer).toEqual(testFile2);
});
it('should cleanup entities on transaction failure', async () => {
const path = setup();
const submission = await createSubmission();
const fileInfo = createMulterData(path[0]);
// Get initial count of entities
const initialFiles = await new PostyBirbDatabase(
'SubmissionFileSchema',
).findAll();
const initialBuffers = await fileBufferRepository.findAll();
const initialFileCount = initialFiles.length;
const initialBufferCount = initialBuffers.length;
// Mock a method to throw an error partway through creation
const createFileService = module.get<CreateFileService>(CreateFileService);
const originalMethod = createFileService.createFileBufferEntity;
let callCount = 0;
jest
.spyOn(createFileService, 'createFileBufferEntity')
.mockImplementation(async (...args) => {
callCount++;
// Fail on the second buffer creation (thumbnail)
if (callCount === 2) {
throw new Error('Simulated error during buffer creation');
}
return originalMethod.apply(createFileService, args);
});
// Attempt to create a file, which should fail and trigger cleanup
await expect(
service.create(fileInfo, submission as unknown as FileSubmission),
).rejects.toThrow('Simulated error during buffer creation');
// Verify that entities were cleaned up
const finalFiles = await new PostyBirbDatabase(
'SubmissionFileSchema',
).findAll();
const finalBuffers = await fileBufferRepository.findAll();
expect(finalFiles.length).toBe(initialFileCount);
expect(finalBuffers.length).toBe(initialBufferCount);
// Restore the original method
jest.restoreAllMocks();
});
});
================================================
FILE: apps/client-server/src/app/file/file.service.ts
================================================
/* eslint-disable no-param-reassign */
import { BadRequestException, Injectable } from '@nestjs/common';
import { read } from '@postybirb/fs';
import { Logger } from '@postybirb/logger';
import {
EntityId,
FileSubmission,
SubmissionFileMetadata,
} from '@postybirb/types';
import type { queueAsPromised } from 'fastq';
import fastq from 'fastq';
import { readFile } from 'fs/promises';
import { cpus } from 'os';
import { SubmissionFile } from '../drizzle/models';
import { PostyBirbDatabase } from '../drizzle/postybirb-database/postybirb-database';
import { ReorderSubmissionFilesDto } from '../submission/dtos/reorder-submission-files.dto';
import { UpdateAltFileDto } from '../submission/dtos/update-alt-file.dto';
import { MulterFileInfo, TaskOrigin } from './models/multer-file-info';
import { CreateTask, Task, UpdateTask } from './models/task';
import { TaskType } from './models/task-type.enum';
import { CreateFileService } from './services/create-file.service';
import { UpdateFileService } from './services/update-file.service';
/**
* Service that handles storing file data into database.
* @todo text encoding parsing and file name conversion (no periods)
*/
@Injectable()
export class FileService {
private readonly logger = Logger();
private readonly queue: queueAsPromised<Task, SubmissionFile> = fastq.promise<
this,
Task
>(this, this.doTask, Math.min(cpus().length, 5));
private readonly fileBufferRepository = new PostyBirbDatabase(
'FileBufferSchema',
);
private readonly fileRepository = new PostyBirbDatabase(
'SubmissionFileSchema',
);
constructor(
private readonly createFileService: CreateFileService,
private readonly updateFileService: UpdateFileService,
) {}
/**
* Deletes a file.
*
* @param {EntityId} id
* @return {*}
*/
public async remove(id: EntityId) {
this.logger.info(id, `Removing entity '${id}'`);
return this.fileRepository.deleteById([id]);
}
/**
* Queues a file to create a database record.
*
* @param {MulterFileInfo} file
* @param {FileSubmission} submission
* @return {*} {Promise<SubmissionFile>}
*/
public async create(
file: MulterFileInfo,
submission: FileSubmission,
): Promise<SubmissionFile> {
return this.queue.push({ type: TaskType.CREATE, file, submission });
}
/**
* Queues a file to update.
*
* @param {MulterFileInfo} file
* @param {EntityId} submissionFileId
* @param {boolean} forThumbnail
* @return {*} {Promise<SubmissionFile>}
*/
public async update(
file: MulterFileInfo,
submissionFileId: EntityId,
forThumbnail: boolean,
): Promise<SubmissionFile> {
return this.queue.push({
type: TaskType.UPDATE,
file,
submissionFileId,
target: forThumbnail ? 'thumbnail' : undefined,
});
}
private async doTask(task: Task): Promise<SubmissionFile> {
task.file.originalname = this.sanitizeFilename(task.file.originalname);
const buf: Buffer = await this.getFile(task.file.path, task.file.origin);
this.logger.withMetadata(task).info('Reading File');
switch (task.type) {
case TaskType.CREATE:
// eslint-disable-next-line no-case-declarations
const ct = task as CreateTask;
return this.createFileService.create(ct.file, ct.submission, buf);
case TaskType.UPDATE:
// eslint-disable-next-line no-case-declarations
const ut = task as UpdateTask;
return this.updateFileService.update(
ut.file,
ut.submissionFileId,
buf,
ut.target,
);
default:
throw new BadRequestException(`Unknown TaskType '${task.type}'`);
}
}
/**
* Removes periods from the filename.
* There is some website that doesn't like them.
*
* @param {string} filename
* @return {*} {string}
*/
private sanitizeFilename(filename: string): string {
const nameParts = filename.split('.');
const ext = nameParts.pop();
return `${nameParts.join('_')}.${ext}`;
}
private async getFile(path: string, taskOrigin: TaskOrigin): Promise<Buffer> {
switch (taskOrigin) {
case 'directory-watcher':
return readFile(path);
default:
// Approved location
return read(path);
}
}
/**
* Returns file by Id.
*
* @param {EntityId} id
*/
public async findFile(id: EntityId): Promise<SubmissionFile> {
return this.fileRepository.findById(id, { failOnMissing: true });
}
/**
* Gets the size of an alt text file without loading the buffer.
* @param {EntityId} id
*/
async getAltFileSize(id: EntityId): Promise<number> {
const altFile = await this.fileBufferRepository.findById(id, {
failOnMissing: false,
});
return altFile?.size ?? 0;
}
/**
* Gets the raw text of an alt text file.
* @param {EntityId} id
*/
async getAltText(id: EntityId): Promise<string> {
const altFile = await this.fileBufferRepository.findById(id, {
failOnMissing: true,
});
if (altFile.size) {
return altFile.buffer.toString();
}
return '';
}
/**
* Updates the raw text of an alt text file.
* @param {EntityId} id
* @param {UpdateAltFileDto} update
*/
async updateAltText(id: EntityId, update: UpdateAltFileDto) {
const buffer = Buffer.from(update.text ?? '');
return this.fileBufferRepository.update(id, {
buffer,
mimeType: 'text/plain',
size: buffer.length,
});
}
async updateMetadata(id: string, update: SubmissionFileMetadata) {
const file = await this.findFile(id);
const merged = { ...file.metadata, ...update };
await this.fileRepository.update(id, { metadata: merged });
}
async reorderFiles(update: ReorderSubmissionFilesDto) {
await Promise.all(
Object.entries(update.order).map(([id, order]) =>
this.fileRepository.update(id, { order }),
),
);
}
}
================================================
FILE: apps/client-server/src/app/file/models/multer-file-info.ts
================================================
/**
* Matches a multer file info object.
* TypeScript is not importing multer types correctly.
*
* @interface MulterFileInfo
*/
export interface MulterFileInfo {
fieldname: string;
originalname: string;
encoding: string;
mimetype: string;
size: number;
destination: string;
filename: string;
path: string;
buffer?: Buffer;
/**
* Internal origin, empty when external.
* @type {TaskOrigin}
*/
origin?: TaskOrigin;
}
export type TaskOrigin = 'directory-watcher';
================================================
FILE: apps/client-server/src/app/file/models/task-type.enum.ts
================================================
/**
* Defines the requested TaskType
*/
export enum TaskType {
CREATE = 'CREATE', // Creating a new file entity
UPDATE = 'UPDATE', // Updating a file entity
}
================================================
FILE: apps/client-server/src/app/file/models/task.ts
================================================
import { EntityId, FileSubmission } from '@postybirb/types';
import { MulterFileInfo } from './multer-file-info';
import { TaskType } from './task-type.enum';
// Task Type
export type Task = CreateTask | UpdateTask;
// Defines CreateTask Params
export type CreateTask = {
type: TaskType;
/**
* File for use.
* @type {MulterFileInfo}
*/
file: MulterFileInfo;
/**
* Submission the entities will be attached to.
* @type {FileSubmission}
*/
submission: FileSubmission;
};
// Defines UpdateTask Params
export type UpdateTask = {
type: TaskType;
/**
* Updating file.
*
* @type {MulterFileInfo}
*/
file: MulterFileInfo;
/**
* SubmissionFile being updated.
* @type {EntityId}
*/
submissionFileId: EntityId;
/**
* The target type being updates (primary on empty).
* @type {string}
*/
target?: 'thumbnail';
};
================================================
FILE: apps/client-server/src/app/file/services/create-file.service.ts
================================================
import * as rtf from '@iarna/rtf-to-html';
import { Injectable } from '@nestjs/common';
import { Insert, Select } from '@postybirb/database';
import { removeFile } from '@postybirb/fs';
import { Logger } from '@postybirb/logger';
import {
DefaultSubmissionFileMetadata,
FileSubmission,
FileType,
IFileBuffer,
} from '@postybirb/types';
import { getFileType } from '@postybirb/utils/file-type';
import { eq } from 'drizzle-orm';
import { async as hash } from 'hasha';
import { htmlToText } from 'html-to-text';
import * as mammoth from 'mammoth';
import { parse } from 'path';
import { promisify } from 'util';
import { v4 as uuid } from 'uuid';
import {
FileBuffer,
fromDatabaseRecord,
SubmissionFile,
} from '../../drizzle/models';
import { PostyBirbDatabase } from '../../drizzle/postybirb-database/postybirb-database';
import {
TransactionContext,
withTransactionContext,
} from '../../drizzle/transaction-context';
import { SharpInstanceManager } from '../../image-processing/sharp-instance-manager';
import { MulterFileInfo } from '../models/multer-file-info';
import { ImageUtil } from '../utils/image.util';
/**
* A Service that defines operations for creating a SubmissionFile.
* @class CreateFileService
*/
@Injectable()
export class CreateFileService {
private readonly logger = Logger();
private readonly fileBufferRepository = new PostyBirbDatabase(
'FileBufferSchema',
);
private readonly fileRepository = new PostyBirbDatabase(
'SubmissionFileSchema',
);
constructor(
private readonly sharpInstanceManager: SharpInstanceManager,
) {}
/**
* Creates file entity and stores it.
* @todo extra data (image resize per website)
* @todo figure out what to do about non-image
*
* @param {MulterFileInfo} file
* @param {MulterFileInfo} submission
* @param {Buffer} buf
* @return {*} {Promise<SubmissionFile>}
*/
public async create(
file: MulterFileInfo,
submission: FileSubmission,
buf: Buffer,
): Promise<SubmissionFile> {
try {
this.logger.withMetadata(file).info(`Creating SubmissionFile entity`);
const newSubmission = await withTransactionContext(
this.fileRepository.db,
async (ctx) => {
let entity = await this.createSubmissionFile(
ctx,
file,
submission,
buf,
);
if (ImageUtil.isImage(file.mimetype, true)) {
this.logger.info('[Mutation] Populating as Image');
entity = await this.populateAsImageFile(ctx, entity, file, buf);
}
if (getFileType(file.originalname) === FileType.TEXT) {
await this.createSubmissionTextAltFile(ctx, entity, file, buf);
}
const primaryFile = await this.createFileBufferEntity(
ctx,
entity,
buf,
);
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set({ primaryFileId: primaryFile.id })
.where(eq(this.fileRepository.schemaEntity.id, entity.id));
this.logger
.withMetadata({ id: entity.id })
.info('SubmissionFile Created');
return entity;
},
);
return await this.fileRepository.findById(newSubmission.id);
} catch (err) {
this.logger.error(err.message, err.stack);
throw err;
} finally {
if (!file.origin) {
removeFile(file.path);
}
}
}
/**
* Populates an alt file containing text data extracted from a file.
* Currently supports docx, rtf, and plaintext.
*
* @param {SubmissionFile} entity
* @param {MulterFileInfo} file
* @param {Buffer} buf
*/
async createSubmissionTextAltFile(
ctx: TransactionContext,
entity: SubmissionFile,
file: MulterFileInfo,
buf: Buffer,
) {
// Default to empty string - all TEXT files get an alt file
let altText = '';
if (
file.mimetype ===
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.mimetype === 'application/msword' ||
file.originalname.endsWith('.docx') ||
file.originalname.endsWith('.doc')
) {
this.logger.info('[Mutation] Creating Alt File for Text Document: DOCX');
altText = (await mammoth.extractRawText({ buffer: buf })).value;
} else if (
file.mimetype === 'application/rtf' ||
file.originalname.endsWith('.rtf')
) {
this.logger.info('[Mutation] Creating Alt File for Text Document: RTF');
const promisifiedRtf = promisify(rtf.fromString);
const rtfHtml = await promisifiedRtf(buf.toString(), {
template(_, __, content: string) {
return content;
},
});
altText = htmlToText(rtfHtml, { wordwrap: false });
} else if (
file.mimetype === 'text/plain' ||
file.originalname.endsWith('.txt')
) {
this.logger.info('[Mutation] Creating Alt File for Text Document: TXT');
altText = buf.toString();
} else {
this.logger.info(
`[Mutation] Creating empty Alt File for unsupported text format: ${file.mimetype}`,
);
}
const prettifiedBuf = Buffer.from(altText ?? '');
const altFile = await this.createFileBufferEntity(
ctx,
entity,
prettifiedBuf,
{
mimeType: 'text/plain',
fileName: `${entity.fileName}.txt`,
},
);
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set({
altFileId: altFile.id,
hasAltFile: true,
})
.where(eq(this.fileRepository.schemaEntity.id, entity.id));
this.logger.withMetadata({ id: altFile.id }).info('Alt File Created');
}
/**
* Creates a SubmissionFile with pre-populated fields.
*
* @param {MulterFileInfo} file
* @param {FileSubmission} submission
* @param {Buffer} buf
* @return {*} {Promise<SubmissionFile>}
*/
private async createSubmissionFile(
ctx: TransactionContext,
file: MulterFileInfo,
submission: FileSubmission,
buf: Buffer,
): Promise<SubmissionFile> {
const { mimetype: mimeType, originalname, size } = file;
const submissionFile: Insert<'SubmissionFileSchema'> = {
submissionId: submission.id,
mimeType,
fileName: originalname,
size,
hash: await hash(buf, { algorithm: 'sha256' }),
width: 0,
height: 0,
hasThumbnail: false,
metadata: DefaultSubmissionFileMetadata(),
order: Date.now(),
};
const sf = fromDatabaseRecord(
SubmissionFile,
await ctx
.getDb()
.insert(this.fileRepository.schemaEntity)
.values(submissionFile)
.returning(),
);
const entity = sf[0];
ctx.track('SubmissionFileSchema', entity.id);
return entity;
}
/**
* Populates SubmissionFile with Image specific fields.
* Width, Height, Thumbnail.
*
* @param {SubmissionFile} entity
* @param {MulterFileInfo} file
* @param {Buffer} buf
* @return {*} {Promise<void>}
*/
private async populateAsImageFile(
ctx: TransactionContext,
entity: SubmissionFile,
file: MulterFileInfo,
buf: Buffer,
): Promise<SubmissionFile> {
const meta = await this.sharpInstanceManager.getMetadata(buf);
const thumbnail = await this.createFileThumbnail(
ctx,
entity,
file,
buf,
);
const update: Select<typeof this.fileRepository.schemaEntity> = {
width: meta.width ?? 0,
height: meta.height ?? 0,
hasThumbnail: true,
thumbnailId: thumbnail.id,
metadata: {
...entity.metadata,
dimensions: {
default: {
width: meta.width ?? 0,
height: meta.height ?? 0,
},
},
},
};
return fromDatabaseRecord(
SubmissionFile,
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set(update)
.where(eq(this.fileRepository.schemaEntity.id, entity.id))
.returning(),
)[0];
}
/**
* Returns a thumbnail entity for a file.
*
* @param {SubmissionFile} fileEntity
* @param {MulterFileInfo} file
* @param {Buffer} imageBuffer - The source image buffer
* @return {*} {Promise<IFileBuffer>}
*/
public async createFileThumbnail(
ctx: TransactionContext,
fileEntity: SubmissionFile,
file: MulterFileInfo,
imageBuffer: Buffer,
): Promise<IFileBuffer> {
const {
buffer: thumbnailBuf,
height,
width,
mimeType: thumbnailMimeType,
} = await this.generateThumbnail(
imageBuffer,
file.mimetype,
);
// Remove existing extension and add the appropriate thumbnail extension
const fileNameWithoutExt = parse(fileEntity.fileName).name;
const thumbnailExt = thumbnailMimeType === 'image/jpeg' ? 'jpg' : 'png';
return this.createFileBufferEntity(ctx, fileEntity, thumbnailBuf, {
height,
width,
mimeType: thumbnailMimeType,
fileName: `thumbnail_${fileNameWithoutExt}.${thumbnailExt}`,
});
}
/**
* Generates a thumbnail for display at specific dimension requirements.
* Delegates to the sharp worker pool for crash isolation.
*
* @param {Buffer} imageBuffer - The source image buffer
* @param {string} sourceMimeType - The mimetype of the source image
* @param {number} [preferredDimension=400] - The preferred thumbnail dimension
* @return {*} {Promise<{ width: number; height: number; buffer: Buffer; mimeType: string }>}
*/
public async generateThumbnail(
imageBuffer: Buffer,
sourceMimeType: string,
preferredDimension = 400,
): Promise<{
width: number;
height: number;
buffer: Buffer;
mimeType: string;
}> {
const result = await this.sharpInstanceManager.generateThumbnail(
imageBuffer,
sourceMimeType,
'thumbnail',
preferredDimension,
);
return {
buffer: result.buffer,
height: result.height,
width: result.width,
mimeType: result.mimeType,
};
}
/**
* Creates a file buffer entity for storing blob data of a file.
*
* @param {File} fileEntity
* @param {Buffer} buf
* @param {string} type - thumbnail/alt/primary
* @return {*} {IFileBuffer}
*/
public async createFileBufferEntity(
ctx: TransactionContext,
fileEntity: SubmissionFile,
buf: Buffer,
opts: Select<'FileBufferSchema'> = {} as Select<'FileBufferSchema'>,
): Promise<FileBuffer> {
const { mimeType, height, width, fileName } = fileEntity;
const data: Insert<'FileBufferSchema'> = {
id: uuid(),
buffer: buf,
submissionFileId: fileEntity.id,
height,
width,
fileName,
mimeType,
size: buf.length,
...opts,
};
const result = fromDatabaseRecord(
FileBuffer,
await ctx
.getDb()
.insert(this.fileBufferRepository.schemaEntity)
.values(data)
.returning(),
)[0];
ctx.track('FileBufferSchema', result.id);
return result;
}
}
================================================
FILE: apps/client-server/src/app/file/services/update-file.service.ts
================================================
/* eslint-disable no-param-reassign */
import * as rtf from '@iarna/rtf-to-html';
import {
BadRequestException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { Logger } from '@postybirb/logger';
import { EntityId, FileType } from '@postybirb/types';
import { getFileType } from '@postybirb/utils/file-type';
import { eq } from 'drizzle-orm';
import { async as hash } from 'hasha';
import { htmlToText } from 'html-to-text';
import * as mammoth from 'mammoth';
import { parse } from 'path';
import { promisify } from 'util';
import { SubmissionFile } from '../../drizzle/models';
import { PostyBirbDatabase } from '../../drizzle/postybirb-database/postybirb-database';
import {
TransactionContext,
withTransactionContext,
} from '../../drizzle/transaction-context';
import { SharpInstanceManager } from '../../image-processing/sharp-instance-manager';
import { MulterFileInfo } from '../models/multer-file-info';
import { ImageUtil } from '../utils/image.util';
import { CreateFileService } from './create-file.service';
/**
* A Service for updating existing SubmissionFile entities.
*/
@Injectable()
export class UpdateFileService {
private readonly logger = Logger();
private readonly fileRepository = new PostyBirbDatabase(
'SubmissionFileSchema',
);
private readonly fileBufferRepository = new PostyBirbDatabase(
'FileBufferSchema',
);
constructor(
private readonly createFileService: CreateFileService,
private readonly sharpInstanceManager: SharpInstanceManager,
) {}
/**
* Creates file entity and stores it.
*
* @param {MulterFileInfo} file
* @param {MulterFileInfo} submission
* @param {Buffer} buf
* @param {string} target
* @return {*} {Promise<SubmissionFile>}
*/
public async update(
file: MulterFileInfo,
submissionFileId: EntityId,
buf: Buffer,
target?: 'thumbnail',
): Promise<SubmissionFile> {
const submissionFile = await this.findFile(submissionFileId);
await withTransactionContext(this.fileRepository.db, async (ctx) => {
if (target === 'thumbnail') {
await this.replaceFileThumbnail(ctx, submissionFile, file, buf);
} else {
await this.replacePrimaryFile(ctx, submissionFile, file, buf);
}
});
// Notify subscribers so SubmissionService emits a websocket update
this.fileRepository.forceNotify([submissionFileId], 'update');
this.fileBufferRepository.forceNotify([submissionFileId], 'update');
// return the latest
return this.findFile(submissionFileId);
}
private async replaceFileThumbnail(
ctx: TransactionContext,
submissionFile: SubmissionFile,
file: MulterFileInfo,
buf: Buffer,
) {
const thumbnailDetails = await this.getImageDetails(file, buf);
let { thumbnailId } = submissionFile;
if (!thumbnailId) {
// Create a new thumbnail buffer entity
const thumbnail = await this.createFileService.createFileBufferEntity(
ctx,
submissionFile,
thumbnailDetails.buffer,
{
width: thumbnailDetails.width,
height: thumbnailDetails.height,
mimeType: file.mimetype,
},
);
thumbnailId = thumbnail.id;
} else {
// Update existing thumbnail buffer
await ctx
.getDb()
.update(this.fileBufferRepository.schemaEntity)
.set({
buffer: thumbnailDetails.buffer,
size: thumbnailDetails.buffer.length,
mimeType: file.mimetype,
width: thumbnailDetails.width,
height: thumbnailDetails.height,
})
.where(eq(this.fileBufferRepository.schemaEntity.id, thumbnailId));
}
// Recompute hash from thumbnail buffer so the frontend cache-buster updates
const thumbnailHash = await hash(thumbnailDetails.buffer, {
algorithm: 'sha256',
});
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set({
thumbnailId,
hasCustomThumbnail: true,
hasThumbnail: true,
hash: thumbnailHash,
})
.where(eq(this.fileRepository.schemaEntity.id, submissionFile.id));
}
async replacePrimaryFile(
ctx: TransactionContext,
submissionFile: SubmissionFile,
file: MulterFileInfo,
buf: Buffer,
) {
return this.updateFileEntity(ctx, submissionFile, file, buf);
}
private async updateFileEntity(
ctx: TransactionContext,
submissionFile: SubmissionFile,
file: MulterFileInfo,
buf: Buffer,
) {
const fileHash = await hash(buf, { algorithm: 'sha256' });
// Only need to replace when unique file is given
if (submissionFile.hash !== fileHash) {
const fileType = getFileType(file.filename);
if (fileType === FileType.IMAGE) {
await this.updateImageFileProps(ctx, submissionFile, file, buf);
}
// Update submission file entity
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set({
hash: fileHash,
size: buf.length,
fileName: file.filename,
mimeType: file.mimetype,
})
.where(eq(this.fileRepository.schemaEntity.id, submissionFile.id));
// Just to get the latest data
// Duplicate props to primary file
await ctx
.getDb()
.update(this.fileBufferRepository.schemaEntity)
.set({
buffer: buf,
size: buf.length,
fileName: file.filename,
mimeType: file.mimetype,
})
.where(
eq(
this.fileBufferRepository.schemaEntity.id,
submissionFile.primaryFileId,
),
);
if (
getFileType(file.originalname) === FileType.TEXT &&
submissionFile.hasAltFile
) {
const altFileText =
(await this.repopulateTextFile(file, buf)) ||
submissionFile?.altFile?.buffer;
if (altFileText) {
await ctx
.getDb()
.update(this.fileBufferRepository.schemaEntity)
.set({
buffer: altFileText,
size: altFileText.length,
})
.where(
eq(
this.fileBufferRepository.schemaEntity.id,
submissionFile.altFile.id,
),
);
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set({
hasAltFile: true,
})
.where(eq(this.fileRepository.schemaEntity.id, submissionFile.id));
}
}
}
}
async repopulateTextFile(
file: MulterFileInfo,
buf: Buffer,
): Promise<Buffer | null> {
let altText: string;
if (
file.mimetype ===
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' ||
file.originalname.endsWith('.docx')
) {
this.logger.info('[Mutation] Updating Alt File for Text Document: DOCX');
altText = (await mammoth.extractRawText({ buffer: buf })).value;
}
if (
file.mimetype === 'application/rtf' ||
file.originalname.endsWith('.rtf')
) {
this.logger.info('[Mutation] Updating Alt File for Text Document: RTF');
const promisifiedRtf = promisify(rtf.fromString);
const rtfHtml = await promisifiedRtf(buf.toString(), {
template(_, __, content: string) {
return content;
},
});
altText = htmlToText(rtfHtml, { wordwrap: false });
}
if (file.mimetype === 'text/plain' || file.originalname.endsWith('.txt')) {
this.logger.info('[Mutation] Updating Alt File for Text Document: TXT');
altText = buf.toString();
}
return altText
? Buffer.from(altText)
: null;
}
private async updateImageFileProps(
ctx: TransactionContext,
submissionFile: SubmissionFile,
file: MulterFileInfo,
buf: Buffer,
) {
const { width, height } = await this.getImageDetails(
file,
buf,
);
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set({
width,
height,
})
.where(eq(this.fileRepository.schemaEntity.id, submissionFile.id));
await ctx
.getDb()
.update(this.fileBufferRepository.schemaEntity)
.set({
width,
height,
})
.where(
eq(
this.fileBufferRepository.schemaEntity.id,
submissionFile.primaryFileId,
),
);
// Reset metadata dimensions so they don't reference the old file size
const updatedMetadata = { ...submissionFile.metadata };
if (updatedMetadata.dimensions) {
updatedMetadata.dimensions = {
...updatedMetadata.dimensions,
default: { width, height },
};
}
await ctx
.getDb()
.update(this.fileRepository.schemaEntity)
.set({ metadata: updatedMetadata })
.where(eq(this.fileRepository.schemaEntity.id, submissionFile.id));
if (submissionFile.hasThumbnail && !submissionFile.hasCustomThumbnail) {
// Regenerate auto-thumbnail;
const {
buffer: thumbnailBuf,
width: thumbnailWidth,
height: thumbnailHeight,
mimeType: thumbnailMimeType,
} = await this.createFileService.generateThumbnail(
buf,
file.mimetype,
);
const fileNameWithoutExt = parse(file.filename).name;
const thumbnailExt = thumbnailMimeType === 'image/jpeg' ? 'jpg' : 'png';
await ctx
.getDb()
.update(this.fileBufferRepository.schemaEntity)
.set({
buffer: thumbnailBuf,
width: thumbnailWidth,
height: thumbnailHeight,
size: thumbnailBuf.length,
mimeType: thumbnailMimeType,
fileName: `thumbnail_${fileNameWithoutExt}.${thumbnailExt}`,
})
.where(
eq(
this.fileBufferRepository.schemaEntity.id,
submissionFile.thumbnailId,
),
);
}
}
/**
* Details of a multer file.
*
* @param {MulterFileInfo} file
*/
private async getImageDetails(file: MulterFileInfo, buf: Buffer) {
if (ImageUtil.isImage(file.mimetype, false)) {
const { height, width } = await this.sharpInstanceManager.getMetadata(buf);
return { buffer: buf, width, height };
}
throw new BadRequestException('File is not an image');
}
/**
* Returns file by Id.
*
* @param {EntityId} id
*/
private async findFile(id: EntityId): Promise<SubmissionFile> {
try {
const entity = await this.fileRepository.findOne({
where: (f, { eq: equals }) => equals(f.id, id),
// !bug - https://github.com/drizzle-team/drizzle-orm/issues/3497
// with: {
// thumbnail: true,
// primaryFile: true,
// altFile: true,
// },
});
return entity;
} catch (e) {
this.logger.error(e.message, e.stack);
throw new NotFoundException(id);
}
}
}
================================================
FILE: apps/client-server/src/app/file/utils/image.util.ts
================================================
/**
* Utility class for image-related checks.
*
* NOTE: Sharp image processing has been moved to SharpInstanceManager
* which runs sharp in isolated worker threads for crash protection.
* The load() and getMetadata() methods have been removed.
* Use SharpInstanceManager.getMetadata() or SharpInstanceManager.resizeForPost() instead.
*/
export class ImageUtil {
static isImage(mimetype: string, includeGIF = false): boolean {
if (includeGIF && mimetype === 'image/gif') {
return true;
}
return mimetype.startsWith('image/') && mimetype !== 'image/gif';
}
}
================================================
FILE: apps/client-server/src/app/file-converter/converters/file-converter.ts
================================================
import { IFileBuffer } from '@postybirb/types';
export interface IFileConverter {
/**
* Determines if the file can be converted to any of the allowable output mime types.
*
* @param {IFileBuffer} file
* @param {string[]} allowableOutputMimeTypes
* @return {*} {boolean}
*/
canConvert(file: IFileBuffer, allowableOutputMimeTypes: string[]): boolean;
/**
* Converts the file to one of the allowable output mime types.
*
* @param {IFileBuffer} file
* @param {string[]} allowableOutputMimeTypes
* @return {*} {Promise<IFileBuffer>}
*/
convert(
file: IFileBuffer,
allowableOutputMimeTypes: string[],
): Promise<IFileBuffer>;
}
================================================
FILE: apps/client-server/src/app/file-converter/converters/text-file-converter.ts
================================================
import { IFileBuffer } from '@postybirb/types';
import { htmlToText } from 'html-to-text';
import { TurndownService } from 'turndown';
import { IFileConverter } from './file-converter';
const supportedInputMimeTypes = ['text/html', 'text/plain'] as const;
const supportedOutputMimeTypes = [
'text/plain',
'text/html',
'text/markdown',
] as const;
type SupportedInputMimeTypes = (typeof supportedInputMimeTypes)[number];
type SupportedOutputMimeTypes = (typeof supportedOutputMimeTypes)[number];
type ConversionMap = {
[inputMimeType in SupportedInputMimeTypes]: {
[outputMimeType in SupportedOutputMimeTypes]: (
file: IFileBuffer,
) => Promise<IFileBuffer>;
};
};
type ConversionWeights = {
[outputMimeType in SupportedOutputMimeTypes]: number;
};
/**
* A class that converts text files to other text formats.
* Largely for use when converting AltFiles (text/plain or text/html) to other desirable formats.
* @class TextFileConverter
* @implements {IFileConverter}
*/
export class TextFileConverter implements IFileConverter {
private passThrough = async (file: IFileBuffer): Promise<IFileBuffer> => ({
...file,
});
private convertHtmlToPlaintext = async (
file: IFileBuffer,
): Promise<IFileBuffer> => {
const text = htmlToText(file.buffer.toString(), {
wordwrap: 120,
});
return this.toMergedBuffer(file, text, 'text/plain');
};
private convertHtmlToMarkdown = async (
file: IFileBuffer,
): Promise<IFileBuffer> => {
const turndownService = new TurndownService();
const markdown = turndownService.turndown(file.buffer.toString());
return this.toMergedBuffer(file, markdown, 'text/markdown');
};
/**
* Converts plain text to HTML by wrapping lines in <p> tags.
*/
private convertPlaintextToHtml = async (
file: IFileBuffer,
): Promise<IFileBuffer> => {
const lines = file.buffer.toString().split(/\n/);
const html = lines
.map((line) => `<p>${line || '<br>'}</p>`)
.join('\n');
return this.toMergedBuffer(file, html, 'text/html');
};
/**
* Plain text is valid markdown, so this is a passthrough with mime type change.
*/
private convertPlaintextToMarkdown = async (
file: IFileBuffer,
): Promise<IFileBuffer> => this.toMergedBuffer(file, file.buffer.toString(), 'text/markdown');
private readonly supportConversionMappers: ConversionMap = {
'text/html': {
'text/html': this.passThrough,
'text/plain': this.convertHtmlToPlaintext,
'text/markdown': this.convertHtmlToMarkdown,
},
'text/plain': {
'text/plain': this.passThrough,
'text/html': this.convertPlaintextToHtml,
'text/markdown': this.convertPlaintextToMarkdown,
},
};
/**
* Defines the preference of conversion, trying to convert to the most preferred format first.
*/
private readonly conversionWeights: ConversionWeights = {
'text/plain': Number.MAX_SAFE_INTEGER,
'text/html': 1,
'text/markdown': 2,
};
canConvert(file: IFileBuffer, allowableOutputMimeTypes: string[]): boolean {
return (
supportedInputMimeTypes.includes(
file.mimeType as SupportedInputMimeTypes,
) &&
supportedOutputMimeTypes.some((m) => allowableOutputMimeTypes.includes(m))
);
}
async convert(
file: IFileBuffer,
allowableOutputMimeTypes: string[],
): Promise<IFileBuffer> {
const conversionMap =
this.supportConversionMappers[file.mimeType as SupportedInputMimeTypes];
const sortedOutputMimeTypes = allowableOutputMimeTypes
.filter((mimeType) => mimeType in conversionMap)
.sort(
(a, b) =>
this.conversionWeights[a as SupportedOutputMimeTypes] -
this.conversionWeights[b as SupportedOutputMimeTypes],
);
for (const outputMimeType of sortedOutputMimeTypes) {
const conversionFunction =
conversionMap[outputMimeType as SupportedOutputMimeTypes];
if (conversionFunction) {
return conversionFunction(file);
}
}
throw new Error(
`Cannot convert file ${file.fileName} with mime type: ${file.mimeType}`,
);
}
private toMergedBuffer(
fb: IFileBuffer,
str: string,
mimeType: string,
): IFileBuffer {
return {
...fb,
buffer: Buffer.from(str),
mimeType,
};
}
}
================================================
FILE: apps/client-server/src/app/file-converter/file-converter.module.ts
================================================
import { Module } from '@nestjs/common';
import { FileConverterService } from './file-converter.service';
@Module({
providers: [FileConverterService],
exports: [FileConverterService],
})
export class FileConverterModule {}
================================================
FILE: apps/client-server/src/app/file-converter/file-converter.service.spec.ts
================================================
import { Test, TestingModule } from '@nestjs/testing';
import { clearDatabase } from '@postybirb/database';
import { FileConverterService } from './file-converter.service';
describe('FileConverterService', () => {
let service: FileConverterService;
beforeEach(async () => {
clearDatabase();
const module: TestingModule = await Test.createTestingModule({
providers: [FileConverterService],
}).compile();
service = module.get<FileConverterService>(FileConverterService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});
================================================
FILE: apps/client-server/src/app/file-converter/file-converter.service.ts
================================================
import { Injectable } from '@nestjs/common';
import { IFileBuffer } from '@postybirb/types';
import { IFileConverter } from './converters/file-converter';
import { TextFileConverter } from './converters/text-file-converter';
@Injectable()
export class FileConverterService {
private readonly converters: IFileConverter[] = [new TextFileConverter()];
public async convert<T extends IFileBuffer>(
file: T,
allowableOutputMimeTypes: string[],
): Promise<IFileBuffer> {
const converter = this.converters.find((c) =>
c.canConvert(file, allowableOutputMimeTypes),
);
if (!converter) {
throw new Error('No converter found for file');
}
return converter.convert(file, allowableOutputMimeTypes);
}
public async canConvert(
mimeType: string,
allowableOutputMimeTypes: string[],
): Promise<boolean> {
return this.converters.some((c) =>
c.canConvert({ mimeType } as IFileBuffer, allowableOutputMimeTypes),
);
}
}
================================================
FILE: apps/client-server/src/app/form-generator/dtos/form-generation-request.dto.ts
================================================
import { ApiProperty } from '@nestjs/swagger';
import {
AccountId,
IFormGenerationRequestDto,
SubmissionType,
} from '@postybirb/types';
import { IsBoolean, IsEnum, IsOptional, IsString } from 'class-validator';
export class FormGenerationRequestDto implements IFormGenerationRequestDto {
@ApiProperty()
@IsString()
accountId: AccountId;
@ApiProperty({ enum: SubmissionType })
@IsEnum(SubmissionType)
type: SubmissionType;
@ApiProperty()
@IsOptional()
@IsBoolean()
isMultiSubmission?: boolean;
}
================================================
FILE: apps/client-server/src/app/form-generator/form-generator.controller.ts
================================================
import { Body, Controller, Post } from '@nestjs/common';
import { ApiResponse, ApiTags } from '@nestjs/swagger';
import { NULL_ACCOUNT_ID } from '@postybirb/types';
import { FormGenerationRequestDto } from './dtos/form-generation-request.dto';
import { FormGeneratorService } from './form-generator.service';
@ApiTags('form-generator')
@Controller('form-generator')
export class FormGeneratorController {
gitextract_minozs1b/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github/ │ └── workflows/ │ ├── build.yml │ ├── ci.yml │ └── i18n.yml.disabled ├── .gitignore ├── .husky/ │ ├── commit-msg │ └── post-merge ├── .prettierignore ├── .prettierrc ├── .vscode/ │ ├── extensions.json │ ├── launch.json │ └── settings.json ├── .yarn/ │ └── patches/ │ ├── @handlewithcare-prosemirror-inputrules-npm-0.1.3-897e37b56f.patch │ ├── @tiptap-html-npm-3.15.3-a9641901db.patch │ ├── jest-snapshot-npm-29.7.0-15ef0a4ad6.patch │ └── strong-log-transformer-npm-2.1.0-45addd9278.patch ├── .yarnrc.yml ├── Dockerfile ├── LICENSE ├── README.md ├── TRANSLATION.md ├── apps/ │ ├── client-server/ │ │ ├── .eslintrc.json │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── account/ │ │ │ │ │ ├── account.controller.ts │ │ │ │ │ ├── account.events.ts │ │ │ │ │ ├── account.module.ts │ │ │ │ │ ├── account.service.spec.ts │ │ │ │ │ ├── account.service.ts │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── create-account.dto.ts │ │ │ │ │ │ ├── set-website-data-request.dto.ts │ │ │ │ │ │ └── update-account.dto.ts │ │ │ │ │ └── login-state-poller.ts │ │ │ │ ├── app.controller.ts │ │ │ │ ├── app.module.ts │ │ │ │ ├── app.service.ts │ │ │ │ ├── common/ │ │ │ │ │ ├── controller/ │ │ │ │ │ │ └── postybirb-controller.ts │ │ │ │ │ └── service/ │ │ │ │ │ └── postybirb-service.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── custom-shortcuts/ │ │ │ │ │ ├── custom-shortcut.events.ts │ │ │ │ │ ├── custom-shortcuts.controller.ts │ │ │ │ │ ├── custom-shortcuts.module.ts │ │ │ │ │ ├── custom-shortcuts.service.ts │ │ │ │ │ └── dtos/ │ │ │ │ │ ├── create-custom-shortcut.dto.ts │ │ │ │ │ └── update-custom-shortcut.dto.ts │ │ │ │ ├── directory-watchers/ │ │ │ │ │ ├── directory-watcher.events.ts │ │ │ │ │ ├── directory-watchers.controller.ts │ │ │ │ │ ├── directory-watchers.module.ts │ │ │ │ │ ├── directory-watchers.service.spec.ts │ │ │ │ │ ├── directory-watchers.service.ts │ │ │ │ │ └── dtos/ │ │ │ │ │ ├── check-path.dto.ts │ │ │ │ │ ├── create-directory-watcher.dto.ts │ │ │ │ │ └── update-directory-watcher.dto.ts │ │ │ │ ├── drizzle/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── account.entity.ts │ │ │ │ │ │ ├── custom-shortcut.entity.ts │ │ │ │ │ │ ├── database-entity.spec.ts │ │ │ │ │ │ ├── database-entity.ts │ │ │ │ │ │ ├── directory-watcher.entity.ts │ │ │ │ │ │ ├── file-buffer.entity.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── notification.entity.ts │ │ │ │ │ │ ├── post-event.entity.ts │ │ │ │ │ │ ├── post-queue-record.entity.ts │ │ │ │ │ │ ├── post-record.entity.ts │ │ │ │ │ │ ├── settings.entity.ts │ │ │ │ │ │ ├── submission-file.entity.ts │ │ │ │ │ │ ├── submission.entity.ts │ │ │ │ │ │ ├── tag-converter.entity.ts │ │ │ │ │ │ ├── tag-group.entity.ts │ │ │ │ │ │ ├── user-converter.entity.ts │ │ │ │ │ │ ├── user-specified-website-options.entity.ts │ │ │ │ │ │ ├── website-data.entity.ts │ │ │ │ │ │ └── website-options.entity.ts │ │ │ │ │ ├── postybirb-database/ │ │ │ │ │ │ ├── find-options.type.ts │ │ │ │ │ │ ├── postybirb-database.spec.ts │ │ │ │ │ │ ├── postybirb-database.ts │ │ │ │ │ │ ├── postybirb-database.util.ts │ │ │ │ │ │ └── schema-entity-map.ts │ │ │ │ │ └── transaction-context.ts │ │ │ │ ├── file/ │ │ │ │ │ ├── file.controller.ts │ │ │ │ │ ├── file.module.ts │ │ │ │ │ ├── file.service.spec.ts │ │ │ │ │ ├── file.service.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── multer-file-info.ts │ │ │ │ │ │ ├── task-type.enum.ts │ │ │ │ │ │ └── task.ts │ │ │ │ │ ├── services/ │ │ │ │ │ │ ├── create-file.service.ts │ │ │ │ │ │ └── update-file.service.ts │ │ │ │ │ └── utils/ │ │ │ │ │ └── image.util.ts │ │ │ │ ├── file-converter/ │ │ │ │ │ ├── converters/ │ │ │ │ │ │ ├── file-converter.ts │ │ │ │ │ │ └── text-file-converter.ts │ │ │ │ │ ├── file-converter.module.ts │ │ │ │ │ ├── file-converter.service.spec.ts │ │ │ │ │ └── file-converter.service.ts │ │ │ │ ├── form-generator/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ └── form-generation-request.dto.ts │ │ │ │ │ ├── form-generator.controller.ts │ │ │ │ │ ├── form-generator.module.ts │ │ │ │ │ ├── form-generator.service.spec.ts │ │ │ │ │ └── form-generator.service.ts │ │ │ │ ├── image-processing/ │ │ │ │ │ ├── image-processing.module.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ └── sharp-instance-manager.ts │ │ │ │ ├── legacy-database-importer/ │ │ │ │ │ ├── converters/ │ │ │ │ │ │ ├── legacy-converter.ts │ │ │ │ │ │ ├── legacy-custom-shortcut.converter.spec.ts │ │ │ │ │ │ ├── legacy-custom-shortcut.converter.ts │ │ │ │ │ │ ├── legacy-tag-converter.converter.spec.ts │ │ │ │ │ │ ├── legacy-tag-converter.converter.ts │ │ │ │ │ │ ├── legacy-tag-group.converter.spec.ts │ │ │ │ │ │ ├── legacy-tag-group.converter.ts │ │ │ │ │ │ ├── legacy-user-account.converter.spec.ts │ │ │ │ │ │ ├── legacy-user-account.converter.ts │ │ │ │ │ │ ├── legacy-website-data.converter.spec.ts │ │ │ │ │ │ └── legacy-website-data.converter.ts │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ └── legacy-import.dto.ts │ │ │ │ │ ├── legacy-database-importer.controller.ts │ │ │ │ │ ├── legacy-database-importer.module.ts │ │ │ │ │ ├── legacy-database-importer.service.ts │ │ │ │ │ ├── legacy-entities/ │ │ │ │ │ │ ├── legacy-converter-entity.ts │ │ │ │ │ │ ├── legacy-custom-shortcut.ts │ │ │ │ │ │ ├── legacy-tag-converter.ts │ │ │ │ │ │ ├── legacy-tag-group.ts │ │ │ │ │ │ ├── legacy-user-account.ts │ │ │ │ │ │ └── legacy-website-data.ts │ │ │ │ │ ├── transformers/ │ │ │ │ │ │ ├── implementations/ │ │ │ │ │ │ │ ├── bluesky-data-transformer.ts │ │ │ │ │ │ │ ├── custom-data-transformer.ts │ │ │ │ │ │ │ ├── discord-data-transformer.ts │ │ │ │ │ │ │ ├── e621-data-transformer.ts │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── inkbunny-data-transformer.ts │ │ │ │ │ │ │ ├── megalodon-data-transformer.ts │ │ │ │ │ │ │ ├── telegram-data-transformer.ts │ │ │ │ │ │ │ └── twitter-data-transformer.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── legacy-website-data-transformer.ts │ │ │ │ │ │ └── website-data-transformer-registry.ts │ │ │ │ │ └── utils/ │ │ │ │ │ ├── ndjson-parser.ts │ │ │ │ │ └── website-name-mapper.ts │ │ │ │ ├── logs/ │ │ │ │ │ ├── logs.controller.ts │ │ │ │ │ ├── logs.module.ts │ │ │ │ │ └── logs.service.ts │ │ │ │ ├── notifications/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── create-notification.dto.ts │ │ │ │ │ │ └── update-notification.dto.ts │ │ │ │ │ ├── notification.events.ts │ │ │ │ │ ├── notifications.controller.ts │ │ │ │ │ ├── notifications.module.ts │ │ │ │ │ ├── notifications.service.spec.ts │ │ │ │ │ └── notifications.service.ts │ │ │ │ ├── post/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── post-queue-action.dto.ts │ │ │ │ │ │ └── queue-post-record.dto.ts │ │ │ │ │ ├── errors/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── invalid-post-chain.error.ts │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── cancellable-token.ts │ │ │ │ │ │ ├── cancellation-error.ts │ │ │ │ │ │ └── posting-file.ts │ │ │ │ │ ├── post.controller.ts │ │ │ │ │ ├── post.module.ts │ │ │ │ │ ├── post.service.ts │ │ │ │ │ └── services/ │ │ │ │ │ ├── post-file-resizer/ │ │ │ │ │ │ ├── post-file-resizer.service.spec.ts │ │ │ │ │ │ └── post-file-resizer.service.ts │ │ │ │ │ ├── post-manager/ │ │ │ │ │ │ └── post-manager.controller.ts │ │ │ │ │ ├── post-manager-v2/ │ │ │ │ │ │ ├── base-post-manager.service.ts │ │ │ │ │ │ ├── file-submission-post-manager.service.spec.ts │ │ │ │ │ │ ├── file-submission-post-manager.service.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── message-submission-post-manager.service.spec.ts │ │ │ │ │ │ ├── message-submission-post-manager.service.ts │ │ │ │ │ │ └── post-manager-registry.service.ts │ │ │ │ │ ├── post-queue/ │ │ │ │ │ │ ├── post-queue.controller.ts │ │ │ │ │ │ ├── post-queue.service.spec.ts │ │ │ │ │ │ └── post-queue.service.ts │ │ │ │ │ └── post-record-factory/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── post-event.repository.ts │ │ │ │ │ ├── post-record-factory.service.spec.ts │ │ │ │ │ └── post-record-factory.service.ts │ │ │ │ ├── post-parsers/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── description-node/ │ │ │ │ │ │ │ ├── converters/ │ │ │ │ │ │ │ │ ├── base-converter.ts │ │ │ │ │ │ │ │ ├── bbcode-converter.ts │ │ │ │ │ │ │ │ ├── custom-converter.ts │ │ │ │ │ │ │ │ ├── html-converter.ts │ │ │ │ │ │ │ │ ├── npf-converter.spec.ts │ │ │ │ │ │ │ │ ├── npf-converter.ts │ │ │ │ │ │ │ │ └── plaintext-converter.ts │ │ │ │ │ │ │ ├── description-node-tree.ts │ │ │ │ │ │ │ ├── description-node.base.ts │ │ │ │ │ │ │ └── description-node.types.ts │ │ │ │ │ │ └── description-node.spec.ts │ │ │ │ │ ├── parsers/ │ │ │ │ │ │ ├── content-warning-parser.ts │ │ │ │ │ │ ├── description-parser.service.spec.ts │ │ │ │ │ │ ├── description-parser.service.ts │ │ │ │ │ │ ├── rating-parser.spec.ts │ │ │ │ │ │ ├── rating-parser.ts │ │ │ │ │ │ ├── tag-parser.service.spec.ts │ │ │ │ │ │ ├── tag-parser.service.ts │ │ │ │ │ │ ├── title-parser.spec.ts │ │ │ │ │ │ └── title-parser.ts │ │ │ │ │ ├── post-parsers.module.ts │ │ │ │ │ └── post-parsers.service.ts │ │ │ │ ├── remote/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── update-cookies-remote.dto.ts │ │ │ │ │ ├── remote.controller.ts │ │ │ │ │ ├── remote.middleware.ts │ │ │ │ │ ├── remote.module.ts │ │ │ │ │ └── remote.service.ts │ │ │ │ ├── security-and-authentication/ │ │ │ │ │ └── ssl.ts │ │ │ │ ├── settings/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── update-settings.dto.ts │ │ │ │ │ │ └── update-startup-settings.dto.ts │ │ │ │ │ ├── settings.controller.ts │ │ │ │ │ ├── settings.events.ts │ │ │ │ │ ├── settings.module.ts │ │ │ │ │ ├── settings.service.spec.ts │ │ │ │ │ └── settings.service.ts │ │ │ │ ├── submission/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── apply-multi-submission.dto.ts │ │ │ │ │ │ ├── apply-template-options.dto.ts │ │ │ │ │ │ ├── create-submission.dto.ts │ │ │ │ │ │ ├── reorder-submission-files.dto.ts │ │ │ │ │ │ ├── reorder-submission.dto.ts │ │ │ │ │ │ ├── template-option.dto.ts │ │ │ │ │ │ ├── update-alt-file.dto.ts │ │ │ │ │ │ ├── update-submission-template-name.dto.ts │ │ │ │ │ │ └── update-submission.dto.ts │ │ │ │ │ ├── file-submission.controller.ts │ │ │ │ │ ├── services/ │ │ │ │ │ │ ├── file-submission.service.ts │ │ │ │ │ │ ├── message-submission.service.ts │ │ │ │ │ │ ├── submission-service.interface.ts │ │ │ │ │ │ ├── submission.service.spec.ts │ │ │ │ │ │ └── submission.service.ts │ │ │ │ │ ├── submission.controller.ts │ │ │ │ │ ├── submission.events.ts │ │ │ │ │ └── submission.module.ts │ │ │ │ ├── tag-converters/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── create-tag-converter.dto.ts │ │ │ │ │ │ └── update-tag-converter.dto.ts │ │ │ │ │ ├── tag-converter.events.ts │ │ │ │ │ ├── tag-converters.controller.ts │ │ │ │ │ ├── tag-converters.module.ts │ │ │ │ │ ├── tag-converters.service.spec.ts │ │ │ │ │ └── tag-converters.service.ts │ │ │ │ ├── tag-groups/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── create-tag-group.dto.ts │ │ │ │ │ │ └── update-tag-group.dto.ts │ │ │ │ │ ├── tag-group.events.ts │ │ │ │ │ ├── tag-groups.controller.ts │ │ │ │ │ ├── tag-groups.module.ts │ │ │ │ │ ├── tag-groups.service.spec.ts │ │ │ │ │ └── tag-groups.service.ts │ │ │ │ ├── update/ │ │ │ │ │ ├── update.controller.ts │ │ │ │ │ ├── update.events.ts │ │ │ │ │ ├── update.module.ts │ │ │ │ │ └── update.service.ts │ │ │ │ ├── user-converters/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── create-user-converter.dto.ts │ │ │ │ │ │ └── update-user-converter.dto.ts │ │ │ │ │ ├── user-converter.events.ts │ │ │ │ │ ├── user-converters.controller.ts │ │ │ │ │ ├── user-converters.module.ts │ │ │ │ │ ├── user-converters.service.spec.ts │ │ │ │ │ └── user-converters.service.ts │ │ │ │ ├── user-specified-website-options/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── create-user-specified-website-options.dto.ts │ │ │ │ │ │ └── update-user-specified-website-options.dto.ts │ │ │ │ │ ├── user-specified-website-options.controller.ts │ │ │ │ │ ├── user-specified-website-options.module.ts │ │ │ │ │ ├── user-specified-website-options.service.spec.ts │ │ │ │ │ └── user-specified-website-options.service.ts │ │ │ │ ├── utils/ │ │ │ │ │ ├── blocknote-to-tiptap.ts │ │ │ │ │ ├── coerce.util.ts │ │ │ │ │ ├── filesize.util.ts │ │ │ │ │ ├── html-parser.util.ts │ │ │ │ │ ├── select-option.util.ts │ │ │ │ │ └── wait.util.ts │ │ │ │ ├── validation/ │ │ │ │ │ ├── validation.module.ts │ │ │ │ │ ├── validation.service.spec.ts │ │ │ │ │ ├── validation.service.ts │ │ │ │ │ └── validators/ │ │ │ │ │ ├── common-field-validators.ts │ │ │ │ │ ├── datetime-field-validators.ts │ │ │ │ │ ├── description-validators.ts │ │ │ │ │ ├── file-submission-validators.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── select-field-validators.ts │ │ │ │ │ ├── tag-validators.ts │ │ │ │ │ ├── title-validators.ts │ │ │ │ │ └── validator.type.ts │ │ │ │ ├── web-socket/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ └── web-socket-event.ts │ │ │ │ │ ├── web-socket-adapter.ts │ │ │ │ │ ├── web-socket-gateway.ts │ │ │ │ │ ├── web-socket.events.ts │ │ │ │ │ └── web-socket.module.ts │ │ │ │ ├── website-options/ │ │ │ │ │ ├── dtos/ │ │ │ │ │ │ ├── create-website-options.dto.ts │ │ │ │ │ │ ├── preview-description.dto.ts │ │ │ │ │ │ ├── update-submission-website-options.dto.ts │ │ │ │ │ │ ├── update-website-options.dto.ts │ │ │ │ │ │ └── validate-website-options.dto.ts │ │ │ │ │ ├── website-options.controller.ts │ │ │ │ │ ├── website-options.module.ts │ │ │ │ │ ├── website-options.service.spec.ts │ │ │ │ │ └── website-options.service.ts │ │ │ │ └── websites/ │ │ │ │ ├── commons/ │ │ │ │ │ ├── post-builder.spec.ts │ │ │ │ │ ├── post-builder.ts │ │ │ │ │ ├── validator-passthru.ts │ │ │ │ │ └── validator.ts │ │ │ │ ├── decorators/ │ │ │ │ │ ├── disable-ads.decorator.ts │ │ │ │ │ ├── login-flow.decorator.ts │ │ │ │ │ ├── supports-files.decorator.ts │ │ │ │ │ ├── supports-username-shortcut.decorator.ts │ │ │ │ │ ├── website-decorator-props.ts │ │ │ │ │ └── website-metadata.decorator.ts │ │ │ │ ├── dtos/ │ │ │ │ │ └── oauth-website-request.dto.ts │ │ │ │ ├── implementations/ │ │ │ │ │ ├── artconomy/ │ │ │ │ │ │ ├── artconomy.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── artconomy-account-data.ts │ │ │ │ │ │ ├── artconomy-file-submission.ts │ │ │ │ │ │ └── artconomy-message-submission.ts │ │ │ │ │ ├── aryion/ │ │ │ │ │ │ ├── aryion.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── aryion-account-data.ts │ │ │ │ │ │ └── aryion-file-submission.ts │ │ │ │ │ ├── bluesky/ │ │ │ │ │ │ ├── bluesky.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── bluesky-file-submission.ts │ │ │ │ │ │ └── bluesky-message-submission.ts │ │ │ │ │ ├── cara/ │ │ │ │ │ │ ├── cara.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── cara-account-data.ts │ │ │ │ │ │ ├── cara-file-submission.ts │ │ │ │ │ │ └── cara-message-submission.ts │ │ │ │ │ ├── custom/ │ │ │ │ │ │ ├── custom.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── custom-file-submission.ts │ │ │ │ │ │ └── custom-message-submission.ts │ │ │ │ │ ├── default/ │ │ │ │ │ │ └── default.website.ts │ │ │ │ │ ├── derpibooru/ │ │ │ │ │ │ ├── derpibooru.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── derpibooru-file-submission.ts │ │ │ │ │ ├── deviant-art/ │ │ │ │ │ │ ├── deviant-art-description-converter.ts │ │ │ │ │ │ ├── deviant-art.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── deviant-art-account-data.ts │ │ │ │ │ │ ├── deviant-art-file-submission.ts │ │ │ │ │ │ └── deviant-art-message-submission.ts │ │ │ │ │ ├── discord/ │ │ │ │ │ │ ├── discord.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── discord-file-submission.ts │ │ │ │ │ │ └── discord-message-submission.ts │ │ │ │ │ ├── e621/ │ │ │ │ │ │ ├── e621.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── e621-file-submission.ts │ │ │ │ │ ├── firefish/ │ │ │ │ │ │ └── firefish.website.ts │ │ │ │ │ ├── friendica/ │ │ │ │ │ │ └── friendica.website.ts │ │ │ │ │ ├── fur-affinity/ │ │ │ │ │ │ ├── fur-affinity.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── fur-affinity-account-data.ts │ │ │ │ │ │ ├── fur-affinity-categories.ts │ │ │ │ │ │ ├── fur-affinity-file-submission.ts │ │ │ │ │ │ ├── fur-affinity-message-submission.ts │ │ │ │ │ │ ├── fur-affinity-species-options.ts │ │ │ │ │ │ └── fur-affinity-themes.ts │ │ │ │ │ ├── furbooru/ │ │ │ │ │ │ ├── furbooru.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── furbooru-file-submission.ts │ │ │ │ │ ├── gotosocial/ │ │ │ │ │ │ └── gotosocial.website.ts │ │ │ │ │ ├── hentai-foundry/ │ │ │ │ │ │ ├── hentai-foundry.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── hentai-foundry-account-data.ts │ │ │ │ │ │ ├── hentai-foundry-categories.ts │ │ │ │ │ │ ├── hentai-foundry-file-submission.ts │ │ │ │ │ │ └── hentai-foundry-message-submission.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── inkbunny/ │ │ │ │ │ │ ├── inkbunny.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── inkbunny-file-submission.ts │ │ │ │ │ ├── instagram/ │ │ │ │ │ │ ├── instagram-api-service/ │ │ │ │ │ │ │ └── instagram-api-service.ts │ │ │ │ │ │ ├── instagram-blob-service/ │ │ │ │ │ │ │ └── instagram-blob-service.ts │ │ │ │ │ │ ├── instagram.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── instagram-file-submission.ts │ │ │ │ │ ├── itaku/ │ │ │ │ │ │ ├── itaku.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── itaku-account-data.ts │ │ │ │ │ │ ├── itaku-file-submission.ts │ │ │ │ │ │ ├── itaku-message-submission.ts │ │ │ │ │ │ └── itaku-user-info.ts │ │ │ │ │ ├── ko-fi/ │ │ │ │ │ │ ├── ko-fi.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── ko-fi-account-data.ts │ │ │ │ │ │ ├── ko-fi-file-submission.ts │ │ │ │ │ │ └── ko-fi-message-submission.ts │ │ │ │ │ ├── manebooru/ │ │ │ │ │ │ ├── manebooru.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ └── manebooru-file-submission.ts │ │ │ │ │ ├── mastodon/ │ │ │ │ │ │ └── mastodon.website.ts │ │ │ │ │ ├── megalodon/ │ │ │ │ │ │ ├── megalodon-api-service.ts │ │ │ │ │ │ ├── megalodon.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── megalodon-file-submission.ts │ │ │ │ │ │ └── megalodon-message-submission.ts │ │ │ │ │ ├── misskey/ │ │ │ │ │ │ ├── misskey-api-service.ts │ │ │ │ │ │ ├── misskey.website.ts │ │ │ │ │ │ └── models/ │ │ │ │ │ │ ├── misskey-file-submission.ts │ │ │ │ │ │ └── misskey-message-submission.ts │ │ │ │ │ ├── newgrounds/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── newgrounds-account-data.ts │ │ │ │ │ │ │ ├── newgrounds-base-submission.ts │ │ │ │ │ │ │ ├── newgrounds-file-submission.ts │ │ │ │ │ │ │ └── newgrounds-message-submission.ts │ │ │ │ │ │ └── newgrounds.website.ts │ │ │ │ │ ├── patreon/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── patreon-account-data.ts │ │ │ │ │ │ │ ├── patreon-campaign-types.ts │ │ │ │ │ │ │ ├── patreon-collection-types.ts │ │ │ │ │ │ │ ├── patreon-file-submission.ts │ │ │ │ │ │ │ ├── patreon-media-upload-types.ts │ │ │ │ │ │ │ ├── patreon-message-submission.ts │ │ │ │ │ │ │ └── patreon-post-types.ts │ │ │ │ │ │ ├── patreon-description-converter.ts │ │ │ │ │ │ └── patreon.website.ts │ │ │ │ │ ├── philomena/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── philomena-account-data.ts │ │ │ │ │ │ │ └── philomena-file-submission.ts │ │ │ │ │ │ └── philomena.website.ts │ │ │ │ │ ├── picarto/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── picarto-account-data.ts │ │ │ │ │ │ │ ├── picarto-categories.ts │ │ │ │ │ │ │ ├── picarto-file-submission.ts │ │ │ │ │ │ │ └── picarto-software.ts │ │ │ │ │ │ └── picarto.website.ts │ │ │ │ │ ├── piczel/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── piczel-account-data.ts │ │ │ │ │ │ │ └── piczel-file-submission.ts │ │ │ │ │ │ └── piczel.website.ts │ │ │ │ │ ├── pillowfort/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── pillowfort-account-data.ts │ │ │ │ │ │ │ ├── pillowfort-file-submission.ts │ │ │ │ │ │ │ └── pillowfort-message-submission.ts │ │ │ │ │ │ └── pillowfort.website.ts │ │ │ │ │ ├── pixelfed/ │ │ │ │ │ │ └── pixelfed.website.ts │ │ │ │ │ ├── pixiv/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── pixiv-account-data.ts │ │ │ │ │ │ │ └── pixiv-file-submission.ts │ │ │ │ │ │ └── pixiv.website.ts │ │ │ │ │ ├── pleroma/ │ │ │ │ │ │ └── pleroma.website.ts │ │ │ │ │ ├── provider.ts │ │ │ │ │ ├── sofurry/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── sofurry-account-data.ts │ │ │ │ │ │ │ ├── sofurry-categories.ts │ │ │ │ │ │ │ └── sofurry-file-submission.ts │ │ │ │ │ │ └── sofurry.website.ts │ │ │ │ │ ├── subscribe-star/ │ │ │ │ │ │ ├── base-subscribe-star.website.ts │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── subscribe-star-account-data.ts │ │ │ │ │ │ │ ├── subscribe-star-file-submission.ts │ │ │ │ │ │ │ └── subscribe-star-message-submission.ts │ │ │ │ │ │ ├── subscribe-star-adult.website.ts │ │ │ │ │ │ └── subscribe-star.website.ts │ │ │ │ │ ├── telegram/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── telegram-file-submission.ts │ │ │ │ │ │ │ └── telegram-message-submission.ts │ │ │ │ │ │ └── telegram.website.ts │ │ │ │ │ ├── test/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── test-file-submission.ts │ │ │ │ │ │ │ └── test-message-submission.ts │ │ │ │ │ │ └── test.website.ts │ │ │ │ │ ├── toyhouse/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── toyhouse-account-data.ts │ │ │ │ │ │ │ └── toyhouse-file-submission.ts │ │ │ │ │ │ └── toyhouse.website.ts │ │ │ │ │ ├── tumblr/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── tumblr-account-data.ts │ │ │ │ │ │ │ ├── tumblr-file-submission.ts │ │ │ │ │ │ │ └── tumblr-message-submission.ts │ │ │ │ │ │ └── tumblr.website.ts │ │ │ │ │ ├── twitter/ │ │ │ │ │ │ ├── models/ │ │ │ │ │ │ │ ├── twitter-file-submission.ts │ │ │ │ │ │ │ └── twitter-message-submission.ts │ │ │ │ │ │ ├── twitter-api-service/ │ │ │ │ │ │ │ └── twitter-api-service.ts │ │ │ │ │ │ └── twitter.website.ts │ │ │ │ │ └── weasyl/ │ │ │ │ │ ├── models/ │ │ │ │ │ │ ├── weasyl-account-data.ts │ │ │ │ │ │ ├── weasyl-categories.ts │ │ │ │ │ │ ├── weasyl-file-submission.ts │ │ │ │ │ │ └── weasyl-message-submission.ts │ │ │ │ │ └── weasyl.website.ts │ │ │ │ ├── models/ │ │ │ │ │ ├── base-website-options.spec.ts │ │ │ │ │ ├── base-website-options.ts │ │ │ │ │ ├── data-property-accessibility.ts │ │ │ │ │ ├── default-website-options.ts │ │ │ │ │ └── website-modifiers/ │ │ │ │ │ ├── file-website.ts │ │ │ │ │ ├── message-website.ts │ │ │ │ │ ├── oauth-website.ts │ │ │ │ │ ├── with-custom-description-parser.ts │ │ │ │ │ ├── with-dynamic-file-size-limits.ts │ │ │ │ │ └── with-runtime-description-parser.ts │ │ │ │ ├── website-data-manager.spec.ts │ │ │ │ ├── website-data-manager.ts │ │ │ │ ├── website-registry.service.spec.ts │ │ │ │ ├── website-registry.service.ts │ │ │ │ ├── website.events.ts │ │ │ │ ├── website.spec.ts │ │ │ │ ├── website.ts │ │ │ │ ├── websites.controller.ts │ │ │ │ └── websites.module.ts │ │ │ ├── assets/ │ │ │ │ ├── .gitkeep │ │ │ │ └── sharp-worker.js │ │ │ ├── environments/ │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── main.ts │ │ │ └── test-files/ │ │ │ └── README.md │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ ├── postybirb/ │ │ ├── .eslintrc.json │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── app/ │ │ │ │ ├── api/ │ │ │ │ │ └── preload.ts │ │ │ │ ├── app.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── events/ │ │ │ │ │ └── electron.events.ts │ │ │ │ └── loader/ │ │ │ │ ├── css/ │ │ │ │ │ └── style.css │ │ │ │ ├── fonts/ │ │ │ │ │ └── Mylodon-Light.otf │ │ │ │ ├── loader.html │ │ │ │ └── loader.js │ │ │ ├── environments/ │ │ │ │ ├── environment.base.ts │ │ │ │ ├── environment.prod.ts │ │ │ │ └── environment.ts │ │ │ ├── main.ts │ │ │ └── migrations/ │ │ │ ├── 0000_tough_ken_ellis.sql │ │ │ ├── 0001_noisy_kate_bishop.sql │ │ │ ├── 0002_pretty_sunfire.sql │ │ │ ├── 0003_glamorous_power_pack.sql │ │ │ ├── 0004_fuzzy_rafael_vega.sql │ │ │ ├── 0005_exotic_nebula.sql │ │ │ ├── 0006_cooing_songbird.sql │ │ │ └── meta/ │ │ │ ├── 0000_snapshot.json │ │ │ ├── 0001_snapshot.json │ │ │ ├── 0002_snapshot.json │ │ │ ├── 0003_snapshot.json │ │ │ ├── 0004_snapshot.json │ │ │ ├── 0005_snapshot.json │ │ │ ├── 0006_snapshot.json │ │ │ └── _journal.json │ │ ├── tsconfig.app.json │ │ ├── tsconfig.json │ │ └── tsconfig.spec.json │ ├── postybirb-cloud-server/ │ │ ├── .gitignore │ │ ├── host.json │ │ ├── local.settings.json │ │ ├── package.json │ │ ├── src/ │ │ │ └── functions/ │ │ │ └── upload.ts │ │ └── tsconfig.json │ └── postybirb-ui/ │ ├── .eslintrc.json │ ├── index.html │ ├── jest.config.ts │ ├── postcss.config.js │ ├── project.json │ ├── src/ │ │ ├── README.md │ │ ├── api/ │ │ │ ├── account.api.ts │ │ │ ├── base.api.ts │ │ │ ├── custom-shortcut.api.ts │ │ │ ├── directory-watchers.api.ts │ │ │ ├── file-submission.api.ts │ │ │ ├── form-generator.api.ts │ │ │ ├── legacy-database-importer.api.ts │ │ │ ├── notification.api.ts │ │ │ ├── post-manager.api.ts │ │ │ ├── post-queue.api.ts │ │ │ ├── post.api.ts │ │ │ ├── remote.api.ts │ │ │ ├── settings.api.ts │ │ │ ├── submission.api.ts │ │ │ ├── tag-converters.api.ts │ │ │ ├── tag-groups.api.ts │ │ │ ├── update.api.ts │ │ │ ├── user-converters.api.ts │ │ │ ├── user-specified-website-options.api.ts │ │ │ ├── website-options.api.ts │ │ │ └── websites.api.ts │ │ ├── app-insights-ui.ts │ │ ├── blocknote-locales.d.ts │ │ ├── components/ │ │ │ ├── confirm-action-modal/ │ │ │ │ ├── confirm-action-modal.tsx │ │ │ │ └── index.ts │ │ │ ├── dialogs/ │ │ │ │ └── settings-dialog/ │ │ │ │ ├── sections/ │ │ │ │ │ ├── app-settings-section.tsx │ │ │ │ │ ├── appearance-settings-section.tsx │ │ │ │ │ ├── data-settings-section.tsx │ │ │ │ │ ├── description-settings-section.tsx │ │ │ │ │ ├── import-settings-section.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── notifications-settings-section.tsx │ │ │ │ │ ├── remote-settings-section.tsx │ │ │ │ │ ├── spellchecker-settings-section.tsx │ │ │ │ │ └── tags-settings-section.tsx │ │ │ │ ├── settings-dialog.module.css │ │ │ │ └── settings-dialog.tsx │ │ │ ├── disclaimer/ │ │ │ │ └── disclaimer.tsx │ │ │ ├── drawers/ │ │ │ │ ├── converter-drawer/ │ │ │ │ │ ├── converter-drawer.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── custom-shortcuts-drawer/ │ │ │ │ │ ├── custom-shortcuts-drawer.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── drawers.tsx │ │ │ │ ├── file-watcher-drawer/ │ │ │ │ │ ├── file-watcher-drawer.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── notifications-drawer/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── notifications-drawer.tsx │ │ │ │ ├── schedule-drawer/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── schedule-calendar.tsx │ │ │ │ │ ├── schedule-drawer.css │ │ │ │ │ ├── schedule-drawer.tsx │ │ │ │ │ └── submission-list.tsx │ │ │ │ ├── section-drawer.tsx │ │ │ │ ├── tag-converter-drawer/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── tag-converter-drawer.tsx │ │ │ │ ├── tag-group-drawer/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── tag-group-drawer.tsx │ │ │ │ └── user-converter-drawer/ │ │ │ │ ├── index.ts │ │ │ │ └── user-converter-drawer.tsx │ │ │ ├── empty-state/ │ │ │ │ ├── empty-state.tsx │ │ │ │ └── index.ts │ │ │ ├── error-boundary/ │ │ │ │ ├── error-boundary.tsx │ │ │ │ ├── index.ts │ │ │ │ └── specialized-error-boundaries.tsx │ │ │ ├── hold-to-confirm/ │ │ │ │ ├── hold-to-confirm.tsx │ │ │ │ └── index.ts │ │ │ ├── language-picker/ │ │ │ │ ├── index.ts │ │ │ │ ├── language-picker.css │ │ │ │ └── language-picker.tsx │ │ │ ├── layout/ │ │ │ │ ├── content-area.tsx │ │ │ │ ├── content-navbar.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── primary-content.tsx │ │ │ │ ├── section-panel.tsx │ │ │ │ └── side-nav.tsx │ │ │ ├── onboarding-tour/ │ │ │ │ ├── index.ts │ │ │ │ ├── mantine-tooltip.tsx │ │ │ │ ├── tour-provider.tsx │ │ │ │ └── tours/ │ │ │ │ ├── accounts-tour.tsx │ │ │ │ ├── custom-shortcuts-tour.tsx │ │ │ │ ├── file-watchers-tour.tsx │ │ │ │ ├── home-tour.tsx │ │ │ │ ├── layout-tour.tsx │ │ │ │ ├── notifications-tour.tsx │ │ │ │ ├── schedule-tour.tsx │ │ │ │ ├── submission-edit-tour.tsx │ │ │ │ ├── submissions-tour.tsx │ │ │ │ ├── tag-converters-tour.tsx │ │ │ │ ├── tag-groups-tour.tsx │ │ │ │ ├── templates-tour.tsx │ │ │ │ └── user-converters-tour.tsx │ │ │ ├── sections/ │ │ │ │ ├── accounts-section/ │ │ │ │ │ ├── account-section-header.tsx │ │ │ │ │ ├── accounts-content.tsx │ │ │ │ │ ├── accounts-section.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ ├── accounts-context.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── custom-login-placeholder.tsx │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── use-account-actions.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── login-webview.tsx │ │ │ │ │ ├── website-account-card.tsx │ │ │ │ │ ├── website-visibility-picker.tsx │ │ │ │ │ └── webview-tag.ts │ │ │ │ ├── file-submissions-section/ │ │ │ │ │ └── hooks/ │ │ │ │ │ └── use-file-submissions.ts │ │ │ │ ├── home-section/ │ │ │ │ │ ├── account-health-panel.tsx │ │ │ │ │ ├── home-content.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── queue-control-card.tsx │ │ │ │ │ ├── recent-activity-panel.tsx │ │ │ │ │ ├── schedule-calendar-panel.tsx │ │ │ │ │ ├── stat-card.tsx │ │ │ │ │ ├── upcoming-posts-panel.tsx │ │ │ │ │ └── validation-issues-panel.tsx │ │ │ │ ├── index.ts │ │ │ │ ├── submissions-section/ │ │ │ │ │ ├── archived-submission-list.tsx │ │ │ │ │ ├── context/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── submissions-context.tsx │ │ │ │ │ ├── file-submission-modal/ │ │ │ │ │ │ ├── file-dropzone.tsx │ │ │ │ │ │ ├── file-list.tsx │ │ │ │ │ │ ├── file-preview.tsx │ │ │ │ │ │ ├── file-submission-modal.css │ │ │ │ │ │ ├── file-submission-modal.tsx │ │ │ │ │ │ ├── file-submission-modal.utils.ts │ │ │ │ │ │ ├── image-editor.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── submission-options.tsx │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── use-global-dropzone.ts │ │ │ │ │ │ ├── use-submission-actions.ts │ │ │ │ │ │ ├── use-submission-create.ts │ │ │ │ │ │ ├── use-submission-delete.ts │ │ │ │ │ │ ├── use-submission-handlers.ts │ │ │ │ │ │ ├── use-submission-post.ts │ │ │ │ │ │ ├── use-submission-selection.ts │ │ │ │ │ │ ├── use-submission-update.ts │ │ │ │ │ │ └── use-submissions.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── post-confirm-modal/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── post-confirm-modal.tsx │ │ │ │ │ ├── resume-mode-modal/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ └── resume-mode-modal.tsx │ │ │ │ │ ├── submission-card/ │ │ │ │ │ │ ├── archived-submission-card.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── sortable-submission-card.tsx │ │ │ │ │ │ ├── submission-actions.tsx │ │ │ │ │ │ ├── submission-badges.tsx │ │ │ │ │ │ ├── submission-card.tsx │ │ │ │ │ │ ├── submission-quick-edit-actions.tsx │ │ │ │ │ │ ├── submission-thumbnail.tsx │ │ │ │ │ │ ├── submission-title.tsx │ │ │ │ │ │ ├── types.ts │ │ │ │ │ │ └── utils.ts │ │ │ │ │ ├── submission-edit-card/ │ │ │ │ │ │ ├── account-selection/ │ │ │ │ │ │ │ ├── account-option-row.tsx │ │ │ │ │ │ │ ├── account-select.tsx │ │ │ │ │ │ │ ├── account-selection-form.tsx │ │ │ │ │ │ │ ├── account-selection.css │ │ │ │ │ │ │ ├── form/ │ │ │ │ │ │ │ │ ├── fields/ │ │ │ │ │ │ │ │ │ ├── boolean-field.tsx │ │ │ │ │ │ │ │ │ ├── datetime-field.tsx │ │ │ │ │ │ │ │ │ ├── description-field.tsx │ │ │ │ │ │ │ │ │ ├── description-preview-panel.tsx │ │ │ │ │ │ │ │ │ ├── field-copy-button.tsx │ │ │ │ │ │ │ │ │ ├── field-label.tsx │ │ │ │ │ │ │ │ │ ├── field.css │ │ │ │ │ │ │ │ │ ├── form-field.type.ts │ │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ │ ├── input-field.tsx │ │ │ │ │ │ │ │ │ ├── radio-field.tsx │ │ │ │ │ │ │ │ │ ├── select-field.tsx │ │ │ │ │ │ │ │ │ ├── select-utils.ts │ │ │ │ │ │ │ │ │ ├── tag-field.tsx │ │ │ │ │ │ │ │ │ └── tree-select.tsx │ │ │ │ │ │ │ │ ├── form-field.tsx │ │ │ │ │ │ │ │ ├── form-fields-context.tsx │ │ │ │ │ │ │ │ ├── hooks/ │ │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ │ ├── use-default-option.tsx │ │ │ │ │ │ │ │ │ └── use-validations.tsx │ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ │ ├── save-defaults-popover.tsx │ │ │ │ │ │ │ │ ├── section-layout.css │ │ │ │ │ │ │ │ ├── section-layout.tsx │ │ │ │ │ │ │ │ └── validation-alerts.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── selected-accounts-forms.tsx │ │ │ │ │ │ ├── actions/ │ │ │ │ │ │ │ ├── apply-template-action.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── save-to-many-action.tsx │ │ │ │ │ │ │ └── submission-edit-card-actions.tsx │ │ │ │ │ │ ├── body/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── submission-edit-card-body.tsx │ │ │ │ │ │ ├── context/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── submission-edit-card-context.tsx │ │ │ │ │ │ ├── defaults-form/ │ │ │ │ │ │ │ ├── defaults-form.css │ │ │ │ │ │ │ ├── defaults-form.tsx │ │ │ │ │ │ │ └── index.ts │ │ │ │ │ │ ├── file-management/ │ │ │ │ │ │ │ ├── bulk-file-editor.tsx │ │ │ │ │ │ │ ├── file-actions.tsx │ │ │ │ │ │ │ ├── file-alt-text-editor.tsx │ │ │ │ │ │ │ ├── file-management.css │ │ │ │ │ │ │ ├── file-metadata.tsx │ │ │ │ │ │ │ ├── file-preview.tsx │ │ │ │ │ │ │ ├── file-uploader.tsx │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ ├── submission-file-card.tsx │ │ │ │ │ │ │ ├── submission-file-manager.tsx │ │ │ │ │ │ │ └── use-submission-accounts.ts │ │ │ │ │ │ ├── header/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── submission-edit-card-header.tsx │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── schedule-form/ │ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ │ └── schedule-form.tsx │ │ │ │ │ │ ├── submission-edit-card.css │ │ │ │ │ │ └── submission-edit-card.tsx │ │ │ │ │ ├── submission-history/ │ │ │ │ │ │ ├── history-utils.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── post-history-content.tsx │ │ │ │ │ │ └── post-record-card.tsx │ │ │ │ │ ├── submission-history-drawer.tsx │ │ │ │ │ ├── submission-list.tsx │ │ │ │ │ ├── submission-section-header.tsx │ │ │ │ │ ├── submissions-content.tsx │ │ │ │ │ ├── submissions-section.css │ │ │ │ │ ├── submissions-section.tsx │ │ │ │ │ └── types.ts │ │ │ │ └── templates-section/ │ │ │ │ ├── index.ts │ │ │ │ ├── template-card.tsx │ │ │ │ ├── templates-content.tsx │ │ │ │ ├── templates-section.css │ │ │ │ └── templates-section.tsx │ │ │ ├── shared/ │ │ │ │ ├── account-picker/ │ │ │ │ │ ├── account-picker.css │ │ │ │ │ ├── account-picker.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── basic-website-select/ │ │ │ │ │ └── basic-website-select.tsx │ │ │ │ ├── copy-to-clipboard/ │ │ │ │ │ ├── copy-to-clipboard.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── description-editor/ │ │ │ │ │ ├── components/ │ │ │ │ │ │ ├── bubble-toolbar.tsx │ │ │ │ │ │ ├── description-toolbar.tsx │ │ │ │ │ │ ├── html-edit-modal.tsx │ │ │ │ │ │ ├── insert-media-modal.tsx │ │ │ │ │ │ └── suggestion-menu.tsx │ │ │ │ │ ├── custom-blocks/ │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── shortcut.css │ │ │ │ │ │ └── website-only-selector.tsx │ │ │ │ │ ├── description-editor.css │ │ │ │ │ ├── description-editor.tsx │ │ │ │ │ ├── extensions/ │ │ │ │ │ │ ├── content-warning-shortcut.tsx │ │ │ │ │ │ ├── custom-shortcut.tsx │ │ │ │ │ │ ├── default-shortcut.tsx │ │ │ │ │ │ ├── indent.ts │ │ │ │ │ │ ├── index.ts │ │ │ │ │ │ ├── resizable-image.tsx │ │ │ │ │ │ ├── tags-shortcut.tsx │ │ │ │ │ │ ├── title-shortcut.tsx │ │ │ │ │ │ └── username-shortcut.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── types.ts │ │ │ │ ├── external-link/ │ │ │ │ │ ├── external-link.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── index.ts │ │ │ │ ├── multi-scheduler-modal/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── multi-scheduler-modal.css │ │ │ │ │ └── multi-scheduler-modal.tsx │ │ │ │ ├── rating-input/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── rating-input.tsx │ │ │ │ ├── reorderable-submission-list/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── reorderable-submission-list.css │ │ │ │ │ └── reorderable-submission-list.tsx │ │ │ │ ├── schedule-popover/ │ │ │ │ │ ├── cron-picker.tsx │ │ │ │ │ ├── index.ts │ │ │ │ │ └── schedule-popover.tsx │ │ │ │ ├── search-input.tsx │ │ │ │ ├── simple-tag-input/ │ │ │ │ │ ├── index.ts │ │ │ │ │ └── simple-tag-input.tsx │ │ │ │ ├── submission-picker/ │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── submission-picker-modal.tsx │ │ │ │ │ └── submission-picker.tsx │ │ │ │ └── template-picker/ │ │ │ │ ├── index.ts │ │ │ │ ├── template-picker-modal.tsx │ │ │ │ └── template-picker.tsx │ │ │ ├── theme-picker/ │ │ │ │ ├── index.ts │ │ │ │ └── theme-picker.tsx │ │ │ ├── update-button/ │ │ │ │ ├── index.ts │ │ │ │ ├── update-button.tsx │ │ │ │ └── update-modal.tsx │ │ │ ├── website-components/ │ │ │ │ ├── bluesky/ │ │ │ │ │ └── description-preview.tsx │ │ │ │ ├── e621/ │ │ │ │ │ ├── e621-dtext-renderer.tsx │ │ │ │ │ └── e621-tag-search-provider.tsx │ │ │ │ ├── furaffinity/ │ │ │ │ │ ├── furaffinity-bbcode-renderer.tsx │ │ │ │ │ └── furaffinity-bbcode.ts │ │ │ │ ├── index.ts │ │ │ │ ├── telegram/ │ │ │ │ │ └── telegram-format-renderer.tsx │ │ │ │ └── tumblr/ │ │ │ │ └── tumblr-npf-renderer.tsx │ │ │ └── website-login-views/ │ │ │ ├── bluesky/ │ │ │ │ ├── bluesky-login-view.tsx │ │ │ │ └── index.ts │ │ │ ├── custom/ │ │ │ │ ├── custom-login-view.tsx │ │ │ │ └── index.ts │ │ │ ├── discord/ │ │ │ │ ├── discord-login-view.tsx │ │ │ │ └── index.ts │ │ │ ├── e621/ │ │ │ │ ├── e621-login-view.tsx │ │ │ │ └── index.ts │ │ │ ├── helpers.tsx │ │ │ ├── index.ts │ │ │ ├── inkbunny/ │ │ │ │ ├── index.ts │ │ │ │ └── inkbunny-login-view.tsx │ │ │ ├── instagram/ │ │ │ │ ├── index.ts │ │ │ │ ├── instagram-login-view.tsx │ │ │ │ └── instagram-setup-guide.tsx │ │ │ ├── login-view-container.tsx │ │ │ ├── megalodon/ │ │ │ │ ├── index.ts │ │ │ │ └── megalodon-login-view.tsx │ │ │ ├── misskey/ │ │ │ │ ├── index.ts │ │ │ │ └── misskey-login-view.tsx │ │ │ ├── telegram/ │ │ │ │ ├── index.ts │ │ │ │ └── telegram-login-view.tsx │ │ │ ├── twitter/ │ │ │ │ ├── index.ts │ │ │ │ └── twitter-login-view.tsx │ │ │ └── types.ts │ │ ├── config/ │ │ │ ├── keybindings.ts │ │ │ └── nav-items.tsx │ │ ├── environments/ │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── hooks/ │ │ │ ├── index.ts │ │ │ ├── tag-search/ │ │ │ │ ├── index.ts │ │ │ │ ├── tag-search-provider.ts │ │ │ │ ├── tag-search-providers.ts │ │ │ │ └── use-tag-search.ts │ │ │ ├── use-keybindings.ts │ │ │ └── use-locale.ts │ │ ├── i18n/ │ │ │ ├── languages.tsx │ │ │ └── validation-translation.tsx │ │ ├── index.tsx │ │ ├── main.tsx │ │ ├── models/ │ │ │ └── http-error-response.ts │ │ ├── providers/ │ │ │ └── i18n-provider.tsx │ │ ├── shared/ │ │ │ └── platform-utils.ts │ │ ├── stores/ │ │ │ ├── create-entity-store.ts │ │ │ ├── create-typed-store.ts │ │ │ ├── entity/ │ │ │ │ ├── account-store.ts │ │ │ │ ├── custom-shortcut-store.ts │ │ │ │ ├── directory-watcher-store.ts │ │ │ │ ├── notification-store.ts │ │ │ │ ├── settings-store.ts │ │ │ │ ├── submission-store.ts │ │ │ │ ├── tag-converter-store.ts │ │ │ │ ├── tag-group-store.ts │ │ │ │ ├── user-converter-store.ts │ │ │ │ └── website-store.ts │ │ │ ├── index.ts │ │ │ ├── records/ │ │ │ │ ├── account-record.ts │ │ │ │ ├── base-record.ts │ │ │ │ ├── custom-shortcut-record.ts │ │ │ │ ├── directory-watcher-record.ts │ │ │ │ ├── index.ts │ │ │ │ ├── notification-record.ts │ │ │ │ ├── settings-record.ts │ │ │ │ ├── submission-record.ts │ │ │ │ ├── tag-converter-record.ts │ │ │ │ ├── tag-group-record.ts │ │ │ │ ├── user-converter-record.ts │ │ │ │ └── website-record.ts │ │ │ ├── store-init.ts │ │ │ └── ui/ │ │ │ ├── accounts-ui-store.ts │ │ │ ├── appearance-store.ts │ │ │ ├── drawer-store.ts │ │ │ ├── locale-store.ts │ │ │ ├── navigation-store.ts │ │ │ ├── submissions-ui-store.ts │ │ │ ├── templates-ui-store.ts │ │ │ └── tour-store.ts │ │ ├── styles/ │ │ │ └── layout.css │ │ ├── styles.css │ │ ├── theme/ │ │ │ ├── css-variable-resolver.ts │ │ │ ├── theme-styles.css │ │ │ └── theme.ts │ │ ├── transports/ │ │ │ ├── http-client.ts │ │ │ └── websocket.ts │ │ ├── types/ │ │ │ ├── account-filters.ts │ │ │ ├── navigation.ts │ │ │ └── view-state.ts │ │ └── utils/ │ │ ├── class-names.ts │ │ ├── environment.ts │ │ ├── index.ts │ │ ├── notifications.tsx │ │ └── open-url.ts │ ├── tailwind.config.js │ ├── tsconfig.app.json │ ├── tsconfig.json │ ├── tsconfig.spec.json │ └── vite.config.ts ├── babel.config.js ├── commitlint.config.js ├── compose.yml ├── docs/ │ ├── DOCKER.md │ ├── POST_QUEUE_FLOWS.md │ ├── app-insights/ │ │ ├── APP_INSIGHTS_QUERIES.md │ │ └── APP_INSIGHTS_SETUP.md │ └── contributing/ │ └── add-a-website/ │ ├── README.md │ └── sections/ │ ├── authenticate-a-user.md │ ├── defining-submission-data.md │ ├── description-parsing.md │ ├── file-website.md │ ├── message-website.md │ └── validation.md ├── drizzle.config.ts ├── electron-builder.yml ├── entrypoint.sh ├── jest.config.ts ├── jest.preset.js ├── jest.reporter.js ├── jest.setup.ts ├── lang/ │ ├── de.po │ ├── en.po │ ├── es.po │ ├── lt.po │ ├── nl.po │ ├── pt_BR.po │ ├── ru.po │ └── ta.po ├── libs/ │ ├── database/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── database.ts │ │ │ ├── helper-types.ts │ │ │ ├── relations/ │ │ │ │ ├── index.ts │ │ │ │ └── relations.ts │ │ │ └── schemas/ │ │ │ ├── account.schema.ts │ │ │ ├── common.schema.ts │ │ │ ├── custom-shortcut.schema.ts │ │ │ ├── directory-watcher.schema.ts │ │ │ ├── file-buffer.schema.ts │ │ │ ├── index.ts │ │ │ ├── notification.schema.ts │ │ │ ├── post-event.schema.ts │ │ │ ├── post-queue-record.schema.ts │ │ │ ├── post-record.schema.ts │ │ │ ├── settings.schema.ts │ │ │ ├── submission-file.schema.ts │ │ │ ├── submission.schema.ts │ │ │ ├── tag-converter.schema.ts │ │ │ ├── tag-group.schema.ts │ │ │ ├── user-converter.schema.ts │ │ │ ├── user-specified-website-options.schema.ts │ │ │ ├── website-data.schema.ts │ │ │ └── website-options.schema.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ └── tsconfig.spec.json │ ├── form-builder/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── constants.ts │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── decorators/ │ │ │ │ ├── boolean-field.decorator.ts │ │ │ │ ├── date-time-field.decorator.ts │ │ │ │ ├── description-field.decorator.ts │ │ │ │ ├── index.ts │ │ │ │ ├── radio-field.decorator.ts │ │ │ │ ├── rating.decorator.ts │ │ │ │ ├── select-field.decorator.ts │ │ │ │ ├── tag-field.decorator.ts │ │ │ │ ├── text-field.decorator.ts │ │ │ │ └── title-field.decorator.ts │ │ │ ├── form-builder.spec.ts │ │ │ ├── form-builder.ts │ │ │ ├── types/ │ │ │ │ ├── field-aggregate.ts │ │ │ │ ├── field.ts │ │ │ │ ├── form-builder-metadata.ts │ │ │ │ ├── index.ts │ │ │ │ └── primitive-record.ts │ │ │ └── utils/ │ │ │ └── assign-metadata.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ └── tsconfig.spec.json │ ├── fs/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── directories.ts │ │ │ ├── fs.spec.ts │ │ │ └── fs.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ └── tsconfig.spec.json │ ├── http/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── form-file.ts │ │ │ ├── http.spec.ts │ │ │ ├── http.ts │ │ │ └── proxy.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ └── tsconfig.spec.json │ ├── logger/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── app-insights.ts │ │ │ ├── logger.ts │ │ │ ├── serialize-log.ts │ │ │ └── winston-appinsights-transport.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ └── tsconfig.spec.json │ ├── socket-events/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── project.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ └── socket-events.ts │ │ ├── tsconfig.json │ │ └── tsconfig.lib.json │ ├── translations/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── project.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ └── field-translations.ts │ │ ├── tsconfig.json │ │ └── tsconfig.lib.json │ ├── types/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── project.json │ │ ├── src/ │ │ │ ├── dtos/ │ │ │ │ ├── account/ │ │ │ │ │ ├── account.dto.ts │ │ │ │ │ ├── create-account.dto.ts │ │ │ │ │ └── update-account.dto.ts │ │ │ │ ├── custom-shortcut/ │ │ │ │ │ ├── create-custom-shortcut.dto.ts │ │ │ │ │ ├── custom-shortcut.dto.ts │ │ │ │ │ └── update-custom-shortcut.dto.ts │ │ │ │ ├── database/ │ │ │ │ │ └── entity.dto.ts │ │ │ │ ├── directory-watcher/ │ │ │ │ │ ├── create-directory-watcher.dto.ts │ │ │ │ │ ├── directory-watcher.dto.ts │ │ │ │ │ └── update-directory-watcher.dto.ts │ │ │ │ ├── index.ts │ │ │ │ ├── notification/ │ │ │ │ │ ├── create-notification.dto.ts │ │ │ │ │ └── update-notification.dto.ts │ │ │ │ ├── post/ │ │ │ │ │ ├── post-event.dto.ts │ │ │ │ │ ├── post-queue-action.dto.ts │ │ │ │ │ ├── post-queue-record.dto.ts │ │ │ │ │ ├── post-record.dto.ts │ │ │ │ │ └── queue-post-record-request.dto.ts │ │ │ │ ├── settings/ │ │ │ │ │ ├── settings.dto.ts │ │ │ │ │ └── update-settings.dto.ts │ │ │ │ ├── submission/ │ │ │ │ │ ├── apply-multi-submission.dto.ts │ │ │ │ │ ├── create-submission.dto.ts │ │ │ │ │ ├── file-buffer.dto.ts │ │ │ │ │ ├── reorder-submission-files.dto.ts │ │ │ │ │ ├── submission-file.dto.ts │ │ │ │ │ ├── submission.dto.ts │ │ │ │ │ ├── update-alt-file.dto.ts │ │ │ │ │ ├── update-submission-template-name.dto.ts │ │ │ │ │ └── update-submission.dto.ts │ │ │ │ ├── tag/ │ │ │ │ │ ├── create-tag-converter.dto.ts │ │ │ │ │ ├── create-tag-group.dto.ts │ │ │ │ │ ├── tag-converter.dto.ts │ │ │ │ │ ├── tag-group.dto.ts │ │ │ │ │ ├── update-tag-converter.dto.ts │ │ │ │ │ └── update-tag-group.dto.ts │ │ │ │ ├── user/ │ │ │ │ │ ├── create-user-converter.dto.ts │ │ │ │ │ ├── update-user-converter.dto.ts │ │ │ │ │ └── user-converter.dto.ts │ │ │ │ ├── website/ │ │ │ │ │ ├── custom-website-route.dto.ts │ │ │ │ │ ├── form-generation-request.dto.ts │ │ │ │ │ ├── oauth-website-request.dto.ts │ │ │ │ │ ├── set-website-data-request.dto.ts │ │ │ │ │ ├── website-data.dto.ts │ │ │ │ │ └── website-info.dto.ts │ │ │ │ └── website-options/ │ │ │ │ ├── create-user-specified-website-options.dto.ts │ │ │ │ ├── create-website-options.dto.ts │ │ │ │ ├── preview-description.dto.ts │ │ │ │ ├── update-submission-website-options.dto.ts │ │ │ │ ├── update-user-specified-website-options.dto.ts │ │ │ │ ├── update-website-options.dto.ts │ │ │ │ ├── user-specified-website-options.dto.ts │ │ │ │ ├── validate-website-options.dto.ts │ │ │ │ └── website-options.dto.ts │ │ │ ├── enums/ │ │ │ │ ├── description-types.enum.ts │ │ │ │ ├── directory-watcher-import-action.enum.ts │ │ │ │ ├── file-type.enum.ts │ │ │ │ ├── index.ts │ │ │ │ ├── post-event-type.enum.ts │ │ │ │ ├── post-record-resume-mode.enum.ts │ │ │ │ ├── post-record-state.enum.ts │ │ │ │ ├── schedule-type.enum.ts │ │ │ │ ├── submission-rating.enum.ts │ │ │ │ └── submission-type.enum.ts │ │ │ ├── index.ts │ │ │ ├── models/ │ │ │ │ ├── account/ │ │ │ │ │ └── account.interface.ts │ │ │ │ ├── common/ │ │ │ │ │ └── dynamic-object.ts │ │ │ │ ├── custom-shortcut/ │ │ │ │ │ └── custom-shortcut.interface.ts │ │ │ │ ├── database/ │ │ │ │ │ ├── entity-primitive.type.ts │ │ │ │ │ └── entity.interface.ts │ │ │ │ ├── directory-watcher/ │ │ │ │ │ └── directory-watcher.interface.ts │ │ │ │ ├── file/ │ │ │ │ │ ├── file-buffer.interface.ts │ │ │ │ │ └── file-dimensions.interface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── notification/ │ │ │ │ │ └── notification.interface.ts │ │ │ │ ├── post/ │ │ │ │ │ ├── post-event.interface.ts │ │ │ │ │ ├── post-queue-record.interface.ts │ │ │ │ │ ├── post-record.interface.ts │ │ │ │ │ └── post-response.type.ts │ │ │ │ ├── remote/ │ │ │ │ │ └── update-cookies-remote.type.ts │ │ │ │ ├── settings/ │ │ │ │ │ ├── settings-options.interface.ts │ │ │ │ │ ├── settings.constants.ts │ │ │ │ │ └── settings.interface.ts │ │ │ │ ├── submission/ │ │ │ │ │ ├── default-submission-file-props.ts │ │ │ │ │ ├── description-value.type.ts │ │ │ │ │ ├── file-submission/ │ │ │ │ │ │ ├── file-submission-metadata.type.ts │ │ │ │ │ │ ├── file-submission.ts │ │ │ │ │ │ └── modified-file-dimension.type.ts │ │ │ │ │ ├── message-submission/ │ │ │ │ │ │ └── message-submission.type.ts │ │ │ │ │ ├── npf-description.type.ts │ │ │ │ │ ├── submission-file-props.interface.ts │ │ │ │ │ ├── submission-file.interface.ts │ │ │ │ │ ├── submission-metadata.interface.ts │ │ │ │ │ ├── submission-metadata.type.ts │ │ │ │ │ ├── submission-schedule-info.interface.ts │ │ │ │ │ ├── submission.interface.ts │ │ │ │ │ ├── validation-result.type.ts │ │ │ │ │ └── website-form-fields.interface.ts │ │ │ │ ├── tag/ │ │ │ │ │ ├── default-tag-value.ts │ │ │ │ │ ├── tag-converter.interface.ts │ │ │ │ │ ├── tag-group.interface.ts │ │ │ │ │ ├── tag-value.type.ts │ │ │ │ │ └── tag.type.ts │ │ │ │ ├── update/ │ │ │ │ │ └── update.type.ts │ │ │ │ ├── user/ │ │ │ │ │ └── user-converter.interface.ts │ │ │ │ ├── website/ │ │ │ │ │ ├── file-website-form-fields.interface.ts │ │ │ │ │ ├── folder.type.ts │ │ │ │ │ ├── image-resize-props.ts │ │ │ │ │ ├── login-request-data.type.ts │ │ │ │ │ ├── login-response.interface.ts │ │ │ │ │ ├── login-state.class.ts │ │ │ │ │ ├── login-state.interface.ts │ │ │ │ │ ├── post-data.type.ts │ │ │ │ │ ├── website-data.interface.ts │ │ │ │ │ ├── website-info.interface.ts │ │ │ │ │ ├── website-login-type.ts │ │ │ │ │ └── website.type.ts │ │ │ │ └── website-options/ │ │ │ │ ├── user-specified-website-options.interface.ts │ │ │ │ └── website-options.interface.ts │ │ │ ├── website-modifiers/ │ │ │ │ ├── index.ts │ │ │ │ ├── oauth-routes.ts │ │ │ │ ├── username-shortcut.ts │ │ │ │ ├── website-file-options.ts │ │ │ │ └── website-metadata.ts │ │ │ └── website-public/ │ │ │ ├── README.md │ │ │ ├── bluesky-account-data.ts │ │ │ ├── custom-account-data.ts │ │ │ ├── discord-account-data.ts │ │ │ ├── e621-account-data.ts │ │ │ ├── index.ts │ │ │ ├── inkbunny-account-data.ts │ │ │ ├── instagram-account-data.ts │ │ │ ├── megalodon-account-data.ts │ │ │ ├── misskey-account-data.ts │ │ │ ├── telegram-account-data.ts │ │ │ └── twitter-account-data.ts │ │ ├── tsconfig.json │ │ └── tsconfig.lib.json │ └── utils/ │ ├── electron/ │ │ ├── .eslintrc.json │ │ ├── README.md │ │ ├── jest.config.ts │ │ ├── project.json │ │ ├── src/ │ │ │ ├── index.ts │ │ │ └── lib/ │ │ │ ├── browser-window-utils.ts │ │ │ ├── postybirb-env-config.ts │ │ │ ├── remote-utils.ts │ │ │ ├── startup-options-electron.ts │ │ │ ├── utils-electron.spec.ts │ │ │ ├── utils-electron.ts │ │ │ └── utils-test.ts │ │ ├── tsconfig.json │ │ ├── tsconfig.lib.json │ │ └── tsconfig.spec.json │ └── file-type/ │ ├── .eslintrc.json │ ├── README.md │ ├── project.json │ ├── src/ │ │ ├── index.ts │ │ └── lib/ │ │ ├── calculate-image-resize.ts │ │ ├── get-file-type.ts │ │ ├── is-audio.ts │ │ ├── is-image.ts │ │ ├── is-text.ts │ │ ├── is-video.ts │ │ └── mime-helper.ts │ ├── tsconfig.json │ └── tsconfig.lib.json ├── lingui.config.ts ├── nx.json ├── package.json ├── packaging-resources/ │ ├── entitlements.mac.plist │ ├── icons/ │ │ └── icon.icns │ └── installer.nsh ├── scripts/ │ ├── add-website/ │ │ ├── create-file-from-template.js │ │ ├── create-website.js │ │ ├── parse-add-website-input.js │ │ └── templates/ │ │ ├── account-data.hbs │ │ ├── file-submission.hbs │ │ ├── message-submission.hbs │ │ └── website.hbs │ ├── add-website.js │ ├── inject-app-insights.js │ ├── package.json │ ├── tsconfig.json │ └── windows-signing/ │ ├── hasher.cjs │ └── pull-and-sign.ps1 └── tsconfig.base.json
Showing preview only (330K chars total). Download the full file or copy to clipboard to get everything.
SYMBOL INDEX (3277 symbols across 873 files)
FILE: apps/client-server/src/app/account/account.controller.ts
class AccountController (line 21) | class AccountController extends PostyBirbController<'AccountSchema'> {
method constructor (line 22) | constructor(readonly service: AccountService) {
method create (line 29) | create(@Body() createAccountDto: CreateAccountDto) {
method clear (line 38) | async clear(@Param('id') id: AccountId) {
method refresh (line 49) | async refresh(@Param('id') id: AccountId) {
method update (line 56) | update(
method setWebsiteData (line 67) | setWebsiteData(@Body() oauthRequestDto: SetWebsiteDataRequestDto) {
FILE: apps/client-server/src/app/account/account.events.ts
type AccountEventTypes (line 5) | type AccountEventTypes = AccountUpdateEvent;
class AccountUpdateEvent (line 7) | class AccountUpdateEvent implements WebsocketEvent<IAccountDto[]> {
FILE: apps/client-server/src/app/account/account.module.ts
class AccountModule (line 12) | class AccountModule {}
FILE: apps/client-server/src/app/account/account.service.spec.ts
method withWebsiteInstance (line 25) | withWebsiteInstance(websiteInstance) {
method withWebsiteInstance (line 35) | withWebsiteInstance(websiteInstance) {
method withWebsiteInstance (line 45) | withWebsiteInstance(websiteInstance) {
FILE: apps/client-server/src/app/account/account.service.ts
class AccountService (line 34) | class AccountService
method constructor (line 48) | constructor(
method onModuleInit (line 67) | async onModuleInit() {
method pollLoginStates (line 91) | private pollLoginStates() {
method deleteUnregisteredAccounts (line 97) | private async deleteUnregisteredAccounts() {
method populateNullAccount (line 124) | private async populateNullAccount(): Promise<void> {
method initWebsiteRegistry (line 133) | private async initWebsiteRegistry(): Promise<void> {
method initWebsiteLoginRefreshTimers (line 147) | private initWebsiteLoginRefreshTimers(): void {
method emit (line 166) | public async emit() {
method executeOnLoginForInterval (line 183) | private async executeOnLoginForInterval(interval: string | number) {
method afterCreate (line 201) | private afterCreate(account: Account, website: UnknownWebsite) {
method manuallyExecuteOnLogin (line 214) | async manuallyExecuteOnLogin(id: AccountId): Promise<void> {
method create (line 231) | async create(createDto: CreateAccountDto): Promise<Account> {
method findById (line 246) | public findById(id: AccountId, options?: FindOptions) {
method findAll (line 252) | public async findAll() {
method update (line 262) | async update(id: AccountId, update: UpdateAccountDto) {
method remove (line 269) | async remove(id: AccountId) {
method clearAccountData (line 282) | async clearAccountData(id: AccountId) {
method setAccountData (line 296) | async setAccountData(setWebsiteDataRequestDto: SetWebsiteDataRequestDt...
method injectWebsiteInstance (line 308) | private injectWebsiteInstance(account?: Account): Account | null {
FILE: apps/client-server/src/app/account/dtos/create-account.dto.ts
class CreateAccountDto (line 8) | class CreateAccountDto implements ICreateAccountDto {
FILE: apps/client-server/src/app/account/dtos/set-website-data-request.dto.ts
class SetWebsiteDataRequestDto (line 9) | class SetWebsiteDataRequestDto
FILE: apps/client-server/src/app/account/dtos/update-account.dto.ts
class UpdateAccountDto (line 8) | class UpdateAccountDto implements IUpdateAccountDto {
FILE: apps/client-server/src/app/account/login-state-poller.ts
class LoginStatePoller (line 13) | class LoginStatePoller {
method constructor (line 22) | constructor(
method checkForChanges (line 31) | checkForChanges(): void {
method checkInstance (line 71) | checkInstance(instance: UnknownWebsite): void {
method statesEqual (line 89) | private statesEqual(a: ILoginState, b: ILoginState): boolean {
FILE: apps/client-server/src/app/app.controller.ts
class AppController (line 6) | class AppController {
method constructor (line 7) | constructor(private readonly appService: AppService) {}
method getData (line 10) | getData() {
FILE: apps/client-server/src/app/app.module.ts
class AppModule (line 69) | class AppModule implements NestModule {
method configure (line 70) | configure(consumer: MiddlewareConsumer) {
FILE: apps/client-server/src/app/app.service.ts
class AppService (line 5) | class AppService {
method getData (line 6) | getData(): Record<string, string> {
FILE: apps/client-server/src/app/common/controller/postybirb-controller.ts
method constructor (line 13) | constructor(protected readonly service: PostyBirbService<T>) {}
method findOne (line 17) | findOne(@Param('id') id: EntityId) {
method findAll (line 25) | findAll() {
method remove (line 35) | async remove(@Query('ids') ids: EntityId | EntityId[]) {
FILE: apps/client-server/src/app/common/service/postybirb-service.ts
method constructor (line 22) | constructor(
method emit (line 39) | protected async emit(event: WebSocketEvents) {
method schema (line 49) | protected get schema() {
method throwIfExists (line 59) | protected async throwIfExists(where: SQL) {
method findById (line 71) | public findById(id: EntityId, options?: FindOptions) {
method findAll (line 75) | public findAll() {
method remove (line 79) | public remove(id: EntityId) {
FILE: apps/client-server/src/app/constants.ts
constant WEBSITE_IMPLEMENTATIONS (line 2) | const WEBSITE_IMPLEMENTATIONS = 'WEBSITE_IMPLEMENTATIONS';
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcut.events.ts
type CustomShortcutEventTypes (line 5) | type CustomShortcutEventTypes = CustomShortcutEvent;
class CustomShortcutEvent (line 7) | class CustomShortcutEvent implements WebsocketEvent<ICustomShortcut[]> {
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcuts.controller.ts
class CustomShortcutsController (line 10) | class CustomShortcutsController extends PostyBirbController<'CustomShort...
method constructor (line 11) | constructor(readonly service: CustomShortcutsService) {
method create (line 17) | async create(@Body() createCustomShortcutDto: CreateCustomShortcutDto) {
method update (line 25) | async update(
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcuts.module.ts
class CustomShortcutsModule (line 10) | class CustomShortcutsModule {}
FILE: apps/client-server/src/app/custom-shortcuts/custom-shortcuts.service.ts
class CustomShortcutsService (line 12) | class CustomShortcutsService extends PostyBirbService<'CustomShortcutSch...
method constructor (line 13) | constructor(@Optional() webSocket?: WSGateway) {
method emit (line 18) | public async emit() {
method create (line 26) | public async create(
method update (line 38) | public async update(
method remove (line 52) | public async remove(id: EntityId) {
FILE: apps/client-server/src/app/custom-shortcuts/dtos/create-custom-shortcut.dto.ts
class CreateCustomShortcutDto (line 4) | class CreateCustomShortcutDto implements ICreateCustomShortcutDto {
FILE: apps/client-server/src/app/custom-shortcuts/dtos/update-custom-shortcut.dto.ts
class UpdateCustomShortcutDto (line 4) | class UpdateCustomShortcutDto implements IUpdateCustomShortcutDto {
FILE: apps/client-server/src/app/directory-watchers/directory-watcher.events.ts
type DirectoryWatcherEventTypes (line 5) | type DirectoryWatcherEventTypes = DirectoryWatcherUpdateEvent;
class DirectoryWatcherUpdateEvent (line 7) | class DirectoryWatcherUpdateEvent
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.controller.ts
class DirectoryWatchersController (line 21) | class DirectoryWatchersController extends PostyBirbController<'Directory...
method constructor (line 22) | constructor(readonly service: DirectoryWatchersService) {
method create (line 29) | create(@Body() createDto: CreateDirectoryWatcherDto) {
method update (line 36) | update(
method checkPath (line 46) | checkPath(@Body() checkPathDto: CheckPathDto) {
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.module.ts
class DirectoryWatchersModule (line 12) | class DirectoryWatchersModule {}
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.service.spec.ts
function createSubmission (line 50) | async function createSubmission() {
FILE: apps/client-server/src/app/directory-watchers/directory-watchers.service.ts
constant FILE_COUNT_WARNING_THRESHOLD (line 45) | const FILE_COUNT_WARNING_THRESHOLD = 10;
type CheckPathResult (line 50) | interface CheckPathResult {
class DirectoryWatchersService (line 62) | class DirectoryWatchersService extends PostyBirbService<'DirectoryWatche...
method constructor (line 73) | constructor(
method emitUpdates (line 84) | protected async emitUpdates() {
method run (line 95) | private async run() {
method ensureDirectoryStructure (line 122) | private async ensureDirectoryStructure(basePath: string): Promise<void> {
method recoverOrphanedFiles (line 147) | private async recoverOrphanedFiles(watcher: DirectoryWatcher): Promise...
method read (line 195) | private async read(watcher: DirectoryWatcher) {
method processFileWithMove (line 261) | private async processFileWithMove(
method create (line 373) | async create(
method update (line 393) | async update(id: EntityId, update: UpdateDirectoryWatcherDto) {
method checkPath (line 441) | async checkPath(path: string): Promise<CheckPathResult> {
FILE: apps/client-server/src/app/directory-watchers/dtos/check-path.dto.ts
class CheckPathDto (line 4) | class CheckPathDto {
FILE: apps/client-server/src/app/directory-watchers/dtos/create-directory-watcher.dto.ts
class CreateDirectoryWatcherDto (line 8) | class CreateDirectoryWatcherDto implements ICreateDirectoryWatcherDto {
FILE: apps/client-server/src/app/directory-watchers/dtos/update-directory-watcher.dto.ts
class UpdateDirectoryWatcherDto (line 9) | class UpdateDirectoryWatcherDto implements IUpdateDirectoryWatcherDto {
FILE: apps/client-server/src/app/drizzle/models/account.entity.ts
class Account (line 7) | class Account extends DatabaseEntity implements IAccount {
method constructor (line 26) | constructor(entity: Partial<IAccount>) {
method toObject (line 31) | toObject(): IAccount {
method toDTO (line 37) | toDTO(): IAccountDto {
method withWebsiteInstance (line 56) | withWebsiteInstance(websiteInstance: UnknownWebsite): this {
FILE: apps/client-server/src/app/drizzle/models/custom-shortcut.entity.ts
class CustomShortcut (line 10) | class CustomShortcut extends DatabaseEntity implements ICustomShortcut {
method constructor (line 15) | constructor(entity: Partial<ICustomShortcut>) {
method toObject (line 20) | public toObject(): ICustomShortcut {
method toDTO (line 26) | public toDTO(): ICustomShortcutDto {
FILE: apps/client-server/src/app/drizzle/models/database-entity.spec.ts
class TestEntity (line 6) | class TestEntity extends DatabaseEntity {
method constructor (line 9) | constructor(entity: Partial<TestEntity>) {
method toObject (line 14) | toObject(): IEntity {
method toDTO (line 20) | toDTO(): IEntityDto {
FILE: apps/client-server/src/app/drizzle/models/database-entity.ts
function fromDatabaseRecord (line 18) | function fromDatabaseRecord<TEntity>(
method constructor (line 39) | constructor(entity: Partial<IEntity>) {
method toJSON (line 50) | public toJSON(): string {
FILE: apps/client-server/src/app/drizzle/models/directory-watcher.entity.ts
class DirectoryWatcher (line 11) | class DirectoryWatcher
method toObject (line 24) | toObject(): IDirectoryWatcher {
method toDTO (line 30) | toDTO(): DirectoryWatcherDto {
FILE: apps/client-server/src/app/drizzle/models/file-buffer.entity.ts
class FileBuffer (line 5) | class FileBuffer extends DatabaseEntity implements IFileBuffer {
method constructor (line 22) | constructor(entity: Partial<FileBuffer>) {
method toObject (line 27) | toObject(): IFileBuffer {
method toDTO (line 33) | toDTO(): FileBufferDto {
FILE: apps/client-server/src/app/drizzle/models/notification.entity.ts
class Notification (line 5) | class Notification extends DatabaseEntity implements INotification {
method toObject (line 20) | toObject(): INotification {
method toDTO (line 24) | toDTO(): INotification {
FILE: apps/client-server/src/app/drizzle/models/post-event.entity.ts
class PostEvent (line 15) | class PostEvent extends DatabaseEntity implements IPostEvent {
method constructor (line 36) | constructor(entity: Partial<IPostEvent>) {
method toObject (line 41) | toObject(): IPostEvent {
method toDTO (line 47) | toDTO(): PostEventDto {
FILE: apps/client-server/src/app/drizzle/models/post-queue-record.entity.ts
class PostQueueRecord (line 12) | class PostQueueRecord
method toObject (line 26) | toObject(): IPostQueueRecord {
method toDTO (line 32) | toDTO(): PostQueueRecordDto {
FILE: apps/client-server/src/app/drizzle/models/post-record.entity.ts
class PostRecord (line 15) | class PostRecord extends DatabaseEntity implements IPostRecord {
method constructor (line 48) | constructor(entity: Partial<IPostRecord>) {
method toObject (line 58) | toObject(): IPostRecord {
method toDTO (line 64) | toDTO(): PostRecordDto {
FILE: apps/client-server/src/app/drizzle/models/settings.entity.ts
class Settings (line 10) | class Settings extends DatabaseEntity implements ISettings {
method toObject (line 15) | toObject(): ISettings {
method toDTO (line 21) | toDTO(): SettingsDto {
FILE: apps/client-server/src/app/drizzle/models/submission-file.entity.ts
class SubmissionFile (line 14) | class SubmissionFile extends DatabaseEntity implements ISubmissionFile {
method constructor (line 57) | constructor(entity: Partial<SubmissionFile>) {
method toObject (line 62) | toObject(): ISubmissionFile {
method toDTO (line 68) | toDTO(): ISubmissionFileDto {
method load (line 77) | public async load(fileTarget?: 'file' | 'thumbnail' | 'alt') {
FILE: apps/client-server/src/app/drizzle/models/submission.entity.ts
class Submission (line 16) | class Submission<T extends ISubmissionMetadata = ISubmissionMetadata>
method constructor (line 52) | constructor(entity: Partial<ISubmission<T>>) {
method toObject (line 57) | toObject(): ISubmission {
method toDTO (line 63) | toDTO(): ISubmissionDto {
method getSubmissionName (line 76) | getSubmissionName(): string {
FILE: apps/client-server/src/app/drizzle/models/tag-converter.entity.ts
class TagConverter (line 5) | class TagConverter extends DatabaseEntity implements ITagConverter {
method constructor (line 10) | constructor(entity: ITagConverter) {
method toObject (line 15) | toObject(): ITagConverter {
method toDTO (line 21) | toDTO(): TagConverterDto {
FILE: apps/client-server/src/app/drizzle/models/tag-group.entity.ts
class TagGroup (line 5) | class TagGroup extends DatabaseEntity implements ITagGroup {
method toObject (line 10) | toObject(): ITagGroup {
method toDTO (line 16) | toDTO(): TagGroupDto {
FILE: apps/client-server/src/app/drizzle/models/user-converter.entity.ts
class UserConverter (line 5) | class UserConverter extends DatabaseEntity implements IUserConverter {
method constructor (line 10) | constructor(entity: IUserConverter) {
method toObject (line 15) | toObject(): IUserConverter {
method toDTO (line 21) | toDTO(): UserConverterDto {
FILE: apps/client-server/src/app/drizzle/models/user-specified-website-options.entity.ts
class UserSpecifiedWebsiteOptions (line 12) | class UserSpecifiedWebsiteOptions
method toObject (line 25) | toObject(): IUserSpecifiedWebsiteOptions {
method toDTO (line 31) | toDTO(): UserSpecifiedWebsiteOptionsDto {
FILE: apps/client-server/src/app/drizzle/models/website-data.entity.ts
class WebsiteData (line 11) | class WebsiteData<T extends DynamicObject = any>
method toObject (line 20) | toObject(): IWebsiteData {
method toDTO (line 24) | toDTO(): IWebsiteDataDto {
FILE: apps/client-server/src/app/drizzle/models/website-options.entity.ts
class WebsiteOptions (line 14) | class WebsiteOptions extends DatabaseEntity implements IWebsiteOptions {
method constructor (line 29) | constructor(entity: Partial<WebsiteOptions>) {
method toObject (line 34) | toObject(): IWebsiteOptions {
method toDTO (line 40) | toDTO(): WebsiteOptionsDto {
FILE: apps/client-server/src/app/drizzle/postybirb-database/find-options.type.ts
type FindOptions (line 1) | type FindOptions = {
FILE: apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.ts
type ExtractedRelations (line 24) | type ExtractedRelations = ExtractTablesWithRelations<typeof Schemas>;
type Relation (line 26) | type Relation<TSchemaKey extends SchemaKey> =
type Action (line 29) | type Action = 'delete' | 'insert' | 'update';
type SubscribeCallback (line 31) | type SubscribeCallback = (ids: EntityId[], action: Action) => void;
class PostyBirbDatabase (line 33) | class PostyBirbDatabase<
method constructor (line 62) | constructor(
method subscribe (line 76) | public subscribe(
method notify (line 91) | private notify(ids: EntityId[], action: Action) {
method forceNotify (line 105) | public forceNotify(ids: EntityId[], action: Action) {
method notifySubscribers (line 117) | public static notifySubscribers(
method EntityClass (line 127) | public get EntityClass() {
method schemaEntity (line 131) | public get schemaEntity() {
method classConverter (line 137) | private classConverter(value: any | any[]): TEntityClass | TEntityClas...
method insert (line 146) | public async insert(
method deleteById (line 165) | public async deleteById(ids: EntityId[]) {
method findById (line 176) | public async findById(
method select (line 200) | public async select(query: SQL): Promise<TEntityClass[]> {
method find (line 205) | public async find<
method findOne (line 232) | public async findOne<
method findAll (line 259) | public async findAll(): Promise<TEntityClass[]> {
method update (line 268) | public async update(
method count (line 283) | public count(filter?: SQL): Promise<number> {
FILE: apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.util.ts
class PostyBirbDatabaseUtil (line 6) | class PostyBirbDatabaseUtil {
method saveFromEntity (line 7) | static async saveFromEntity<T extends DatabaseEntity>(entity: T) {
FILE: apps/client-server/src/app/drizzle/postybirb-database/schema-entity-map.ts
type DatabaseSchemaEntityMap (line 22) | type DatabaseSchemaEntityMap = {
FILE: apps/client-server/src/app/drizzle/transaction-context.ts
type TrackedEntity (line 8) | interface TrackedEntity {
class TransactionContext (line 18) | class TransactionContext {
method constructor (line 25) | constructor(db: PostyBirbDatabaseType) {
method track (line 33) | track(schemaKey: SchemaKey, id: EntityId): void {
method trackMany (line 40) | trackMany(schemaKey: SchemaKey, ids: EntityId[]): void {
method getDb (line 47) | getDb(): PostyBirbDatabaseType {
method cleanup (line 55) | async cleanup(): Promise<void> {
method commit (line 83) | commit(): void {
function withTransactionContext (line 123) | async function withTransactionContext<T>(
FILE: apps/client-server/src/app/file-converter/converters/file-converter.ts
type IFileConverter (line 3) | interface IFileConverter {
FILE: apps/client-server/src/app/file-converter/converters/text-file-converter.ts
type SupportedInputMimeTypes (line 13) | type SupportedInputMimeTypes = (typeof supportedInputMimeTypes)[number];
type SupportedOutputMimeTypes (line 14) | type SupportedOutputMimeTypes = (typeof supportedOutputMimeTypes)[number];
type ConversionMap (line 16) | type ConversionMap = {
type ConversionWeights (line 24) | type ConversionWeights = {
class TextFileConverter (line 34) | class TextFileConverter implements IFileConverter {
method canConvert (line 98) | canConvert(file: IFileBuffer, allowableOutputMimeTypes: string[]): boo...
method convert (line 107) | async convert(
method toMergedBuffer (line 135) | private toMergedBuffer(
FILE: apps/client-server/src/app/file-converter/file-converter.module.ts
class FileConverterModule (line 8) | class FileConverterModule {}
FILE: apps/client-server/src/app/file-converter/file-converter.service.ts
class FileConverterService (line 7) | class FileConverterService {
method convert (line 10) | public async convert<T extends IFileBuffer>(
method canConvert (line 25) | public async canConvert(
FILE: apps/client-server/src/app/file/file.controller.ts
class FileController (line 15) | class FileController {
method constructor (line 16) | constructor(private readonly service: FileService) {}
method getThumbnail (line 21) | async getThumbnail(
method getFileBufferForTarget (line 39) | private getFileBufferForTarget(
FILE: apps/client-server/src/app/file/file.module.ts
class FileModule (line 14) | class FileModule {}
FILE: apps/client-server/src/app/file/file.service.spec.ts
function createSubmission (line 43) | async function createSubmission() {
function createMulterData (line 52) | function createMulterData(path: string): MulterFileInfo {
function createMulterData2 (line 66) | function createMulterData2(path: string): MulterFileInfo {
function setup (line 80) | function setup(): string[] {
function loadBuffers (line 136) | async function loadBuffers(rec: SubmissionFile) {
FILE: apps/client-server/src/app/file/file.service.ts
class FileService (line 29) | class FileService {
method constructor (line 45) | constructor(
method remove (line 56) | public async remove(id: EntityId) {
method create (line 68) | public async create(
method update (line 83) | public async update(
method doTask (line 96) | private async doTask(task: Task): Promise<SubmissionFile> {
method sanitizeFilename (line 126) | private sanitizeFilename(filename: string): string {
method getFile (line 132) | private async getFile(path: string, taskOrigin: TaskOrigin): Promise<B...
method findFile (line 147) | public async findFile(id: EntityId): Promise<SubmissionFile> {
method getAltFileSize (line 155) | async getAltFileSize(id: EntityId): Promise<number> {
method getAltText (line 166) | async getAltText(id: EntityId): Promise<string> {
method updateAltText (line 182) | async updateAltText(id: EntityId, update: UpdateAltFileDto) {
method updateMetadata (line 191) | async updateMetadata(id: string, update: SubmissionFileMetadata) {
method reorderFiles (line 197) | async reorderFiles(update: ReorderSubmissionFilesDto) {
FILE: apps/client-server/src/app/file/models/multer-file-info.ts
type MulterFileInfo (line 7) | interface MulterFileInfo {
type TaskOrigin (line 24) | type TaskOrigin = 'directory-watcher';
FILE: apps/client-server/src/app/file/models/task-type.enum.ts
type TaskType (line 4) | enum TaskType {
FILE: apps/client-server/src/app/file/models/task.ts
type Task (line 6) | type Task = CreateTask | UpdateTask;
type CreateTask (line 9) | type CreateTask = {
type UpdateTask (line 24) | type UpdateTask = {
FILE: apps/client-server/src/app/file/services/create-file.service.ts
class CreateFileService (line 40) | class CreateFileService {
method constructor (line 51) | constructor(
method create (line 65) | public async create(
method createSubmissionTextAltFile (line 128) | async createSubmissionTextAltFile(
method createSubmissionFile (line 199) | private async createSubmissionFile(
method populateAsImageFile (line 241) | private async populateAsImageFile(
method createFileThumbnail (line 289) | public async createFileThumbnail(
method generateThumbnail (line 326) | public async generateThumbnail(
method createFileBufferEntity (line 359) | public async createFileBufferEntity(
FILE: apps/client-server/src/app/file/services/update-file.service.ts
class UpdateFileService (line 32) | class UpdateFileService {
method constructor (line 43) | constructor(
method update (line 57) | public async update(
method replaceFileThumbnail (line 81) | private async replaceFileThumbnail(
method replacePrimaryFile (line 135) | async replacePrimaryFile(
method updateFileEntity (line 144) | private async updateFileEntity(
method repopulateTextFile (line 223) | async repopulateTextFile(
method updateImageFileProps (line 261) | private async updateImageFileProps(
method getImageDetails (line 348) | private async getImageDetails(file: MulterFileInfo, buf: Buffer) {
method findFile (line 362) | private async findFile(id: EntityId): Promise<SubmissionFile> {
FILE: apps/client-server/src/app/file/utils/image.util.ts
class ImageUtil (line 9) | class ImageUtil {
method isImage (line 10) | static isImage(mimetype: string, includeGIF = false): boolean {
FILE: apps/client-server/src/app/form-generator/dtos/form-generation-request.dto.ts
class FormGenerationRequestDto (line 9) | class FormGenerationRequestDto implements IFormGenerationRequestDto {
FILE: apps/client-server/src/app/form-generator/form-generator.controller.ts
class FormGeneratorController (line 9) | class FormGeneratorController {
method constructor (line 10) | constructor(private readonly service: FormGeneratorService) {}
method getFormForWebsite (line 22) | getFormForWebsite(@Body() request: FormGenerationRequestDto) {
FILE: apps/client-server/src/app/form-generator/form-generator.module.ts
class FormGeneratorModule (line 14) | class FormGeneratorModule {}
FILE: apps/client-server/src/app/form-generator/form-generator.service.ts
class FormGeneratorService (line 22) | class FormGeneratorService {
method constructor (line 23) | constructor(
method generateForm (line 35) | async generateForm(
method getDefaultForm (line 86) | async getDefaultForm(type: SubmissionType, isMultiSubmission = false) {
method populateUserDefaults (line 98) | private async populateUserDefaults(
FILE: apps/client-server/src/app/image-processing/image-processing.module.ts
class ImageProcessingModule (line 14) | class ImageProcessingModule {}
FILE: apps/client-server/src/app/image-processing/sharp-instance-manager.ts
type SharpWorkerInput (line 13) | interface SharpWorkerInput {
type SharpWorkerResult (line 31) | interface SharpWorkerResult {
class SharpInstanceManager (line 56) | class SharpInstanceManager implements OnModuleDestroy {
method constructor (line 69) | constructor() {
method runHealthCheck (line 108) | private async runHealthCheck(): Promise<void> {
method onModuleDestroy (line 135) | async onModuleDestroy() {
method resolveWorkerPath (line 158) | private resolveWorkerPath(): string {
method processImage (line 202) | async processImage(input: SharpWorkerInput): Promise<SharpWorkerResult> {
method getMetadata (line 235) | async getMetadata(buffer: Buffer): Promise<{
method generateThumbnail (line 259) | async generateThumbnail(
method resizeForPost (line 292) | async resizeForPost(input: {
method getStats (line 324) | getStats() {
FILE: apps/client-server/src/app/legacy-database-importer/converters/legacy-converter.ts
method constructor (line 18) | constructor(protected readonly databasePath: string) {}
method getEntityFilePath (line 20) | private getEntityFilePath(): string {
method getModernDatabase (line 24) | private getModernDatabase() {
method import (line 28) | public async import(): Promise<void> {
FILE: apps/client-server/src/app/legacy-database-importer/converters/legacy-custom-shortcut.converter.ts
class LegacyCustomShortcutConverter (line 5) | class LegacyCustomShortcutConverter extends LegacyConverter {
FILE: apps/client-server/src/app/legacy-database-importer/converters/legacy-tag-converter.converter.ts
class LegacyTagConverterConverter (line 5) | class LegacyTagConverterConverter extends LegacyConverter {
FILE: apps/client-server/src/app/legacy-database-importer/converters/legacy-tag-group.converter.ts
class LegacyTagGroupConverter (line 5) | class LegacyTagGroupConverter extends LegacyConverter {
FILE: apps/client-server/src/app/legacy-database-importer/converters/legacy-user-account.converter.ts
class LegacyUserAccountConverter (line 5) | class LegacyUserAccountConverter extends LegacyConverter {
FILE: apps/client-server/src/app/legacy-database-importer/converters/legacy-website-data.converter.spec.ts
function runConverters (line 47) | async function runConverters() {
FILE: apps/client-server/src/app/legacy-database-importer/converters/legacy-website-data.converter.ts
class LegacyWebsiteDataConverter (line 17) | class LegacyWebsiteDataConverter extends LegacyConverter {
FILE: apps/client-server/src/app/legacy-database-importer/dtos/legacy-import.dto.ts
class LegacyImportDto (line 3) | class LegacyImportDto {
FILE: apps/client-server/src/app/legacy-database-importer/legacy-database-importer.controller.ts
class LegacyDatabaseImporterController (line 6) | class LegacyDatabaseImporterController {
method constructor (line 7) | constructor(
method import (line 12) | async import(@Body() importRequest: LegacyImportDto) {
FILE: apps/client-server/src/app/legacy-database-importer/legacy-database-importer.module.ts
class LegacyDatabaseImporterModule (line 12) | class LegacyDatabaseImporterModule {}
FILE: apps/client-server/src/app/legacy-database-importer/legacy-database-importer.service.ts
class LegacyDatabaseImporterService (line 15) | class LegacyDatabaseImporterService {
method constructor (line 23) | constructor(private readonly accountService: AccountService) {}
method import (line 25) | async import(importRequest: LegacyImportDto): Promise<{ errors: Error[...
method processImport (line 87) | private async processImport(
FILE: apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-converter-entity.ts
type MinimalEntity (line 3) | type MinimalEntity<T extends IEntity> = Omit<
type LegacyConverterEntity (line 8) | interface LegacyConverterEntity<T extends IEntity> {
FILE: apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-custom-shortcut.ts
class LegacyCustomShortcut (line 41) | class LegacyCustomShortcut implements LegacyConverterEntity<ICustomShort...
method constructor (line 54) | constructor(data: Partial<LegacyCustomShortcut>) {
method convert (line 58) | async convert(): Promise<MinimalEntity<ICustomShortcut>> {
method convertLegacyToModernShortcut (line 93) | private convertLegacyToModernShortcut(blocks: TipTapNode[]): TipTapNod...
method convertDefaultToBlock (line 239) | private convertDefaultToBlock(blocks: TipTapNode[]): TipTapNode[] {
method wrapLegacyShortcuts (line 315) | private wrapLegacyShortcuts(content: string): string {
FILE: apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-tag-converter.ts
class LegacyTagConverter (line 12) | class LegacyTagConverter
method constructor (line 25) | constructor(data: Partial<LegacyTagConverter>) {
method convert (line 29) | async convert(): Promise<MinimalEntity<ITagConverter>> {
FILE: apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-tag-group.ts
class LegacyTagGroup (line 11) | class LegacyTagGroup implements LegacyConverterEntity<ITagGroup> {
method constructor (line 22) | constructor(data: Partial<LegacyTagGroup>) {
method convert (line 26) | async convert(): Promise<MinimalEntity<ITagGroup>> {
FILE: apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-user-account.ts
class LegacyUserAccount (line 12) | class LegacyUserAccount implements LegacyConverterEntity<IAccount> {
method constructor (line 25) | constructor(data: Partial<LegacyUserAccount>) {
method convert (line 29) | async convert(): Promise<MinimalEntity<IAccount> | null> {
FILE: apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-website-data.ts
class LegacyWebsiteData (line 19) | class LegacyWebsiteData implements LegacyConverterEntity<IWebsiteData> {
method constructor (line 32) | constructor(data: Partial<LegacyWebsiteData>) {
method convert (line 36) | async convert(): Promise<MinimalEntity<IWebsiteData> | null> {
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/bluesky-data-transformer.ts
type LegacyBlueskyAccountData (line 7) | interface LegacyBlueskyAccountData {
class BlueskyDataTransformer (line 20) | class BlueskyDataTransformer
method transform (line 23) | transform(legacyData: LegacyBlueskyAccountData): BlueskyAccountData | ...
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/custom-data-transformer.ts
type LegacyCustomAccountData (line 8) | interface LegacyCustomAccountData {
class CustomDataTransformer (line 29) | class CustomDataTransformer
method transform (line 32) | transform(legacyData: LegacyCustomAccountData): CustomAccountData | nu...
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/discord-data-transformer.ts
type LegacyDiscordAccountData (line 7) | interface LegacyDiscordAccountData {
class DiscordDataTransformer (line 23) | class DiscordDataTransformer
method transform (line 26) | transform(legacyData: LegacyDiscordAccountData): DiscordAccountData | ...
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/e621-data-transformer.ts
type LegacyE621AccountData (line 7) | interface LegacyE621AccountData {
class E621DataTransformer (line 20) | class E621DataTransformer
method transform (line 23) | transform(legacyData: LegacyE621AccountData): E621AccountData | null {
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/inkbunny-data-transformer.ts
type LegacyInkbunnyAccountData (line 7) | interface LegacyInkbunnyAccountData {
class InkbunnyDataTransformer (line 21) | class InkbunnyDataTransformer
method transform (line 24) | transform(legacyData: LegacyInkbunnyAccountData): InkbunnyAccountData ...
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/megalodon-data-transformer.ts
type LegacyMegalodonAccountData (line 8) | interface LegacyMegalodonAccountData {
class MegalodonDataTransformer (line 27) | class MegalodonDataTransformer
method transform (line 30) | transform(legacyData: LegacyMegalodonAccountData): MegalodonAccountDat...
method normalizeInstanceUrl (line 55) | private normalizeInstanceUrl(url: string): string {
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/telegram-data-transformer.ts
type LegacyTelegramAccountData (line 7) | interface LegacyTelegramAccountData {
class TelegramDataTransformer (line 27) | class TelegramDataTransformer
method transform (line 30) | transform(legacyData: LegacyTelegramAccountData): TelegramAccountData ...
FILE: apps/client-server/src/app/legacy-database-importer/transformers/implementations/twitter-data-transformer.ts
type LegacyTwitterAccountData (line 7) | interface LegacyTwitterAccountData {
class TwitterDataTransformer (line 27) | class TwitterDataTransformer
method transform (line 30) | transform(legacyData: LegacyTwitterAccountData): TwitterAccountData | ...
FILE: apps/client-server/src/app/legacy-database-importer/transformers/legacy-website-data-transformer.ts
type LegacyWebsiteDataTransformer (line 6) | interface LegacyWebsiteDataTransformer<TLegacy = any, TModern = any> {
class PassthroughTransformer (line 19) | class PassthroughTransformer<T = Record<string, unknown>>
method transform (line 22) | transform(legacyData: T): T | null {
FILE: apps/client-server/src/app/legacy-database-importer/transformers/website-data-transformer-registry.ts
class WebsiteDataTransformerRegistry (line 16) | class WebsiteDataTransformerRegistry {
method getTransformer (line 45) | static getTransformer(
method hasTransformer (line 56) | static hasTransformer(legacyWebsiteName: string): boolean {
method getTransformableWebsites (line 63) | static getTransformableWebsites(): string[] {
FILE: apps/client-server/src/app/legacy-database-importer/utils/ndjson-parser.ts
type ParseResult (line 5) | interface ParseResult<T> {
type ParseError (line 10) | interface ParseError {
class NdjsonParser (line 21) | class NdjsonParser {
method parseFile (line 27) | async parseFile<T>(
method fileExists (line 83) | async fileExists(filePath: string): Promise<boolean> {
FILE: apps/client-server/src/app/legacy-database-importer/utils/website-name-mapper.ts
class WebsiteNameMapper (line 6) | class WebsiteNameMapper {
method map (line 54) | static map(legacyName: string): string | null {
method mapMany (line 61) | static mapMany(
method hasMapping (line 73) | static hasMapping(legacyName: string): boolean {
method getAllLegacyNames (line 80) | static getAllLegacyNames(): string[] {
method getAllNewNames (line 87) | static getAllNewNames(): string[] {
FILE: apps/client-server/src/app/logs/logs.controller.ts
class LogsController (line 11) | class LogsController {
method constructor (line 12) | constructor(private readonly service: LogsService) {}
method download (line 16) | download(@Res() response) {
FILE: apps/client-server/src/app/logs/logs.module.ts
class LogsModule (line 10) | class LogsModule {}
FILE: apps/client-server/src/app/logs/logs.service.ts
class LogsService (line 13) | class LogsService {
method getLogsArchive (line 22) | getLogsArchive(): Buffer {
method createTar (line 36) | private createTar(dir: string, fileNames: string[]): Buffer {
method createTarHeader (line 66) | private createTarHeader(fileName: string, fileSize: number): Buffer {
FILE: apps/client-server/src/app/notifications/dtos/create-notification.dto.ts
class CreateNotificationDto (line 5) | class CreateNotificationDto implements ICreateNotificationDto {
FILE: apps/client-server/src/app/notifications/dtos/update-notification.dto.ts
class UpdateNotificationDto (line 5) | class UpdateNotificationDto implements IUpdateNotificationDto {
FILE: apps/client-server/src/app/notifications/notification.events.ts
type NotificationEventTypes (line 5) | type NotificationEventTypes = NotificationEvent;
class NotificationEvent (line 7) | class NotificationEvent implements WebsocketEvent<INotification[]> {
FILE: apps/client-server/src/app/notifications/notifications.controller.ts
class NotificationsController (line 27) | class NotificationsController {
method constructor (line 28) | constructor(readonly service: NotificationsService) {}
method create (line 33) | create(@Body() createDto: CreateNotificationDto) {
method update (line 40) | update(@Param('id') id: EntityId, @Body() updateDto: UpdateNotificatio...
method findAll (line 46) | findAll() {
method remove (line 55) | async remove(@Query('ids') ids: EntityId | EntityId[]) {
FILE: apps/client-server/src/app/notifications/notifications.module.ts
class NotificationsModule (line 20) | class NotificationsModule {}
FILE: apps/client-server/src/app/notifications/notifications.service.ts
class NotificationsService (line 19) | class NotificationsService extends PostyBirbService<'NotificationSchema'> {
method constructor (line 26) | constructor(
method removeStaleNotifications (line 40) | private async removeStaleNotifications() {
method create (line 61) | async create(
method trimNotifications (line 80) | private async trimNotifications() {
method sendDesktopNotification (line 98) | async sendDesktopNotification(
method update (line 160) | update(id: EntityId, update: UpdateNotificationDto) {
method emit (line 169) | protected async emit() {
FILE: apps/client-server/src/app/post-parsers/models/description-node/converters/base-converter.ts
method convertBlocks (line 38) | convertBlocks(nodes: TipTapNode[], context: ConversionContext): string {
method convertRawBlocks (line 46) | convertRawBlocks(blocks: TipTapNode[], context: ConversionContext): stri...
method convertContent (line 73) | protected convertContent(
method convertChildren (line 96) | protected convertChildren(
method shouldRenderShortcut (line 116) | protected shouldRenderShortcut(
method getUsernameShortcutLink (line 133) | protected getUsernameShortcutLink(
FILE: apps/client-server/src/app/post-parsers/models/description-node/converters/bbcode-converter.ts
class BBCodeConverter (line 6) | class BBCodeConverter extends BaseConverter {
method getBlockSeparator (line 7) | protected getBlockSeparator(): string {
method convertBlockNode (line 11) | convertBlockNode(
method convertInlineNode (line 97) | convertInlineNode(
method convertTextNode (line 141) | convertTextNode(
method renderTextWithMarks (line 168) | private renderTextWithMarks(text: string, marks: any[]): string {
FILE: apps/client-server/src/app/post-parsers/models/description-node/converters/custom-converter.ts
type CustomNodeHandler (line 6) | type CustomNodeHandler = (
class CustomConverter (line 15) | class CustomConverter extends BaseConverter {
method constructor (line 16) | constructor(
method getBlockSeparator (line 24) | protected getBlockSeparator(): string {
method convertBlockNode (line 28) | convertBlockNode(
method convertInlineNode (line 35) | convertInlineNode(
method convertTextNode (line 45) | convertTextNode(
FILE: apps/client-server/src/app/post-parsers/models/description-node/converters/html-converter.ts
class HtmlConverter (line 7) | class HtmlConverter extends BaseConverter {
method getBlockSeparator (line 8) | protected getBlockSeparator(): string {
method convertBlockNode (line 12) | convertBlockNode(
method convertInlineNode (line 71) | convertInlineNode(
method convertTextNode (line 122) | convertTextNode(
method renderTextWithMarks (line 152) | private renderTextWithMarks(text: string, marks: any[]): string {
method getBlockTag (line 196) | private getBlockTag(node: TipTapNode): string {
method getBlockStyles (line 203) | private getBlockStyles(node: TipTapNode): string {
method convertImage (line 221) | private convertImage(node: TipTapNode): string {
FILE: apps/client-server/src/app/post-parsers/models/description-node/converters/npf-converter.ts
class NpfConverter (line 16) | class NpfConverter extends BaseConverter {
method getBlockSeparator (line 23) | protected getBlockSeparator(): string {
method convertBlocks (line 30) | convertBlocks(nodes: TipTapNode[], context: ConversionContext): string {
method convertBlockNodeRecursive (line 41) | private convertBlockNodeRecursive(
method convertListItemToNpf (line 108) | private convertListItemToNpf(
method convertBlockNode (line 160) | convertBlockNode(node: TipTapNode, context: ConversionContext): string {
method convertBlockNodeToNpf (line 168) | private convertBlockNodeToNpf(
method convertParagraph (line 191) | private convertParagraph(
method convertHeading (line 201) | private convertHeading(
method convertImage (line 217) | private convertImage(node: TipTapNode): NPFImageBlock {
method convertInlineNode (line 244) | convertInlineNode(node: TipTapNode, context: ConversionContext): string {
method convertTextNode (line 317) | convertTextNode(node: TipTapNode): string {
method extractText (line 330) | private extractText(
method addFormattingForMarks (line 348) | private addFormattingForMarks(
method getMimeType (line 398) | private getMimeType(url: string): string | undefined {
FILE: apps/client-server/src/app/post-parsers/models/description-node/converters/plaintext-converter.ts
class PlainTextConverter (line 6) | class PlainTextConverter extends BaseConverter {
method getBlockSeparator (line 7) | protected getBlockSeparator(): string {
method convertBlockNode (line 11) | convertBlockNode(node: TipTapNode, context: ConversionContext): string {
method convertInlineNode (line 63) | convertInlineNode(node: TipTapNode, context: ConversionContext): string {
method convertTextNode (line 101) | convertTextNode(node: TipTapNode, context: ConversionContext): string {
FILE: apps/client-server/src/app/post-parsers/models/description-node/description-node-tree.ts
type InsertionOptions (line 14) | type InsertionOptions = {
class DescriptionNodeTree (line 20) | class DescriptionNodeTree {
method constructor (line 50) | constructor(
method toBBCode (line 60) | toBBCode(): string {
method toPlainText (line 65) | toPlainText(): string {
method toHtml (line 70) | toHtml(): string {
method toMarkdown (line 75) | toMarkdown(turndownService?: TurndownService): string {
method parseCustom (line 90) | parseCustom(blockHandler: CustomNodeHandler): string {
method parseWithConverter (line 95) | parseWithConverter(converter: BaseConverter): string {
method updateContext (line 99) | public updateContext(updates: Partial<ConversionContext>): void {
method findNodesByType (line 106) | public findNodesByType(type: string): TipTapNode[] {
method findCustomShortcutIds (line 127) | public findCustomShortcutIds(): Set<string> {
method findUsernames (line 144) | public findUsernames(): Set<string> {
method isEmptyNode (line 163) | private isEmptyNode(node: TipTapNode): boolean {
method trimEmptyEdgeNodes (line 183) | private trimEmptyEdgeNodes(nodes: TipTapNode[]): TipTapNode[] {
method withInsertions (line 200) | private withInsertions(): TipTapNode[] {
FILE: apps/client-server/src/app/post-parsers/models/description-node/description-node.base.ts
type ConversionContext (line 8) | interface ConversionContext {
FILE: apps/client-server/src/app/post-parsers/models/description-node/description-node.types.ts
type ITipTapBlockNode (line 13) | interface ITipTapBlockNode extends TipTapNode {
type ITipTapTextNode (line 21) | interface ITipTapTextNode extends TipTapNode {
function isTextNode (line 58) | function isTextNode(node: TipTapNode): node is ITipTapTextNode {
function isInlineShortcut (line 65) | function isInlineShortcut(node: TipTapNode): boolean {
function hasMark (line 72) | function hasMark(node: ITipTapTextNode, markType: string): boolean {
function getMarkAttrs (line 79) | function getMarkAttrs(
FILE: apps/client-server/src/app/post-parsers/parsers/content-warning-parser.ts
class ContentWarningParser (line 6) | class ContentWarningParser {
method parse (line 7) | public async parse(
FILE: apps/client-server/src/app/post-parsers/parsers/description-parser.service.spec.ts
function createWebsiteOptions (line 107) | function createWebsiteOptions(
class PlaintextBaseWebsiteOptions (line 133) | class PlaintextBaseWebsiteOptions extends BaseWebsiteOptions {
class MarkdownBaseWebsiteOptions (line 187) | class MarkdownBaseWebsiteOptions extends BaseWebsiteOptions {
class NoneBaseWebsiteOptions (line 215) | class NoneBaseWebsiteOptions extends BaseWebsiteOptions {
class PlaintextBaseWebsiteOptions (line 342) | class PlaintextBaseWebsiteOptions extends BaseWebsiteOptions {
class PlaintextBaseWebsiteOptions (line 572) | class PlaintextBaseWebsiteOptions extends BaseWebsiteOptions {
class PlaintextBaseWebsiteOptions (line 903) | class PlaintextBaseWebsiteOptions extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/post-parsers/parsers/description-parser.service.ts
class DescriptionParserService (line 24) | class DescriptionParserService {
method constructor (line 27) | constructor(
method parse (line 43) | public async parse(
method createDescription (line 146) | private createDescription(
method resolveCustomShortcutsFromTree (line 188) | private async resolveCustomShortcutsFromTree(
method resolveUsernamesFromTree (line 210) | private async resolveUsernamesFromTree(
method mergeBlocks (line 227) | public mergeBlocks(blocks: TipTapNode[]): TipTapNode[] {
method hasInlineContentType (line 234) | private hasInlineContentType(blocks: TipTapNode[], type: string): bool...
FILE: apps/client-server/src/app/post-parsers/parsers/rating-parser.ts
class RatingParser (line 5) | class RatingParser {
method parse (line 6) | public parse(
FILE: apps/client-server/src/app/post-parsers/parsers/tag-parser.service.spec.ts
class TagOptions (line 156) | class TagOptions extends BaseWebsiteOptions {
method processTag (line 175) | protected processTag(tag: string): string {
class TagOptions (line 171) | class TagOptions extends BaseWebsiteOptions {
method processTag (line 175) | protected processTag(tag: string): string {
class TagOptions (line 224) | class TagOptions extends BaseWebsiteOptions {
method processTag (line 175) | protected processTag(tag: string): string {
FILE: apps/client-server/src/app/post-parsers/parsers/tag-parser.service.ts
class TagParserService (line 8) | class TagParserService {
method constructor (line 9) | constructor(private readonly tagConvertersService: TagConvertersServic...
method parse (line 11) | public async parse(
FILE: apps/client-server/src/app/post-parsers/parsers/title-parser.spec.ts
class TestWebsiteOptions (line 63) | class TestWebsiteOptions extends BaseWebsiteOptions {
class TestDefaultWebsiteOptions (line 68) | class TestDefaultWebsiteOptions extends DefaultWebsiteOptions {
FILE: apps/client-server/src/app/post-parsers/parsers/title-parser.ts
class TitleParser (line 6) | class TitleParser {
method parse (line 7) | public async parse(
FILE: apps/client-server/src/app/post-parsers/post-parsers.module.ts
class PostParsersModule (line 28) | class PostParsersModule {}
FILE: apps/client-server/src/app/post-parsers/post-parsers.service.ts
class PostParsersService (line 17) | class PostParsersService {
method constructor (line 25) | constructor(
method parse (line 30) | public async parse(
FILE: apps/client-server/src/app/post/dtos/post-queue-action.dto.ts
class PostQueueActionDto (line 5) | class PostQueueActionDto implements IPostQueueActionDto {
FILE: apps/client-server/src/app/post/dtos/queue-post-record.dto.ts
class QueuePostRecordRequestDto (line 6) | class QueuePostRecordRequestDto implements IQueuePostRecordRequestDto {
FILE: apps/client-server/src/app/post/errors/invalid-post-chain.error.ts
class InvalidPostChainError (line 14) | class InvalidPostChainError extends Error {
method constructor (line 21) | constructor(
FILE: apps/client-server/src/app/post/models/cancellable-token.ts
class CancellableToken (line 7) | class CancellableToken {
method isCancelled (line 10) | public get isCancelled(): boolean {
method cancel (line 14) | public cancel(): void {
method throwIfCancelled (line 18) | public throwIfCancelled(): void {
FILE: apps/client-server/src/app/post/models/cancellation-error.ts
class CancellationError (line 5) | class CancellationError extends Error {
method constructor (line 6) | constructor(message = 'Task was cancelled.') {
FILE: apps/client-server/src/app/post/models/posting-file.ts
type ThumbnailOptions (line 11) | type ThumbnailOptions = Pick<
type FormDataFileFormat (line 16) | type FormDataFileFormat = {
class PostingFile (line 24) | class PostingFile {
method constructor (line 43) | public constructor(
method normalizeFileName (line 58) | private normalizeFileName(file: IFileBuffer): string {
method withMetadata (line 63) | public withMetadata(metadata: SubmissionFileMetadata): PostingFile {
method toPostFormat (line 68) | public toPostFormat(): FormFile {
method thumbnailToPostFormat (line 75) | public thumbnailToPostFormat(): FormFile | undefined {
FILE: apps/client-server/src/app/post/post.controller.ts
class PostController (line 13) | class PostController extends PostyBirbController<'PostRecordSchema'> {
method constructor (line 14) | constructor(readonly service: PostService) {
method getEvents (line 27) | async getEvents(@Param('id') id: EntityId) {
FILE: apps/client-server/src/app/post/post.module.ts
class PostModule (line 52) | class PostModule {}
FILE: apps/client-server/src/app/post/post.service.ts
class PostService (line 12) | class PostService extends PostyBirbService<'PostRecordSchema'> {
method constructor (line 17) | constructor(@Optional() webSocket?: WSGateway) {
method getEvents (line 27) | async getEvents(postRecordId: EntityId): Promise<PostEventDto[]> {
FILE: apps/client-server/src/app/post/services/post-file-resizer/post-file-resizer.service.spec.ts
function createFile (line 19) | function createFile(
FILE: apps/client-server/src/app/post/services/post-file-resizer/post-file-resizer.service.ts
type ResizeRequest (line 13) | type ResizeRequest = {
class PostFileResizerService (line 30) | class PostFileResizerService {
method constructor (line 33) | constructor(
method resize (line 37) | public async resize(request: ResizeRequest): Promise<PostingFile> {
method process (line 41) | private async process(request: ResizeRequest): Promise<PostingFile> {
method processPrimaryFile (line 63) | private async processPrimaryFile(
method processThumbnailFile (line 94) | private async processThumbnailFile(
FILE: apps/client-server/src/app/post/services/post-manager-v2/base-post-manager.service.ts
type WebsiteInfo (line 34) | interface WebsiteInfo {
method constructor (line 67) | constructor(
method cancelIfRunning (line 89) | public async cancelIfRunning(submissionId: EntityId): Promise<boolean> {
method isPosting (line 102) | public isPosting(): boolean {
method startPost (line 111) | public async startPost(
method postToWebsite (line 158) | protected async postToWebsite(
method emitPostAttemptStarted (line 302) | protected async emitPostAttemptStarted(
method handlePostFailure (line 337) | protected async handlePostFailure(
method preparePostData (line 395) | protected async preparePostData(
method getPostOrder (line 410) | protected getPostOrder(entity: PostRecord): WebsiteInfo[][] {
method finishPost (line 473) | protected async finishPost(entity: PostRecord): Promise<void> {
method waitForPostingWaitInterval (line 519) | protected async waitForPostingWaitInterval(
method ensureLoggedIn (line 553) | protected async ensureLoggedIn(instance: Website<unknown>): Promise<void> {
method redactPostDataForLogging (line 578) | protected redactPostDataForLogging(postData: PostData): string {
FILE: apps/client-server/src/app/post/services/post-manager-v2/file-submission-post-manager.service.spec.ts
function createFileBuffer (line 97) | function createFileBuffer(): FileBuffer {
function createSubmissionFile (line 111) | function createSubmissionFile(
function createSubmission (line 135) | function createSubmission(
function createPostRecord (line 156) | function createPostRecord(
function createMockFileWebsite (line 168) | function createMockFileWebsite(accountId: AccountId): UnknownWebsite {
function createPostingFile (line 193) | function createPostingFile(file: SubmissionFile): PostingFile {
FILE: apps/client-server/src/app/post/services/post-manager-v2/file-submission-post-manager.service.ts
function mimeTypeIsAccepted (line 42) | function mimeTypeIsAccepted(mimeType: string, patterns: string[]): boole...
class FileSubmissionPostManager (line 61) | class FileSubmissionPostManager extends BasePostManager {
method constructor (line 64) | constructor(
method getSupportedType (line 82) | getSupportedType(): SubmissionType {
method attemptToPost (line 86) | protected async attemptToPost(
method getFilesToPost (line 266) | private getFilesToPost(
method verifyPostingFiles (line 300) | private verifyPostingFiles(
method resizeOrModifyFile (line 325) | private async resizeOrModifyFile(
method getResizeParameters (line 420) | private getResizeParameters(
FILE: apps/client-server/src/app/post/services/post-manager-v2/message-submission-post-manager.service.spec.ts
function createSubmission (line 68) | function createSubmission(): Submission {
function createPostRecord (line 87) | function createPostRecord(submission: Submission): PostRecord {
function createMockWebsite (line 97) | function createMockWebsite(accountId: AccountId): UnknownWebsite {
FILE: apps/client-server/src/app/post/services/post-manager-v2/message-submission-post-manager.service.ts
class MessageSubmissionPostManager (line 25) | class MessageSubmissionPostManager extends BasePostManager {
method constructor (line 29) | constructor(
method getSupportedType (line 45) | getSupportedType(): SubmissionType {
method attemptToPost (line 49) | protected async attemptToPost(
FILE: apps/client-server/src/app/post/services/post-manager-v2/post-manager-registry.service.ts
class PostManagerRegistry (line 17) | class PostManagerRegistry {
method constructor (line 22) | constructor(
method getManager (line 37) | public getManager(type: SubmissionType): BasePostManager | undefined {
method startPost (line 46) | public async startPost(postRecord: PostRecord): Promise<void> {
method cancelIfRunning (line 78) | public async cancelIfRunning(submissionId: EntityId): Promise<boolean> {
method isPostingType (line 92) | public isPostingType(type: SubmissionType): boolean {
FILE: apps/client-server/src/app/post/services/post-manager/post-manager.controller.ts
class PostManagerController (line 9) | class PostManagerController {
method constructor (line 10) | constructor(
method cancelIfRunning (line 17) | async cancelIfRunning(@Param('id') id: SubmissionId) {
method isPosting (line 25) | async isPosting(@Param('submissionType') submissionType: SubmissionTyp...
FILE: apps/client-server/src/app/post/services/post-queue/post-queue.controller.ts
class PostQueueController (line 13) | class PostQueueController extends PostyBirbController<'PostQueueRecordSc...
method constructor (line 14) | constructor(readonly service: PostQueueService) {
method enqueue (line 20) | async enqueue(@Body() request: PostQueueActionDto) {
method dequeue (line 26) | async dequeue(@Body() request: PostQueueActionDto) {
method isPaused (line 32) | async isPaused() {
method pause (line 38) | async pause() {
method resume (line 45) | async resume() {
FILE: apps/client-server/src/app/post/services/post-queue/post-queue.service.spec.ts
function createSubmissionDto (line 82) | function createSubmissionDto(): CreateSubmissionDto {
function createAccountDto (line 89) | function createAccountDto(): CreateAccountDto {
function createWebsiteOptionsDto (line 96) | function createWebsiteOptionsDto(
FILE: apps/client-server/src/app/post/services/post-queue/post-queue.service.ts
class PostQueueService (line 36) | class PostQueueService
method constructor (line 59) | constructor(
method onModuleInit (line 79) | async onModuleInit() {
method resumeInterruptedPosts (line 127) | private async resumeInterruptedPosts(
method failOrphanedPostRecords (line 162) | private async failOrphanedPostRecords(): Promise<void> {
method isPaused (line 211) | public async isPaused(): Promise<boolean> {
method pause (line 216) | public async pause() {
method resume (line 224) | public async resume() {
method remove (line 232) | public override remove(id: EntityId) {
method getMostRecentTerminalPostRecord (line 241) | private async getMostRecentTerminalPostRecord(
method handleTerminalState (line 263) | private async handleTerminalState(record: PostRecord): Promise<void> {
method enqueue (line 344) | public async enqueue(
method dequeue (line 434) | public async dequeue(submissionIds: SubmissionId[]) {
method checkForScheduledSubmissions (line 461) | public async checkForScheduledSubmissions() {
method run (line 503) | public async run() {
method isStuck (line 523) | private isStuck(record: PostRecord): boolean {
method execute (line 545) | public async execute() {
method peek (line 643) | public async peek(): Promise<PostQueueRecord | undefined> {
FILE: apps/client-server/src/app/post/services/post-record-factory/post-event.repository.ts
class PostEventRepository (line 17) | class PostEventRepository {
method constructor (line 20) | constructor() {
method getSourceUrlsFromPost (line 33) | async getSourceUrlsFromPost(
method getFailedEvents (line 63) | async getFailedEvents(postRecordId: EntityId): Promise<PostEvent[]> {
method insert (line 82) | async insert(event: Insert<'PostEventSchema'>): Promise<PostEvent> {
FILE: apps/client-server/src/app/post/services/post-record-factory/post-record-factory.service.spec.ts
function createSubmission (line 59) | async function createSubmission(): Promise<EntityId> {
function createAccount (line 67) | async function createAccount(name: string): Promise<EntityId> {
function createPostRecordWithState (line 277) | async function createPostRecordWithState(
function addEvent (line 292) | async function addEvent(
function createPostRecordWithState (line 839) | async function createPostRecordWithState(
function addEvent (line 854) | async function addEvent(
FILE: apps/client-server/src/app/post/services/post-record-factory/post-record-factory.service.ts
type ResumeContext (line 20) | interface ResumeContext {
class PostRecordFactory (line 55) | class PostRecordFactory {
method constructor (line 60) | constructor(private readonly postEventRepository: PostEventRepository) {
method create (line 76) | async create(
method findMostRecentOrigin (line 128) | private async findMostRecentOrigin(
method findInProgressRecord (line 151) | private async findInProgressRecord(
method buildResumeContext (line 191) | async buildResumeContext(
method combineRecordsForAggregation (line 257) | private combineRecordsForAggregation(
method aggregateFromRecords (line 284) | private aggregateFromRecords(
method createEmptyContext (line 300) | private createEmptyContext(resumeMode: PostRecordResumeMode): ResumeCo...
method getRecordsToAggregate (line 321) | private async getRecordsToAggregate(
method aggregateSourceUrls (line 365) | private aggregateSourceUrls(
method aggregateCompletedAccounts (line 387) | private aggregateCompletedAccounts(
method aggregatePostedFiles (line 408) | private aggregatePostedFiles(
method isSourceUrlEvent (line 430) | private isSourceUrlEvent(event: PostEvent): boolean {
method addSourceUrl (line 440) | private addSourceUrl(
method addPostedFile (line 456) | private addPostedFile(
method shouldSkipAccount (line 479) | shouldSkipAccount(context: ResumeContext, accountId: AccountId): boole...
method shouldSkipFile (line 490) | shouldSkipFile(
method getSourceUrlsForAccount (line 518) | getSourceUrlsForAccount(
method getAllSourceUrls (line 530) | getAllSourceUrls(context: ResumeContext): string[] {
method getSourceUrlsExcludingAccount (line 545) | getSourceUrlsExcludingAccount(
FILE: apps/client-server/src/app/remote/models/update-cookies-remote.dto.ts
class UpdateCookiesRemoteDto (line 4) | class UpdateCookiesRemoteDto implements UpdateCookiesRemote {
FILE: apps/client-server/src/app/remote/remote.controller.ts
class RemoteController (line 6) | class RemoteController {
method constructor (line 7) | constructor(private readonly remoteService: RemoteService) {}
method ping (line 10) | ping(@Param('password') password: string) {
method setCookies (line 15) | setCookies(@Body() updateCookies: UpdateCookiesRemoteDto) {
FILE: apps/client-server/src/app/remote/remote.middleware.ts
class RemotePasswordMiddleware (line 10) | class RemotePasswordMiddleware implements NestMiddleware {
method constructor (line 11) | constructor(private readonly remoteService: RemoteService) {}
method use (line 13) | async use(req: Request, res: Response, next: NextFunction) {
FILE: apps/client-server/src/app/remote/remote.module.ts
class RemoteModule (line 12) | class RemoteModule {}
FILE: apps/client-server/src/app/remote/remote.service.ts
class RemoteService (line 8) | class RemoteService {
method validate (line 11) | async validate(password: string): Promise<boolean> {
method setCookies (line 32) | async setCookies(updateCookies: UpdateCookiesRemoteDto) {
method convertCookie (line 61) | private convertCookie(cookie: Electron.Cookie): Electron.CookiesSetDet...
FILE: apps/client-server/src/app/security-and-authentication/ssl.ts
class SSL (line 7) | class SSL {
method getOrCreateSSL (line 10) | static async getOrCreateSSL(): Promise<{ key: string; cert: string }> {
FILE: apps/client-server/src/app/settings/dtos/update-settings.dto.ts
class UpdateSettingsDto (line 8) | class UpdateSettingsDto implements IUpdateSettingsDto {
FILE: apps/client-server/src/app/settings/dtos/update-startup-settings.dto.ts
class UpdateStartupSettingsDto (line 5) | class UpdateStartupSettingsDto implements StartupOptions {
FILE: apps/client-server/src/app/settings/settings.controller.ts
class SettingsController (line 15) | class SettingsController extends PostyBirbController<'SettingsSchema'> {
method constructor (line 16) | constructor(readonly service: SettingsService) {
method update (line 23) | update(
method getStartupSettings (line 33) | getStartupSettings() {
method updateStartupSettings (line 38) | updateStartupSettings(@Body() startupOptions: UpdateStartupSettingsDto) {
FILE: apps/client-server/src/app/settings/settings.events.ts
type SettingsEventTypes (line 5) | type SettingsEventTypes = SettingsUpdateEvent;
class SettingsUpdateEvent (line 7) | class SettingsUpdateEvent implements WebsocketEvent<SettingsDto[]> {
FILE: apps/client-server/src/app/settings/settings.module.ts
class SettingsModule (line 10) | class SettingsModule {}
FILE: apps/client-server/src/app/settings/settings.service.ts
class SettingsService (line 21) | class SettingsService
method constructor (line 25) | constructor(@Optional() webSocket: WSGateway) {
method onModuleInit (line 35) | async onModuleInit() {
method create (line 105) | create(createDto: unknown): Promise<Settings> {
method createDefaultSettings (line 112) | private createDefaultSettings() {
method emit (line 129) | async emit() {
method getStartupSettings (line 139) | public getStartupSettings() {
method getDefaultSettings (line 146) | public getDefaultSettings() {
method updateStartupSettings (line 156) | public updateStartupSettings(startUpOptions: Partial<StartupOptions>) {
method update (line 181) | async update(id: EntityId, updateSettingsDto: UpdateSettingsDto) {
method testRemoteConnection (line 196) | async testRemoteConnection(
FILE: apps/client-server/src/app/submission/dtos/apply-multi-submission.dto.ts
class ApplyMultiSubmissionDto (line 5) | class ApplyMultiSubmissionDto implements IApplyMultiSubmissionDto {
FILE: apps/client-server/src/app/submission/dtos/apply-template-options.dto.ts
class ApplyTemplateOptionsDto (line 16) | class ApplyTemplateOptionsDto {
FILE: apps/client-server/src/app/submission/dtos/create-submission.dto.ts
function parseJsonField (line 22) | function parseJsonField<T>(value: unknown): T | undefined {
class CreateSubmissionDto (line 36) | class CreateSubmissionDto implements ICreateSubmissionDto {
FILE: apps/client-server/src/app/submission/dtos/reorder-submission-files.dto.ts
class ReorderSubmissionFilesDto (line 4) | class ReorderSubmissionFilesDto implements IReorderSubmissionFilesDto {
FILE: apps/client-server/src/app/submission/dtos/reorder-submission.dto.ts
class ReorderSubmissionDto (line 5) | class ReorderSubmissionDto {
FILE: apps/client-server/src/app/submission/dtos/template-option.dto.ts
class TemplateOptionDto (line 8) | class TemplateOptionDto {
FILE: apps/client-server/src/app/submission/dtos/update-alt-file.dto.ts
class UpdateAltFileDto (line 4) | class UpdateAltFileDto implements IUpdateAltFileDto {
FILE: apps/client-server/src/app/submission/dtos/update-submission-template-name.dto.ts
class UpdateSubmissionTemplateNameDto (line 5) | class UpdateSubmissionTemplateNameDto
FILE: apps/client-server/src/app/submission/dtos/update-submission.dto.ts
class UpdateSubmissionDto (line 19) | class UpdateSubmissionDto implements IUpdateSubmissionDto {
FILE: apps/client-server/src/app/submission/file-submission.controller.ts
type Target (line 32) | type Target = 'file' | 'thumbnail';
class FileSubmissionController (line 41) | class FileSubmissionController {
method constructor (line 42) | constructor(
method findOne (line 47) | private findOne(id: SubmissionId) {
method appendFile (line 56) | async appendFile(
method replaceFile (line 80) | async replaceFile(
method removeFile (line 103) | async removeFile(
method getAltFileText (line 122) | async getAltFileText(@Param('id') id: EntityId) {
method updateAltFileText (line 128) | async updateAltFileText(
method updateMetadata (line 137) | async updateMetadata(
method reorderFiles (line 146) | async reorderFiles(@Body() update: ReorderSubmissionFilesDto) {
FILE: apps/client-server/src/app/submission/services/file-submission.service.ts
class FileSubmissionService (line 36) | class FileSubmissionService
method constructor (line 40) | constructor(
method populate (line 52) | async populate(
method guardIsFileSubmission (line 65) | private guardIsFileSubmission(submission: ISubmission) {
method guardFileTypeCompatibility (line 87) | private guardFileTypeCompatibility(
method appendFile (line 118) | async appendFile(id: EntityId | FileSubmission, file: MulterFileInfo) {
method replaceFile (line 142) | async replaceFile(id: EntityId, fileId: EntityId, file: MulterFileInfo) {
method replaceThumbnail (line 158) | async replaceThumbnail(
method removeFile (line 177) | async removeFile(id: SubmissionId, fileId: EntityId) {
method getAltFileText (line 189) | getAltFileText(id: EntityId) {
method updateAltFileText (line 193) | updateAltFileText(id: EntityId, update: UpdateAltFileDto) {
method updateMetadata (line 197) | updateMetadata(id: EntityId, update: SubmissionFileMetadata) {
method reorderFiles (line 201) | reorderFiles(update: ReorderSubmissionFilesDto) {
FILE: apps/client-server/src/app/submission/services/message-submission.service.ts
class MessageSubmissionService (line 7) | class MessageSubmissionService
method populate (line 10) | async populate(
FILE: apps/client-server/src/app/submission/services/submission-service.interface.ts
type ISubmissionService (line 5) | interface ISubmissionService<
FILE: apps/client-server/src/app/submission/services/submission.service.spec.ts
function setup (line 97) | function setup(): string {
function createAccount (line 103) | async function createAccount() {
function createSubmissionDto (line 113) | function createSubmissionDto(): CreateSubmissionDto {
function createMulterData (line 120) | function createMulterData(path: string): MulterFileInfo {
FILE: apps/client-server/src/app/submission/services/submission.service.ts
type SubmissionEntity (line 49) | type SubmissionEntity = Submission<SubmissionMetadataType>;
class SubmissionService (line 56) | class SubmissionService
method constructor (line 62) | constructor(
method onModuleInit (line 108) | async onModuleInit() {
method normalizeOrders (line 121) | private async normalizeOrders() {
method cleanupUninitializedSubmissions (line 144) | private async cleanupUninitializedSubmissions() {
method emit (line 163) | public async emit() {
method performEmit (line 178) | private async performEmit() {
method findAllAsDto (line 187) | public async findAllAsDto(): Promise<ISubmissionDto<ISubmissionMetadat...
method findAll (line 229) | public async findAll() {
method populateMultiSubmission (line 234) | private async populateMultiSubmission(type: SubmissionType) {
method create (line 256) | async create(
method applyOverridingTemplate (line 394) | async applyOverridingTemplate(id: SubmissionId, templateId: Submission...
method update (line 458) | async update(id: SubmissionId, update: UpdateSubmissionDto) {
method remove (line 537) | public async remove(id: SubmissionId) {
method applyMultiSubmission (line 543) | async applyMultiSubmission(applyMultiSubmissionDto: ApplyMultiSubmissi...
method applyTemplateOptions (line 609) | async applyTemplateOptions(dto: ApplyTemplateOptionsDto): Promise<{
method duplicate (line 699) | public async duplicate(id: SubmissionId) {
method updateTemplateName (line 835) | async updateTemplateName(
method reorder (line 862) | async reorder(
method unarchive (line 915) | async unarchive(id: SubmissionId) {
method archive (line 926) | async archive(id: SubmissionId) {
FILE: apps/client-server/src/app/submission/submission.controller.ts
class SubmissionController (line 38) | class SubmissionController extends PostyBirbController<'SubmissionSchema...
method constructor (line 39) | constructor(readonly service: SubmissionService) {
method findAll (line 44) | async findAll(): Promise<ISubmissionDto[]> {
method create (line 53) | async create(
method duplicate (line 86) | async duplicate(@Param('id') id: SubmissionId) {
method unarchive (line 93) | async unarchive(@Param('id') id: SubmissionId) {
method archive (line 100) | async archive(@Param('id') id: SubmissionId) {
method reorder (line 109) | async reorder(@Body() reorderDto: ReorderSubmissionDto) {
method update (line 120) | async update(
method updateTemplateName (line 132) | async updateTemplateName(
method applyMulti (line 144) | async applyMulti(@Body() applyMultiSubmissionDto: ApplyMultiSubmission...
method applyTemplateOptions (line 151) | async applyTemplateOptions(
method applyTemplate (line 160) | async applyTemplate(
FILE: apps/client-server/src/app/submission/submission.events.ts
type SubmissionEventTypes (line 5) | type SubmissionEventTypes = SubmissionUpdateEvent;
class SubmissionUpdateEvent (line 7) | class SubmissionUpdateEvent
FILE: apps/client-server/src/app/submission/submission.module.ts
method destination (line 28) | destination(req, file, cb) {
method filename (line 31) | filename(req, file, cb) {
class SubmissionModule (line 45) | class SubmissionModule {}
FILE: apps/client-server/src/app/tag-converters/dtos/create-tag-converter.dto.ts
class CreateTagConverterDto (line 5) | class CreateTagConverterDto implements ICreateTagConverterDto {
FILE: apps/client-server/src/app/tag-converters/dtos/update-tag-converter.dto.ts
class UpdateTagConverterDto (line 5) | class UpdateTagConverterDto implements IUpdateTagConverterDto {
FILE: apps/client-server/src/app/tag-converters/tag-converter.events.ts
type TagConverterEventTypes (line 5) | type TagConverterEventTypes = TagConverterUpdateEvent;
class TagConverterUpdateEvent (line 7) | class TagConverterUpdateEvent implements WebsocketEvent<TagConverterDto[...
FILE: apps/client-server/src/app/tag-converters/tag-converters.controller.ts
class TagConvertersController (line 20) | class TagConvertersController extends PostyBirbController<'TagConverterS...
method constructor (line 21) | constructor(readonly service: TagConvertersService) {
method create (line 28) | create(@Body() createTagConverterDto: CreateTagConverterDto) {
method update (line 37) | update(@Body() updateDto: UpdateTagConverterDto, @Param('id') id: Enti...
FILE: apps/client-server/src/app/tag-converters/tag-converters.module.ts
class TagConvertersModule (line 10) | class TagConvertersModule {}
FILE: apps/client-server/src/app/tag-converters/tag-converters.service.spec.ts
function createTagConverterDto (line 12) | function createTagConverterDto(
FILE: apps/client-server/src/app/tag-converters/tag-converters.service.ts
class TagConvertersService (line 13) | class TagConvertersService extends PostyBirbService<'TagConverterSchema'> {
method constructor (line 14) | constructor(@Optional() webSocket?: WSGateway) {
method create (line 21) | async create(createDto: CreateTagConverterDto): Promise<TagConverter> {
method update (line 29) | update(id: EntityId, update: UpdateTagConverterDto) {
method convert (line 43) | async convert(
method emit (line 68) | protected async emit() {
FILE: apps/client-server/src/app/tag-groups/dtos/create-tag-group.dto.ts
class CreateTagGroupDto (line 5) | class CreateTagGroupDto implements ICreateTagGroupDto {
FILE: apps/client-server/src/app/tag-groups/dtos/update-tag-group.dto.ts
class UpdateTagGroupDto (line 5) | class UpdateTagGroupDto implements IUpdateTagGroupDto {
FILE: apps/client-server/src/app/tag-groups/tag-group.events.ts
type TagGroupEventTypes (line 5) | type TagGroupEventTypes = TagGroupUpdateEvent;
class TagGroupUpdateEvent (line 7) | class TagGroupUpdateEvent implements WebsocketEvent<TagGroupDto[]> {
FILE: apps/client-server/src/app/tag-groups/tag-groups.controller.ts
class TagGroupsController (line 20) | class TagGroupsController extends PostyBirbController<'TagGroupSchema'> {
method constructor (line 21) | constructor(readonly service: TagGroupsService) {
method create (line 28) | create(@Body() createDto: CreateTagGroupDto) {
method update (line 35) | update(@Param('id') id: EntityId, @Body() updateDto: UpdateTagGroupDto) {
FILE: apps/client-server/src/app/tag-groups/tag-groups.module.ts
class TagGroupsModule (line 9) | class TagGroupsModule {}
FILE: apps/client-server/src/app/tag-groups/tag-groups.service.spec.ts
function createTagGroupDto (line 11) | function createTagGroupDto(name: string, tags: string[]) {
FILE: apps/client-server/src/app/tag-groups/tag-groups.service.ts
class TagGroupsService (line 12) | class TagGroupsService extends PostyBirbService<'TagGroupSchema'> {
method constructor (line 13) | constructor(@Optional() webSocket?: WSGateway) {
method create (line 18) | async create(createDto: CreateTagGroupDto): Promise<TagGroup> {
method update (line 26) | update(id: EntityId, update: UpdateTagGroupDto) {
method emit (line 31) | protected async emit() {
FILE: apps/client-server/src/app/update/update.controller.ts
class UpdateController (line 6) | class UpdateController {
method constructor (line 7) | constructor(private readonly service: UpdateService) {}
method checkForUpdates (line 15) | checkForUpdates(): UpdateState {
method update (line 20) | update() {
method install (line 28) | install() {
FILE: apps/client-server/src/app/update/update.events.ts
type UpdateEventTypes (line 5) | type UpdateEventTypes = UpdateUpdateEvent;
class UpdateUpdateEvent (line 7) | class UpdateUpdateEvent implements WebsocketEvent<UpdateState> {
FILE: apps/client-server/src/app/update/update.module.ts
class UpdateModule (line 11) | class UpdateModule {}
FILE: apps/client-server/src/app/update/update.service.ts
class UpdateService (line 15) | class UpdateService {
method constructor (line 27) | constructor(@Optional() private readonly webSocket?: WSGateway) {
method emit (line 40) | private emit() {
method registerListeners (line 49) | private registerListeners() {
method onUpdateDownloaded (line 64) | private onUpdateDownloaded() {
method onUpdateAvailable (line 74) | private onUpdateAvailable(update: UpdateInfo) {
method onUpdateError (line 83) | private onUpdateError(error: Error) {
method onDownloadProgress (line 93) | private onDownloadProgress(progress: ProgressInfo) {
method checkForUpdates (line 101) | public checkForUpdates() {
method getUpdateState (line 112) | public getUpdateState() {
method update (line 116) | public update() {
method install (line 138) | public install() {
FILE: apps/client-server/src/app/user-converters/dtos/create-user-converter.dto.ts
class CreateUserConverterDto (line 5) | class CreateUserConverterDto implements ICreateUserConverterDto {
FILE: apps/client-server/src/app/user-converters/dtos/update-user-converter.dto.ts
class UpdateUserConverterDto (line 5) | class UpdateUserConverterDto
FILE: apps/client-server/src/app/user-converters/user-converter.events.ts
type UserConverterEventTypes (line 5) | type UserConverterEventTypes = UserConverterUpdateEvent;
class UserConverterUpdateEvent (line 7) | class UserConverterUpdateEvent implements WebsocketEvent<UserConverterDt...
FILE: apps/client-server/src/app/user-converters/user-converters.controller.ts
class UserConvertersController (line 20) | class UserConvertersController extends PostyBirbController<'UserConverte...
method constructor (line 21) | constructor(readonly service: UserConvertersService) {
method create (line 28) | create(@Body() createUserConverterDto: CreateUserConverterDto) {
method update (line 37) | update(@Body() updateDto: UpdateUserConverterDto, @Param('id') id: Ent...
FILE: apps/client-server/src/app/user-converters/user-converters.module.ts
class UserConvertersModule (line 10) | class UserConvertersModule {}
FILE: apps/client-server/src/app/user-converters/user-converters.service.spec.ts
function createUserConverterDto (line 12) | function createUserConverterDto(
FILE: apps/client-server/src/app/user-converters/user-converters.service.ts
class UserConvertersService (line 13) | class UserConvertersService extends PostyBirbService<'UserConverterSchem...
method constructor (line 14) | constructor(@Optional() webSocket?: WSGateway) {
method create (line 21) | async create(createDto: CreateUserConverterDto): Promise<UserConverter> {
method update (line 29) | update(id: EntityId, update: UpdateUserConverterDto) {
method convert (line 41) | async convert(instance: Website<unknown>, username: string): Promise<s...
method emit (line 57) | protected async emit() {
FILE: apps/client-server/src/app/user-specified-website-options/dtos/create-user-specified-website-options.dto.ts
class CreateUserSpecifiedWebsiteOptionsDto (line 10) | class CreateUserSpecifiedWebsiteOptionsDto
FILE: apps/client-server/src/app/user-specified-website-options/dtos/update-user-specified-website-options.dto.ts
class UpdateUserSpecifiedWebsiteOptionsDto (line 9) | class UpdateUserSpecifiedWebsiteOptionsDto
FILE: apps/client-server/src/app/user-specified-website-options/user-specified-website-options.controller.ts
class UserSpecifiedWebsiteOptionsController (line 20) | class UserSpecifiedWebsiteOptionsController extends PostyBirbController<...
method constructor (line 21) | constructor(readonly service: UserSpecifiedWebsiteOptionsService) {
method create (line 28) | async create(@Body() createDto: CreateUserSpecifiedWebsiteOptionsDto) {
method update (line 36) | update(
FILE: apps/client-server/src/app/user-specified-website-options/user-specified-website-options.module.ts
class UserSpecifiedWebsiteOptionsModule (line 10) | class UserSpecifiedWebsiteOptionsModule {}
FILE: apps/client-server/src/app/user-specified-website-options/user-specified-website-options.service.ts
class UserSpecifiedWebsiteOptionsService (line 10) | class UserSpecifiedWebsiteOptionsService extends PostyBirbService<'UserS...
method constructor (line 11) | constructor() {
method create (line 15) | async create(
method update (line 33) | update(id: EntityId, update: UpdateUserSpecifiedWebsiteOptionsDto) {
method upsert (line 45) | async upsert(
method findByAccountAndSubmissionType (line 65) | public findByAccountAndSubmissionType(
FILE: apps/client-server/src/app/utils/blocknote-to-tiptap.ts
type BNTextNode (line 20) | interface BNTextNode {
type BNLinkNode (line 26) | interface BNLinkNode {
type BNInlineNode (line 32) | interface BNInlineNode {
type BNInlineContent (line 37) | type BNInlineContent = BNTextNode | BNLinkNode | BNInlineNode;
type BNBlock (line 39) | interface BNBlock {
constant DEFAULT_PROP_VALUES (line 48) | const DEFAULT_PROP_VALUES: Record<string, any> = {
function isBlockNoteFormat (line 60) | function isBlockNoteFormat(desc: unknown): desc is BNBlock[] {
function convertStyles (line 69) | function convertStyles(styles: Record<string, any>): TipTapMark[] {
function convertInlineContent (line 114) | function convertInlineContent(
function convertBlock (line 169) | function convertBlock(block: BNBlock): TipTapNode[] {
function extractBlockAttrs (line 354) | function extractBlockAttrs(
function mergeAdjacentLists (line 381) | function mergeAdjacentLists(nodes: TipTapNode[]): TipTapNode[] {
function convertBlockNoteToTipTap (line 403) | function convertBlockNoteToTipTap(blocks: BNBlock[]): Description {
function migrateDescription (line 421) | function migrateDescription(desc: unknown): Description {
FILE: apps/client-server/src/app/utils/coerce.util.ts
class Coerce (line 1) | class Coerce {
method boolean (line 2) | static boolean(value: string | boolean): boolean {
FILE: apps/client-server/src/app/utils/filesize.util.ts
class FileSize (line 5) | class FileSize {
method megabytes (line 6) | static megabytes(size: number): number {
method bytesToMB (line 10) | static bytesToMB(size: number): number {
FILE: apps/client-server/src/app/utils/html-parser.util.ts
class HtmlParserUtil (line 3) | class HtmlParserUtil {
method getInputValue (line 4) | public static getInputValue(html: string, name: string, index = 0): st...
FILE: apps/client-server/src/app/utils/select-option.util.ts
class SelectOptionUtil (line 3) | class SelectOptionUtil {
method findOptionById (line 4) | static findOptionById(
FILE: apps/client-server/src/app/utils/wait.util.ts
function wait (line 3) | function wait(milliseconds: number): Promise<void> {
function waitUntil (line 9) | async function waitUntil(
function waitUntilPromised (line 27) | async function waitUntilPromised(
FILE: apps/client-server/src/app/validation/validation.module.ts
class ValidationModule (line 13) | class ValidationModule {}
FILE: apps/client-server/src/app/validation/validation.service.ts
type ValidationCacheRecord (line 30) | type ValidationCacheRecord = {
class ValidationService (line 42) | class ValidationService {
method constructor (line 52) | constructor(
method getCachedValidation (line 59) | private getCachedValidation(
method clearCachedValidation (line 65) | private clearCachedValidation(submissionId: SubmissionId) {
method setCachedValidation (line 69) | private setCachedValidation(
method validateSubmission (line 99) | public async validateSubmission(
method isStale (line 117) | private isStale(submission: Submission): boolean {
method validate (line 143) | public async validate(
method validateWebsiteInstance (line 243) | private async validateWebsiteInstance(
FILE: apps/client-server/src/app/validation/validators/common-field-validators.ts
function validateRequiredTextField (line 6) | async function validateRequiredTextField({
function validateRequiredSelectField (line 45) | async function validateRequiredSelectField({
function validateRequiredRadioField (line 79) | async function validateRequiredRadioField({
function validateRequiredBooleanField (line 108) | async function validateRequiredBooleanField({
function validateRequiredDescriptionField (line 137) | async function validateRequiredDescriptionField({
FILE: apps/client-server/src/app/validation/validators/datetime-field-validators.ts
function validateDateTimeFormat (line 6) | async function validateDateTimeFormat({
function validateDateTimeMinimum (line 42) | async function validateDateTimeMinimum({
function validateDateTimeMaximum (line 86) | async function validateDateTimeMaximum({
function validateDateTimeRange (line 131) | async function validateDateTimeRange({
FILE: apps/client-server/src/app/validation/validators/description-validators.ts
function validateDescriptionMaxLength (line 5) | async function validateDescriptionMaxLength({
function validateDescriptionMinLength (line 32) | async function validateDescriptionMinLength({
function validateTagsPresence (line 59) | async function validateTagsPresence({
function validateTitlePresence (line 95) | async function validateTitlePresence({
FILE: apps/client-server/src/app/validation/validators/file-submission-validators.ts
function isFileHandlingWebsite (line 20) | function isFileHandlingWebsite(
function isFileSubmission (line 26) | function isFileSubmission(
function isFileFiltered (line 32) | function isFileFiltered(
function validateTextFileRequiresFallback (line 43) | async function validateTextFileRequiresFallback({
function validateNotAllFilesIgnored (line 90) | async function validateNotAllFilesIgnored({
function validateAcceptedFiles (line 115) | async function validateAcceptedFiles({
function validateFileBatchSize (line 186) | async function validateFileBatchSize({
function validateFileSize (line 217) | async function validateFileSize({
function validateImageFileDimensions (line 256) | async function validateImageFileDimensions({
FILE: apps/client-server/src/app/validation/validators/select-field-validators.ts
function isSelectField (line 8) | function isSelectField(field: FieldAggregateType): field is SelectFieldT...
function validateSelectFieldMinSelected (line 12) | async function validateSelectFieldMinSelected({
function validateSelectFieldValidOptions (line 42) | async function validateSelectFieldValidOptions({
function flattenSelectOptions (line 116) | function flattenSelectOptions(options: SelectOption[]): string[] {
FILE: apps/client-server/src/app/validation/validators/tag-validators.ts
function validateMaxTags (line 3) | async function validateMaxTags({
function validateMinTags (line 23) | async function validateMinTags({
function validateMaxTagLength (line 43) | async function validateMaxTagLength({
function validateTagHashtag (line 64) | async function validateTagHashtag({
FILE: apps/client-server/src/app/validation/validators/title-validators.ts
function validateTitleMaxLength (line 3) | async function validateTitleMaxLength({
function validateTitleMinLength (line 23) | async function validateTitleMinLength({
FILE: apps/client-server/src/app/validation/validators/validator.type.ts
type ValidatorParams (line 13) | type ValidatorParams = {
type Validator (line 24) | type Validator = (props: ValidatorParams) => Promise<void>;
class FieldValidator (line 26) | class FieldValidator extends SubmissionValidator {
method constructor (line 27) | constructor(
FILE: apps/client-server/src/app/web-socket/models/web-socket-event.ts
method constructor (line 6) | constructor(data: D) {
FILE: apps/client-server/src/app/web-socket/web-socket-adapter.ts
class WebSocketAdapter (line 4) | class WebSocketAdapter extends IoAdapter {
method createIOServer (line 5) | createIOServer(
FILE: apps/client-server/src/app/web-socket/web-socket-gateway.ts
class WSGateway (line 11) | class WSGateway implements OnGatewayInit {
method afterInit (line 15) | afterInit(server: Server) {
method emit (line 25) | public emit(socketEvent: WebSocketEvents) {
FILE: apps/client-server/src/app/web-socket/web-socket.events.ts
type WebSocketEvents (line 13) | type WebSocketEvents =
FILE: apps/client-server/src/app/web-socket/web-socket.module.ts
class WebSocketModule (line 9) | class WebSocketModule {}
FILE: apps/client-server/src/app/website-options/dtos/create-website-options.dto.ts
class CreateWebsiteOptionsDto (line 10) | class CreateWebsiteOptionsDto implements ICreateWebsiteOptionsDto {
FILE: apps/client-server/src/app/website-options/dtos/preview-description.dto.ts
class PreviewDescriptionDto (line 9) | class PreviewDescriptionDto implements IPreviewDescriptionDto {
FILE: apps/client-server/src/app/website-options/dtos/update-submission-website-options.dto.ts
class UpdateSubmissionWebsiteOptionsDto (line 9) | class UpdateSubmissionWebsiteOptionsDto
FILE: apps/client-server/src/app/website-options/dtos/update-website-options.dto.ts
class UpdateWebsiteOptionsDto (line 5) | class UpdateWebsiteOptionsDto implements IUpdateWebsiteOptionsDto {
FILE: apps/client-server/src/app/website-options/dtos/validate-website-options.dto.ts
class ValidateWebsiteOptionsDto (line 9) | class ValidateWebsiteOptionsDto implements IValidateWebsiteOptionsDto {
FILE: apps/client-server/src/app/website-options/website-options.controller.ts
class WebsiteOptionsController (line 24) | class WebsiteOptionsController extends PostyBirbController<'WebsiteOptio...
method constructor (line 25) | constructor(readonly service: WebsiteOptionsService) {
method create (line 37) | create(
method update (line 47) | update(
method updateSubmission (line 58) | updateSubmission(
method validate (line 72) | validate(@Body() validateOptionsDto: ValidateWebsiteOptionsDto) {
method validateSubmission (line 80) | validateSubmission(@Param('submissionId') submissionId: SubmissionId) {
method previewDescription (line 88) | previewDescription(@Body() dto: PreviewDescriptionDto) {
FILE: apps/client-server/src/app/website-options/website-options.module.ts
class WebsiteOptionsModule (line 26) | class WebsiteOptionsModule {}
FILE: apps/client-server/src/app/website-options/website-options.service.spec.ts
function createAccount (line 39) | async function createAccount() {
function createSubmission (line 49) | async function createSubmission() {
FILE: apps/client-server/src/app/website-options/website-options.service.ts
class WebsiteOptionsService (line 50) | class WebsiteOptionsService
method constructor (line 58) | constructor(
method onModuleInit (line 89) | async onModuleInit() {
method migrateBlockNoteDescriptions (line 98) | private async migrateBlockNoteDescriptions() {
method createOption (line 180) | async createOption(
method createOptionInsertObject (line 203) | async createOptionInsertObject(
method create (line 270) | async create(createDto: CreateWebsiteOptionsDto) {
method update (line 335) | async update(id: EntityId, update: UpdateWebsiteOptionsDto) {
method createDefaultSubmissionOptions (line 351) | async createDefaultSubmissionOptions(
method populateDefaultWebsiteOptions (line 375) | private async populateDefaultWebsiteOptions(
method validateWebsiteOption (line 419) | async validateWebsiteOption(
method validateSubmission (line 439) | async validateSubmission(
method previewDescription (line 456) | async previewDescription(
method updateSubmissionOptions (line 505) | async updateSubmissionOptions(
method onCustomShortcutDelete (line 542) | private async onCustomShortcutDelete(id: EntityId) {
method filterCustomShortcut (line 579) | public filterCustomShortcut(
FILE: apps/client-server/src/app/websites/commons/post-builder.spec.ts
function createPostingFile (line 26) | function createPostingFile(overrides = {}) {
FILE: apps/client-server/src/app/websites/commons/post-builder.ts
type FieldValue (line 16) | type FieldValue = string | number | boolean | null | undefined | object;
type Value (line 21) | type Value = FieldValue | FieldValue[];
class PostBuilder (line 38) | class PostBuilder {
method constructor (line 87) | constructor(
method withHeader (line 105) | withHeader(key: string, value: string) {
method withHeaders (line 125) | withHeaders(headers: Record<string, string>) {
method asMultipart (line 143) | asMultipart() {
method asJson (line 154) | asJson() {
method asUrlEncoded (line 165) | asUrlEncoded(skipIndex = false) {
method asRawData (line 183) | asRawData() {
method withData (line 204) | withData(data: Record<string, Value>) {
method getField (line 209) | getField<T>(key: string): T | undefined {
method removeField (line 213) | removeField(key: string) {
method setField (line 235) | setField(key: string, value: Value) {
method setConditional (line 256) | setConditional(
method forEach (line 280) | forEach<T>(
method addFile (line 304) | addFile(key: string, file: PostingFile | FormFile) {
method addFiles (line 322) | addFiles(key: string, files: PostingFile[]) {
method addThumbnail (line 343) | addThumbnail(key: string, file: PostingFile) {
method whenTrue (line 370) | whenTrue(predicate: boolean, callback: (builder: PostBuilder) => void) {
method send (line 397) | async send<ReturnValue>(url: string) {
method build (line 485) | public build(): Record<string, Value> {
method convert (line 525) | private convert(value: FieldValue | PostingFile): FieldValue | FormFile {
method insert (line 540) | private insert(
method getSanitizedData (line 555) | public getSanitizedData(): Record<string, Value> {
method sanitizeDataForLogging (line 559) | private sanitizeDataForLogging(
FILE: apps/client-server/src/app/websites/commons/validator-passthru.ts
function validatorPassthru (line 3) | async function validatorPassthru(): Promise<
FILE: apps/client-server/src/app/websites/commons/validator.ts
type KeysToOmit (line 8) | type KeysToOmit =
type ValidationArray (line 14) | type ValidationArray<Fields extends IWebsiteFormFields> = ValidationMess...
class SubmissionValidator (line 19) | class SubmissionValidator<Fields extends IWebsiteFormFields = never> {
method error (line 31) | error<T extends keyof ValidationMessages>(
method warning (line 46) | warning<T extends keyof ValidationMessages>(
method result (line 57) | get result(): SimpleValidationResult<Fields> {
FILE: apps/client-server/src/app/websites/decorators/disable-ads.decorator.ts
function DisableAds (line 3) | function DisableAds() {
FILE: apps/client-server/src/app/websites/decorators/login-flow.decorator.ts
function UserLoginFlow (line 11) | function UserLoginFlow(url: string) {
function CustomLoginFlow (line 28) | function CustomLoginFlow(loginComponentName?: string) {
FILE: apps/client-server/src/app/websites/decorators/supports-files.decorator.ts
function SupportsFiles (line 16) | function SupportsFiles(
function getSupportedFileSize (line 52) | function getSupportedFileSize(
FILE: apps/client-server/src/app/websites/decorators/supports-username-shortcut.decorator.ts
function SupportsUsernameShortcut (line 10) | function SupportsUsernameShortcut(usernameShortcut: UsernameShortcut) {
FILE: apps/client-server/src/app/websites/decorators/website-decorator-props.ts
type WebsiteDecoratorProps (line 12) | type WebsiteDecoratorProps = {
function defaultWebsiteDecoratorProps (line 58) | function defaultWebsiteDecoratorProps(): WebsiteDecoratorProps {
function injectWebsiteDecoratorProps (line 73) | function injectWebsiteDecoratorProps(
function validateWebsiteDecoratorProps (line 91) | function validateWebsiteDecoratorProps(
FILE: apps/client-server/src/app/websites/decorators/website-metadata.decorator.ts
function WebsiteMetadata (line 6) | function WebsiteMetadata(metadata: IWebsiteMetadata) {
FILE: apps/client-server/src/app/websites/dtos/oauth-website-request.dto.ts
class OAuthWebsiteRequestDto (line 5) | class OAuthWebsiteRequestDto<T extends DynamicObject>
FILE: apps/client-server/src/app/websites/implementations/artconomy/artconomy.website.ts
class Artconomy (line 52) | class Artconomy
method onLogin (line 63) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 96) | createFileModel(): ArtconomyFileSubmission {
method calculateImageResize (line 100) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 104) | async onPostFileSubmission(
method createMessageModel (line 196) | createMessageModel(): ArtconomyMessageSubmission {
method onPostMessageSubmission (line 200) | async onPostMessageSubmission(
method getRating (line 243) | private getRating(rating: SubmissionRating): number {
FILE: apps/client-server/src/app/websites/implementations/artconomy/models/artconomy-account-data.ts
type ArtconomyAccountData (line 3) | type ArtconomyAccountData = {
FILE: apps/client-server/src/app/websites/implementations/artconomy/models/artconomy-file-submission.ts
class ArtconomyFileSubmission (line 15) | class ArtconomyFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/artconomy/models/artconomy-message-submission.ts
class ArtconomyMessageSubmission (line 5) | class ArtconomyMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/aryion/aryion.website.ts
class Aryion (line 58) | class Aryion
method onLogin (line 69) | public async onLogin(): Promise<ILoginState> {
method getFolders (line 90) | private async getFolders($: HTMLElement): Promise<void> {
method parseFolderItem (line 108) | private parseFolderItem(li: HTMLElement, parent: SelectOption[]): void {
method createFileModel (line 146) | createFileModel(): AryionFileSubmission {
method calculateImageResize (line 150) | calculateImageResize(): ImageResizeProps {
method onPostFileSubmission (line 154) | async onPostFileSubmission(
method onValidateFileSubmission (line 226) | async onValidateFileSubmission(
FILE: apps/client-server/src/app/websites/implementations/aryion/models/aryion-account-data.ts
type AryionAccountData (line 3) | type AryionAccountData = {
FILE: apps/client-server/src/app/websites/implementations/aryion/models/aryion-file-submission.ts
class AryionFileSubmission (line 14) | class AryionFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/bluesky/bluesky.website.ts
class Bluesky (line 85) | class Bluesky
method getLoggedInAgent (line 110) | private getLoggedInAgent(): AtpAgent {
method onLogin (line 115) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 135) | createFileModel(): BlueskyFileSubmission {
method calculateImageResize (line 139) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method getDescriptionConverter (line 147) | getDescriptionConverter(): BaseConverter {
method onPostFileSubmission (line 151) | async onPostFileSubmission(
method onPostMessageSubmission (line 168) | async onPostMessageSubmission(
method post (line 182) | private async post(
method createPostResponse (line 213) | private createPostResponse(
method createMessageModel (line 246) | createMessageModel(): BlueskyMessageSubmission {
method onValidateFileSubmission (line 250) | async onValidateFileSubmission(
method onValidateMessageSubmission (line 284) | async onValidateMessageSubmission(
method validateRating (line 296) | private validateRating(
method validateDescription (line 321) | private async validateDescription(
method validateReplyToUrl (line 340) | private validateReplyToUrl(
method getReplyRef (line 356) | private async getReplyRef(
method getPostIdFromUrl (line 375) | private getPostIdFromUrl(url: string): { repo: string; rkey: string } ...
method countFileTypes (line 388) | private countFileTypes(files: (PostingFile | ISubmissionFile)[]): {
method createThreadgate (line 414) | private createThreadgate(
method uploadEmbeds (line 446) | private async uploadEmbeds(
method uploadImage (line 491) | private async uploadImage(
method checkVideoUploadLimits (line 519) | private async checkVideoUploadLimits(agent: AtpAgent): Promise<void> {
method uploadVideo (line 549) | private async uploadVideo(
method generateVideoName (line 587) | private generateVideoName(): string {
method waitForVideoProcessing (line 597) | private async waitForVideoProcessing(jobId: string): Promise<BlobRef> {
method checkFetchResult (line 638) | private async checkFetchResult<T>(
method getAuthToken (line 661) | private async getAuthToken(
method tryGetErrorBody (line 685) | private async tryGetErrorBody(res: Response): Promise<string> {
method getRichText (line 694) | async getRichText(description: string) {
class BlueskyConverter (line 768) | class BlueskyConverter extends PlainTextConverter {
method convertTextNode (line 775) | convertTextNode(node: TipTapNode, context: ConversionContext): string {
method convertBlocks (line 794) | convertBlocks(nodes: TipTapNode[], context: ConversionContext): string {
type RichTextLinkPosition (line 819) | interface RichTextLinkPosition {
type DescriptionWithLinks (line 825) | interface DescriptionWithLinks {
FILE: apps/client-server/src/app/websites/implementations/bluesky/models/bluesky-file-submission.ts
class BlueskyFileSubmission (line 15) | class BlueskyFileSubmission extends BaseWebsiteOptions {
method processTag (line 29) | override processTag(tag: string) {
FILE: apps/client-server/src/app/websites/implementations/bluesky/models/bluesky-message-submission.ts
class BlueskyMessageSubmission (line 3) | class BlueskyMessageSubmission extends BlueskyFileSubmission {}
FILE: apps/client-server/src/app/websites/implementations/cara/cara.website.ts
type CaraPostResult (line 31) | type CaraPostResult = {
type S3UploadCredentials (line 62) | type S3UploadCredentials = {
type S3UploadRequest (line 75) | type S3UploadRequest = {
type CaraMediaItem (line 84) | type CaraMediaItem = {
type CaraUploadResult (line 99) | type CaraUploadResult = CaraMediaItem[];
class Cara (line 141) | class Cara
method onLogin (line 152) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 171) | createFileModel(): CaraFileSubmission {
method calculateImageResize (line 175) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method getS3UploadCredentials (line 182) | private async getS3UploadCredentials(
method uploadToS3 (line 206) | private async uploadToS3(
method uploadImage (line 240) | private async uploadImage(
method onPostFileSubmission (line 328) | async onPostFileSubmission(
method onValidateFileSubmission (line 400) | async onValidateFileSubmission(
method createMessageModel (line 415) | createMessageModel(): CaraMessageSubmission {
method onPostMessageSubmission (line 419) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 458) | async onValidateMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/cara/models/cara-account-data.ts
type CaraAccountData (line 2) | type CaraAccountData = {};
FILE: apps/client-server/src/app/websites/implementations/cara/models/cara-file-submission.ts
class CaraFileSubmission (line 14) | class CaraFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/cara/models/cara-message-submission.ts
class CaraMessageSubmission (line 10) | class CaraMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/custom/custom.website.ts
class Custom (line 35) | class Custom
method onLogin (line 60) | public async onLogin(): Promise<ILoginState> {
method onWebsiteDataChange (line 74) | async onWebsiteDataChange(newData: CustomAccountData): Promise<void> {
method createFileModel (line 82) | createFileModel(): CustomFileSubmission {
method calculateImageResize (line 86) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 90) | async onPostFileSubmission(
method createMessageModel (line 187) | createMessageModel(): CustomMessageSubmission {
method onPostMessageSubmission (line 191) | async onPostMessageSubmission(
method getRuntimeParser (line 250) | getRuntimeParser(): DescriptionType {
FILE: apps/client-server/src/app/websites/implementations/custom/models/custom-file-submission.ts
class CustomFileSubmission (line 5) | class CustomFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/custom/models/custom-message-submission.ts
class CustomMessageSubmission (line 5) | class CustomMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/default/default.website.ts
class DefaultWebsite (line 22) | class DefaultWebsite
method createMessageModel (line 28) | createMessageModel(): BaseWebsiteOptions {
method createFileModel (line 32) | createFileModel(): BaseWebsiteOptions {
method onPostMessageSubmission (line 36) | onPostMessageSubmission(
method onValidateMessageSubmission (line 43) | async onValidateMessageSubmission(
method calculateImageResize (line 49) | calculateImageResize(file: ISubmissionFile): ImageResizeProps | undefi...
method onPostFileSubmission (line 53) | onPostFileSubmission(
method onValidateFileSubmission (line 61) | async onValidateFileSubmission(
method onLogin (line 71) | public onLogin(): Promise<ILoginState> {
FILE: apps/client-server/src/app/websites/implementations/derpibooru/derpibooru.website.ts
class Derpibooru (line 32) | class Derpibooru extends PhilomenaWebsite<DerpibooruFileSubmission> {
method createFileModel (line 35) | createFileModel(): DerpibooruFileSubmission {
FILE: apps/client-server/src/app/websites/implementations/derpibooru/models/derpibooru-file-submission.ts
class DerpibooruFileSubmission (line 3) | class DerpibooruFileSubmission extends PhilomenaFileSubmission {}
FILE: apps/client-server/src/app/websites/implementations/deviant-art/deviant-art-description-converter.ts
method validate (line 42) | validate(url) {
method addCommands (line 52) | addCommands() {
class DeviantArtDescriptionConverter (line 69) | class DeviantArtDescriptionConverter {
method convert (line 70) | static convert(html: string): string {
FILE: apps/client-server/src/app/websites/implementations/deviant-art/deviant-art.website.ts
type DeviantArtFolder (line 32) | interface DeviantArtFolder {
class DeviantArt (line 72) | class DeviantArt
method onLogin (line 87) | public async onLogin(): Promise<ILoginState> {
method getCSRF (line 106) | private async getCSRF(accountId = this.accountId) {
method getFolders (line 113) | private async getFolders(username: string) {
method createFileModel (line 173) | createFileModel(): DeviantArtFileSubmission {
method calculateImageResize (line 177) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 181) | async onPostFileSubmission(
method onValidateFileSubmission (line 325) | async onValidateFileSubmission(
method createMessageModel (line 347) | createMessageModel(): DeviantArtMessageSubmission {
method onPostMessageSubmission (line 351) | async onPostMessageSubmission(
method stripInvalidCharacters (line 418) | private stripInvalidCharacters(title: string) {
FILE: apps/client-server/src/app/websites/implementations/deviant-art/models/deviant-art-account-data.ts
type DeviantArtAccountData (line 3) | type DeviantArtAccountData = {
FILE: apps/client-server/src/app/websites/implementations/deviant-art/models/deviant-art-file-submission.ts
class DeviantArtFileSubmission (line 17) | class DeviantArtFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/deviant-art/models/deviant-art-message-submission.ts
class DeviantArtMessageSubmission (line 5) | class DeviantArtMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/discord/discord.website.ts
class Discord (line 42) | class Discord
method onLogin (line 58) | public async onLogin(): Promise<ILoginState> {
method createMessageModel (line 80) | createMessageModel(): DiscordMessageSubmission {
method createFileModel (line 84) | createFileModel(): DiscordFileSubmission {
method calculateImageResize (line 88) | calculateImageResize(): ImageResizeProps {
method getDynamicFileSizeLimits (line 92) | getDynamicFileSizeLimits(): DynamicFileSizeLimits {
method onPostFileSubmission (line 106) | onPostFileSubmission(
method onPostMessageSubmission (line 157) | onPostMessageSubmission(
method handleResponse (line 180) | private handleResponse(res: HttpResponse<unknown>): IPostResponse {
method handleError (line 189) | private handleError(error: Error, payload: unknown): IPostResponse {
method buildDescription (line 201) | private buildDescription(
FILE: apps/client-server/src/app/websites/implementations/discord/models/discord-file-submission.ts
class DiscordFileSubmission (line 9) | class DiscordFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/discord/models/discord-message-submission.ts
class DiscordMessageSubmission (line 9) | class DiscordMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/e621/e621.website.ts
class E621 (line 58) | class E621
method onLogin (line 73) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 102) | createFileModel(): E621FileSubmission {
method calculateImageResize (line 106) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method getDescriptionConverter (line 110) | getDescriptionConverter(): BaseConverter {
method request (line 116) | private async request<T>(
method onPostFileSubmission (line 136) | async onPostFileSubmission(
method onValidateFileSubmission (line 190) | async onValidateFileSubmission(
method getRating (line 201) | private getRating(rating: SubmissionRating) {
method validateUserFeedback (line 214) | private async validateUserFeedback(
method validateTags (line 252) | private async validateTags(
method tagIsInvalid (line 301) | private tagIsInvalid(context: TagCheckingContext, tag: string) {
method validateTag (line 312) | private validateTag(tag: E621Tag, context: TagCheckingContext) {
method getUserFeedback (line 332) | private async getUserFeedback(username: string) {
method getTagMetadata (line 338) | private async getTagMetadata(formattedTags: string[]) {
method getMetadata (line 348) | private async getMetadata<T extends object>(url: string) {
class E621Converter (line 368) | class E621Converter extends BBCodeConverter {
method convertBlockNode (line 369) | convertBlockNode(node: TipTapNode, context: ConversionContext): string {
method convertBlocks (line 378) | convertBlocks(nodes: TipTapNode[], context: ConversionContext): string {
type TagCheckingContext (line 387) | interface TagCheckingContext {
type E621TagsEmpty (line 394) | interface E621TagsEmpty {
type E621Tags (line 399) | type E621Tags = E621Tag[];
type E621Tag (line 402) | interface E621Tag {
type E621UserFeedbacksEmpty (line 426) | interface E621UserFeedbacksEmpty {
type E621UserFeedbacks (line 431) | type E621UserFeedbacks = E621UserFeedback[];
type E621UserFeedbackCategory (line 434) | enum E621UserFeedbackCategory {
type E621UserFeedback (line 441) | interface E621UserFeedback {
FILE: apps/client-server/src/app/websites/implementations/e621/models/e621-file-submission.ts
class E621FileSubmission (line 10) | class E621FileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/firefish/firefish.website.ts
class Firefish (line 47) | class Firefish extends MegalodonWebsite {
method getMegalodonInstanceType (line 48) | protected getMegalodonInstanceType(): 'firefish' {
method getDefaultMaxDescriptionLength (line 52) | protected getDefaultMaxDescriptionLength(): number {
FILE: apps/client-server/src/app/websites/implementations/friendica/friendica.website.ts
class Friendica (line 47) | class Friendica extends MegalodonWebsite {
method getMegalodonInstanceType (line 48) | protected getMegalodonInstanceType(): 'friendica' {
method getDefaultMaxDescriptionLength (line 52) | protected getDefaultMaxDescriptionLength(): number {
FILE: apps/client-server/src/app/websites/implementations/fur-affinity/fur-affinity.website.ts
class FurAffinity (line 74) | class FurAffinity
method onLogin (line 87) | public async onLogin(): Promise<ILoginState> {
method getFolders (line 111) | private getFolders($: HTMLElement) {
method createFileModel (line 153) | createFileModel(): FurAffinityFileSubmission {
method calculateImageResize (line 157) | calculateImageResize(): ImageResizeProps {
method processForError (line 161) | private processForError(body: string): string | undefined {
method onPostFileSubmission (line 177) | async onPostFileSubmission(
method onValidateFileSubmission (line 291) | async onValidateFileSubmission(
method createMessageModel (line 311) | createMessageModel(): FurAffinityMessageSubmission {
method onPostMessageSubmission (line 315) | async onPostMessageSubmission(
method getContentType (line 362) | private getContentType(type: FileType) {
method getContentCategory (line 376) | private getContentCategory(type: FileType) {
method getRating (line 390) | private getRating(rating: SubmissionRating) {
FILE: apps/client-server/src/app/websites/implementations/fur-affinity/models/fur-affinity-account-data.ts
type FurAffinityAccountData (line 3) | type FurAffinityAccountData = {
FILE: apps/client-server/src/app/websites/implementations/fur-affinity/models/fur-affinity-file-submission.ts
class FurAffinityFileSubmission (line 21) | class FurAffinityFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/fur-affinity/models/fur-affinity-message-submission.ts
class FurAffinityMessageSubmission (line 10) | class FurAffinityMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/furbooru/furbooru.website.ts
class Furbooru (line 31) | class Furbooru extends PhilomenaWebsite<FurbooruFileSubmission> {
method createFileModel (line 34) | createFileModel(): FurbooruFileSubmission {
method onPostFileSubmission (line 38) | async onPostFileSubmission(
FILE: apps/client-server/src/app/websites/implementations/furbooru/models/furbooru-file-submission.ts
class FurbooruFileSubmission (line 5) | class FurbooruFileSubmission extends PhilomenaFileSubmission {
FILE: apps/client-server/src/app/websites/implementations/gotosocial/gotosocial.website.ts
class GoToSocial (line 47) | class GoToSocial extends MegalodonWebsite {
method getMegalodonInstanceType (line 48) | protected getMegalodonInstanceType(): 'gotosocial' {
method getDefaultMaxDescriptionLength (line 52) | protected getDefaultMaxDescriptionLength(): number {
FILE: apps/client-server/src/app/websites/implementations/hentai-foundry/hentai-foundry.website.ts
class HentaiFoundry (line 50) | class HentaiFoundry
method onLogin (line 63) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 84) | createFileModel(): HentaiFoundryFileSubmission {
method createMessageModel (line 88) | createMessageModel(): HentaiFoundryMessageSubmission {
method calculateImageResize (line 92) | calculateImageResize(file: ISubmissionFile): ImageResizeProps | undefi...
method onPostFileSubmission (line 100) | async onPostFileSubmission(
method onPostMessageSubmission (line 211) | async onPostMessageSubmission(
method formatTags (line 247) | private formatTags(tags: string[]): string[] {
FILE: apps/client-server/src/app/websites/implementations/hentai-foundry/models/hentai-foundry-account-data.ts
type HentaiFoundryAccountData (line 3) | type HentaiFoundryAccountData = {
FILE: apps/client-server/src/app/websites/implementations/hentai-foundry/models/hentai-foundry-file-submission.ts
class HentaiFoundryFileSubmission (line 16) | class HentaiFoundryFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/hentai-foundry/models/hentai-foundry-message-submission.ts
class HentaiFoundryMessageSubmission (line 5) | class HentaiFoundryMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/inkbunny/inkbunny.website.ts
class Inkbunny (line 78) | class Inkbunny
method onLogin (line 122) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 153) | createFileModel(): InkbunnyFileSubmission {
method calculateImageResize (line 157) | calculateImageResize(): ImageResizeProps {
method onPostFileSubmission (line 161) | async onPostFileSubmission(
method formatTags (line 259) | private formatTags(tags: string[]): string[] {
method getRating (line 265) | private getRating(rating: SubmissionRating): string {
FILE: apps/client-server/src/app/websites/implementations/inkbunny/models/inkbunny-file-submission.ts
class InkbunnyFileSubmission (line 16) | class InkbunnyFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/instagram/instagram-api-service/instagram-api-service.ts
constant GRAPH_API_BASE (line 4) | const GRAPH_API_BASE = 'https://graph.instagram.com/v21.0';
function getInstagramRedirectUri (line 10) | function getInstagramRedirectUri(port: string | number): string {
constant CODE_EXPIRY_MS (line 24) | const CODE_EXPIRY_MS = 5 * 60 * 1000;
function storeOAuthCode (line 29) | function storeOAuthCode(state: string, code: string): void {
function retrieveOAuthCode (line 37) | function retrieveOAuthCode(state: string): string | undefined {
type InstagramTokenResult (line 50) | interface InstagramTokenResult {
type InstagramLongLivedTokenResult (line 56) | interface InstagramLongLivedTokenResult {
type InstagramBusinessAccount (line 62) | interface InstagramBusinessAccount {
type InstagramContainerResult (line 67) | interface InstagramContainerResult {
type InstagramContainerStatus (line 71) | type InstagramContainerStatus =
type InstagramPublishResult (line 78) | interface InstagramPublishResult {
type InstagramPublishingLimit (line 82) | interface InstagramPublishingLimit {
class InstagramApiService (line 94) | class InstagramApiService {
method logger (line 98) | private static get logger(): PostyBirbLogger {
method getAuthUrl (line 113) | static getAuthUrl(appId: string, redirectUri: string, state: string): ...
method exchangeCodeForToken (line 136) | static async exchangeCodeForToken(
method getLongLivedToken (line 175) | static async getLongLivedToken(
method refreshLongLivedToken (line 209) | static async refreshLongLivedToken(
method getInstagramBusinessAccount (line 240) | static async getInstagramBusinessAccount(
method verifyToken (line 269) | static async verifyToken(
method createImageContainer (line 295) | static async createImageContainer(
method createCarouselContainer (line 342) | static async createCarouselContainer(
method checkContainerStatus (line 382) | static async checkContainerStatus(
method pollUntilReady (line 403) | static async pollUntilReady(
method publishMedia (line 452) | static async publishMedia(
method getMediaPermalink (line 484) | static async getMediaPermalink(
method checkPublishingLimit (line 510) | static async checkPublishingLimit(
FILE: apps/client-server/src/app/websites/implementations/instagram/instagram-blob-service/instagram-blob-service.ts
constant FUNCTION_BASE_URL (line 3) | const FUNCTION_BASE_URL =
type UploadResponse (line 6) | interface UploadResponse {
class InstagramBlobService (line 17) | class InstagramBlobService {
method logger (line 20) | private static get logger(): PostyBirbLogger {
method upload (line 31) | static async upload(
FILE: apps/client-server/src/app/websites/implementations/instagram/instagram.website.ts
class Instagram (line 65) | class Instagram
method onLogin (line 218) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 261) | createFileModel(): InstagramFileSubmission {
method calculateImageResize (line 265) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 280) | async onPostFileSubmission(
method onValidateFileSubmission (line 386) | async onValidateFileSubmission(
FILE: apps/client-server/src/app/websites/implementations/instagram/models/instagram-file-submission.ts
class InstagramFileSubmission (line 15) | class InstagramFileSubmission extends BaseWebsiteOptions {
method processTag (line 47) | override processTag(tag: string): string {
FILE: apps/client-server/src/app/websites/implementations/itaku/itaku.website.ts
type ItakuSessionData (line 31) | type ItakuSessionData = {
class Itaku (line 61) | class Itaku
method onLogin (line 75) | public async onLogin(): Promise<ILoginState> {
method retrieveFolders (line 102) | private async retrieveFolders(): Promise<void> {
method convertRating (line 157) | private convertRating(rating: SubmissionRating): string {
method createFileModel (line 170) | createFileModel(): ItakuFileSubmission {
method calculateImageResize (line 174) | calculateImageResize(): ImageResizeProps {
method uploadFile (line 178) | private async uploadFile(
method postSubmission (line 225) | async postSubmission(
method onPostFileSubmission (line 264) | async onPostFileSubmission(
method onValidateFileSubmission (line 292) | async onValidateFileSubmission(
method createMessageModel (line 315) | createMessageModel(): ItakuMessageSubmission {
method onPostMessageSubmission (line 319) | async onPostMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/itaku/models/itaku-account-data.ts
type ItakuAccountData (line 3) | type ItakuAccountData = {
FILE: apps/client-server/src/app/websites/implementations/itaku/models/itaku-file-submission.ts
class ItakuFileSubmission (line 18) | class ItakuFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/itaku/models/itaku-message-submission.ts
class ItakuMessageSubmission (line 11) | class ItakuMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/itaku/models/itaku-user-info.ts
type ItakuUserInfo (line 1) | type ItakuUserInfo = {
FILE: apps/client-server/src/app/websites/implementations/ko-fi/ko-fi.website.ts
type KoFiSessionData (line 29) | type KoFiSessionData = {
class KoFi (line 43) | class KoFi
method onLogin (line 56) | public async onLogin(): Promise<ILoginState> {
method calculateImageResize (line 92) | calculateImageResize(file: ISubmissionFile): ImageResizeProps | undefi...
method extractId (line 96) | private extractId(html: string): string | null {
method retrieveAlbums (line 101) | private async retrieveAlbums(id: string): Promise<void> {
method createMessageModel (line 133) | createMessageModel(): BaseWebsiteOptions {
method createFileModel (line 137) | createFileModel(): KoFiFileSubmission {
method onPostFileSubmission (line 141) | async onPostFileSubmission(
method onPostMessageSubmission (line 231) | async onPostMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/ko-fi/models/ko-fi-account-data.ts
type KoFiAccountData (line 3) | interface KoFiAccountData {
FILE: apps/client-server/src/app/websites/implementations/ko-fi/models/ko-fi-file-submission.ts
class KoFiFileSubmission (line 17) | class KoFiFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/ko-fi/models/ko-fi-message-submission.ts
class KoFiMessageSubmission (line 5) | class KoFiMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/manebooru/manebooru.website.ts
class Manebooru (line 32) | class Manebooru extends PhilomenaWebsite<ManebooruFileSubmission> {
method createFileModel (line 35) | createFileModel(): ManebooruFileSubmission {
FILE: apps/client-server/src/app/websites/implementations/manebooru/models/manebooru-file-submission.ts
class ManebooruFileSubmission (line 3) | class ManebooruFileSubmission extends PhilomenaFileSubmission {}
FILE: apps/client-server/src/app/websites/implementations/mastodon/mastodon.website.ts
class Mastodon (line 47) | class Mastodon extends MegalodonWebsite {
method getMegalodonInstanceType (line 48) | protected getMegalodonInstanceType(): 'mastodon' {
method getDefaultMaxDescriptionLength (line 52) | protected getDefaultMaxDescriptionLength(): number {
FILE: apps/client-server/src/app/websites/implementations/megalodon/megalodon-api-service.ts
type AppRegistrationData (line 3) | interface AppRegistrationData {
type FediverseInstanceTypes (line 8) | type FediverseInstanceTypes =
class MegalodonApiService (line 19) | class MegalodonApiService {
method registerApp (line 23) | static async registerApp(
method generateAuthUrl (line 53) | static generateAuthUrl(
method fetchAccessToken (line 73) | static async fetchAccessToken(
method createClient (line 98) | static createClient(
FILE: apps/client-server/src/app/websites/implementations/megalodon/megalodon.website.ts
type InstanceLimits (line 34) | interface InstanceLimits {
method getAppName (line 82) | protected getAppName(): string {
method getAppWebsite (line 90) | protected getAppWebsite(): string {
method getRedirectUri (line 98) | protected getRedirectUri(): string {
method getScopes (line 106) | protected getScopes(): string {
method detectMegalodonInstanceType (line 116) | private async detectMegalodonInstanceType(
method getInstanceType (line 123) | private async getInstanceType(
method normalizeInstanceUrl (line 244) | private normalizeInstanceUrl(url: string): string {
method onLogin (line 251) | public async onLogin(): Promise<ILoginState> {
method fetchInstanceLimits (line 311) | private async fetchInstanceLimits(
method getMaxDescriptionLength (line 372) | protected getMaxDescriptionLength(): number {
method getMaxMediaAttachments (line 388) | protected getMaxMediaAttachments(): number {
method getImageSizeLimit (line 395) | protected getImageSizeLimit(): number | undefined {
method getVideoSizeLimit (line 402) | protected getVideoSizeLimit(): number | undefined {
method getImageMatrixLimit (line 409) | protected getImageMatrixLimit(): number | undefined {
method getVideoMatrixLimit (line 416) | protected getVideoMatrixLimit(): number | undefined {
method getSupportedMimeTypes (line 423) | protected getSupportedMimeTypes(): string[] | undefined {
method createFileModel (line 427) | createFileModel(): MegalodonFileSubmission {
method createMessageModel (line 431) | createMessageModel(): MegalodonMessageSubmission {
method calculateImageResize (line 435) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 491) | async onPostFileSubmission(
method onPostMessageSubmission (line 585) | async onPostMessageSubmission(
method onValidateFileSubmission (line 633) | async onValidateFileSubmission(
method onValidateMessageSubmission (line 653) | async onValidateMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/megalodon/models/megalodon-file-submission.ts
class MegalodonFileSubmission (line 16) | class MegalodonFileSubmission extends BaseWebsiteOptions {
method processTag (line 30) | override processTag(tag: string) {
FILE: apps/client-server/src/app/websites/implementations/megalodon/models/megalodon-message-submission.ts
class MegalodonMessageSubmission (line 3) | class MegalodonMessageSubmission extends MegalodonFileSubmission {}
FILE: apps/client-server/src/app/websites/implementations/misskey/misskey-api-service.ts
function ensureJson (line 11) | function ensureJson<T>(body: T | string): T {
type MisskeyUser (line 18) | interface MisskeyUser {
type MisskeyPolicies (line 25) | interface MisskeyPolicies {
type MisskeyMeta (line 33) | interface MisskeyMeta {
type MisskeyDriveFile (line 39) | interface MisskeyDriveFile {
type MisskeyNote (line 47) | interface MisskeyNote {
class MisskeyApiService (line 56) | class MisskeyApiService {
method buildMiAuthUrl (line 60) | static buildMiAuthUrl(
method checkMiAuth (line 77) | static async checkMiAuth(
method verifyCredentials (line 98) | static async verifyCredentials(
method getInstanceMeta (line 119) | static async getInstanceMeta(
method uploadFile (line 134) | static async uploadFile(
method createNote (line 189) | static async createNote(
FILE: apps/client-server/src/app/websites/implementations/misskey/misskey.website.ts
constant MIAUTH_PERMISSIONS (line 31) | const MIAUTH_PERMISSIONS = [
class Misskey (line 64) | class Misskey
method onLogin (line 161) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 212) | createFileModel(): MisskeyFileSubmission {
method calculateImageResize (line 216) | calculateImageResize(file: ISubmissionFile): ImageResizeProps | undefi...
method onPostFileSubmission (line 224) | async onPostFileSubmission(
method onValidateFileSubmission (line 296) | async onValidateFileSubmission(
method createMessageModel (line 320) | createMessageModel(): MisskeyMessageSubmission {
method onPostMessageSubmission (line 324) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 361) | async onValidateMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/misskey/models/misskey-file-submission.ts
class MisskeyFileSubmission (line 16) | class MisskeyFileSubmission extends BaseWebsiteOptions {
method processTag (line 29) | override processTag(tag: string) {
FILE: apps/client-server/src/app/websites/implementations/misskey/models/misskey-message-submission.ts
class MisskeyMessageSubmission (line 3) | class MisskeyMessageSubmission extends MisskeyFileSubmission {}
FILE: apps/client-server/src/app/websites/implementations/newgrounds/models/newgrounds-account-data.ts
type NewgroundsAccountData (line 2) | type NewgroundsAccountData = {};
FILE: apps/client-server/src/app/websites/implementations/newgrounds/models/newgrounds-base-submission.ts
class NewgroundsBaseSubmission (line 5) | class NewgroundsBaseSubmission extends BaseWebsiteOptions {
method processTag (line 17) | protected processTag(tag: string): string {
FILE: apps/client-server/src/app/websites/implementations/newgrounds/models/newgrounds-file-submission.ts
type NewgroundsRating (line 10) | type NewgroundsRating = 'a' | 'b' | 'c';
class NewgroundsFileSubmission (line 12) | class NewgroundsFileSubmission extends NewgroundsBaseSubmission {
FILE: apps/client-server/src/app/websites/implementations/newgrounds/models/newgrounds-message-submission.ts
class NewgroundsMessageSubmission (line 5) | class NewgroundsMessageSubmission extends NewgroundsBaseSubmission {
FILE: apps/client-server/src/app/websites/implementations/newgrounds/newgrounds.website.ts
type NewgroundsPostResponse (line 31) | type NewgroundsPostResponse = {
class Newgrounds (line 59) | class Newgrounds
method onLogin (line 70) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 89) | createFileModel(): NewgroundsFileSubmission {
method calculateImageResize (line 93) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method parseDescription (line 97) | private parseDescription(text: string): string {
method getSuitabilityRating (line 101) | private getSuitabilityRating(rating: SubmissionRating | string): string {
method checkIsSaved (line 117) | private checkIsSaved(response: NewgroundsPostResponse): boolean {
method cleanUpFailedProject (line 121) | private async cleanUpFailedProject(
method onPostFileSubmission (line 138) | async onPostFileSubmission(
method onValidateFileSubmission (line 334) | async onValidateFileSubmission(
method createMessageModel (line 342) | createMessageModel(): NewgroundsMessageSubmission {
method onPostMessageSubmission (line 346) | async onPostMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/patreon/models/patreon-account-data.ts
type PatreonAccountData (line 3) | type PatreonAccountData = {
FILE: apps/client-server/src/app/websites/implementations/patreon/models/patreon-campaign-types.ts
type PatreonCampaignResponse (line 5) | interface PatreonCampaignResponse {
type PatreonCampaign (line 13) | interface PatreonCampaign {
type PatreonCampaignAttributes (line 20) | interface PatreonCampaignAttributes {
type PatreonCampaignRelationships (line 75) | interface PatreonCampaignRelationships {
type PatreonUser (line 85) | interface PatreonUser {
type PatreonUserAttributes (line 94) | interface PatreonUserAttributes {
type PatreonSocialConnections (line 127) | interface PatreonSocialConnections {
type PatreonDiscordConnection (line 143) | interface PatreonDiscordConnection {
type PatreonSocialConnection (line 148) | type PatreonSocialConnection = Record<string, unknown> | null;
type PatreonReward (line 150) | interface PatreonReward {
type PatreonAccessRule (line 159) | interface PatreonAccessRule {
type PatreonRewardAttributes (line 180) | interface PatreonRewardAttributes {
type PatreonRelationship (line 210) | interface PatreonRelationship<T extends string> {
type PatreonRelationshipData (line 217) | interface PatreonRelationshipData<T extends string> {
FILE: apps/client-server/src/app/websites/implementations/patreon/models/patreon-collection-types.ts
type PatreonCollectionThumbnail (line 1) | type PatreonCollectionThumbnail = {
type PatreonCollectionAttributes (line 16) | type PatreonCollectionAttributes = {
type PatreonCollection (line 32) | type PatreonCollection = {
type PatreonCollectionPaginationCursors (line 38) | type PatreonCollectionPaginationCursors = {
type PatreonCollectionPagination (line 42) | type PatreonCollectionPagination = {
type PatreonCollectionMeta (line 47) | type PatreonCollectionMeta = {
type PatreonCollectionResponse (line 51) | type PatreonCollectionResponse = {
FILE: apps/client-server/src/app/websites/implementations/patreon/models/patreon-file-submission.ts
class PatreonFileSubmission (line 12) | class PatreonFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/patreon/models/patreon-media-upload-types.ts
type PatreonMediaState (line 1) | type PatreonMediaState =
type PatreonMediaOwnerType (line 7) | type PatreonMediaOwnerType = 'post' | 'user' | 'campaign';
type PatreonMediaOwnerRelationship (line 9) | type PatreonMediaOwnerRelationship =
type PatreonMediaType (line 15) | type PatreonMediaType = 'image' | 'video' | 'audio';
type PatreonMediaUploadAttributes (line 17) | type PatreonMediaUploadAttributes = {
type PatreonMediaUpload (line 27) | type PatreonMediaUpload = {
type PatreonMediaUploadRequest (line 32) | type PatreonMediaUploadRequest = {
type PatreonMediaUploadParameters (line 37) | type PatreonMediaUploadParameters = {
type PatreonMediaImageUrls (line 49) | type PatreonMediaImageUrls = {
type PatreonMediaDimensions (line 62) | type PatreonMediaDimensions = {
type PatreonMediaMetadata (line 67) | type PatreonMediaMetadata = {
type PatreonMediaDisplay (line 71) | type PatreonMediaDisplay = {
type PatreonMediaUploadResponseAttributes (line 77) | type PatreonMediaUploadResponseAttributes = {
type PatreonMediaUploadResponseData (line 96) | type PatreonMediaUploadResponseData = {
type PatreonMediaUploadResponseLinks (line 102) | type PatreonMediaUploadResponseLinks = {
type PatreonMediaUploadResponse (line 106) | type PatreonMediaUploadResponse = {
FILE: apps/client-server/src/app/websites/implementations/patreon/models/patreon-message-submission.ts
class PatreonMessageSubmission (line 12) | class PatreonMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/patreon/models/patreon-post-types.ts
type PatreonNewPostResponse (line 22) | interface PatreonNewPostResponse {
FILE: apps/client-server/src/app/websites/implementations/patreon/patreon-description-converter.ts
method addAttributes (line 25) | addAttributes() {
method addAttributes (line 44) | addAttributes() {
method renderHTML (line 50) | renderHTML() {
method parseHTML (line 54) | parseHTML() {
type PatreonContentOptions (line 80) | interface PatreonContentOptions {
class PatreonDescriptionConverter (line 88) | class PatreonDescriptionConverter {
method convert (line 93) | static convert(html: string, options: PatreonContentOptions): string {
FILE: apps/client-server/src/app/websites/implementations/patreon/patreon.website.ts
type PatreonAccessRuleSegment (line 43) | type PatreonAccessRuleSegment = Array<{
type PatreonTagSegment (line 49) | type PatreonTagSegment = Array<{
class Patreon (line 92) | class Patreon
method onLogin (line 114) | public async onLogin(): Promise<ILoginState> {
method parseTiers (line 168) | private parseTiers(campaign: PatreonCampaignResponse): SelectOption[] {
method loadCollections (line 224) | private async loadCollections(campaignId: string): Promise<SelectOptio...
method getPostType (line 253) | private getPostType(fileType: FileType): string {
method createFileModel (line 267) | createFileModel(): PatreonFileSubmission {
method calculateImageResize (line 271) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method initializePost (line 275) | private async initializePost() {
method finalizePost (line 299) | private async finalizePost(
method getMediaType (line 318) | private getMediaType(fileType: FileType): PatreonMediaType | undefined {
method uploadMedia (line 331) | private async uploadMedia(
method onPostFileSubmission (line 417) | async onPostFileSubmission(
method onValidateFileSubmission (line 492) | async onValidateFileSubmission(
method createMessageModel (line 500) | createMessageModel(): PatreonMessageSubmission {
method onPostMessageSubmission (line 504) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 538) | async onValidateMessageSubmission(
method createTagsSegment (line 545) | private createTagsSegment(tags: string[]): PatreonTagSegment {
method createAccessRuleSegment (line 556) | private createAccessRuleSegment(
method createDefaultMetadataSegment (line 566) | private createDefaultMetadataSegment() {
method createDataSegment (line 573) | private createDataSegment(
FILE: apps/client-server/src/app/websites/implementations/philomena/models/philomena-account-data.ts
type PhilomenaAccountData (line 2) | type PhilomenaAccountData = {};
FILE: apps/client-server/src/app/websites/implementations/philomena/models/philomena-file-submission.ts
class PhilomenaFileSubmission (line 9) | class PhilomenaFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/philomena/philomena.website.ts
method onLogin (line 41) | public async onLogin(): Promise<ILoginState> {
method calculateImageResize (line 59) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method getRating (line 66) | protected getRating(rating: SubmissionRating): string {
method getKnownRatings (line 83) | protected getKnownRatings(): string[] {
method tagsWithRatingTag (line 98) | protected tagsWithRatingTag(
method getUploadFormFields (line 129) | protected async getUploadFormFields(): Promise<Record<string, string>> {
method onPostFileSubmission (line 151) | async onPostFileSubmission(
FILE: apps/client-server/src/app/websites/implementations/picarto/models/picarto-account-data.ts
type PicartoAccountData (line 3) | type PicartoAccountData = {
FILE: apps/client-server/src/app/websites/implementations/picarto/models/picarto-file-submission.ts
class PicartoFileSubmission (line 12) | class PicartoFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/picarto/picarto.website.ts
class Picarto (line 41) | class Picarto
method onLogin (line 55) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 85) | createFileModel(): PicartoFileSubmission {
method calculateImageResize (line 89) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 97) | async onPostFileSubmission(
method retrieveAlbums (line 235) | private async retrieveAlbums() {
method convertRating (line 267) | private convertRating(rating: SubmissionRating): 'SFW' | 'ECCHI' | 'NS...
FILE: apps/client-server/src/app/websites/implementations/piczel/models/piczel-account-data.ts
type PiczelAccountData (line 3) | type PiczelAccountData = {
FILE: apps/client-server/src/app/websites/implementations/piczel/models/piczel-file-submission.ts
class PiczelFileSubmission (line 13) | class PiczelFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/piczel/piczel.website.ts
class Piczel (line 41) | class Piczel
method onLogin (line 63) | public async onLogin(): Promise<ILoginState> {
method getFolders (line 95) | private async getFolders(username: string): Promise<void> {
method createFileModel (line 118) | createFileModel(): PiczelFileSubmission {
method calculateImageResize (line 122) | calculateImageResize(): ImageResizeProps | undefined {
method onPostFileSubmission (line 126) | async onPostFileSubmission(
FILE: apps/client-server/src/app/websites/implementations/pillowfort/models/pillowfort-account-data.ts
type PillowfortAccountData (line 1) | type PillowfortAccountData = {
FILE: apps/client-server/src/app/websites/implementations/pillowfort/models/pillowfort-file-submission.ts
class PillowfortFileSubmission (line 9) | class PillowfortFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/pillowfort/models/pillowfort-message-submission.ts
class PillowfortMessageSubmission (line 9) | class PillowfortMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/pillowfort/pillowfort.website.ts
class Pillowfort (line 43) | class Pillowfort
method onLogin (line 54) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 78) | createFileModel(): PillowfortFileSubmission {
method calculateImageResize (line 82) | calculateImageResize(): ImageResizeProps {
method onPostFileSubmission (line 87) | async onPostFileSubmission(
method createMessageModel (line 193) | createMessageModel(): PillowfortMessageSubmission {
method onPostMessageSubmission (line 197) | async onPostMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/pixelfed/pixelfed.website.ts
class Pixelfed (line 31) | class Pixelfed extends MegalodonWebsite {
method getMegalodonInstanceType (line 32) | protected getMegalodonInstanceType(): 'pixelfed' {
method getDefaultMaxDescriptionLength (line 36) | protected getDefaultMaxDescriptionLength(): number {
FILE: apps/client-server/src/app/websites/implementations/pixiv/models/pixiv-account-data.ts
type PixivAccountData (line 2) | type PixivAccountData = {};
FILE: apps/client-server/src/app/websites/implementations/pixiv/models/pixiv-file-submission.ts
class PixivFileSubmission (line 19) | class PixivFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/pixiv/pixiv.website.ts
class Pixiv (line 37) | class Pixiv
method onLogin (line 46) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 75) | createFileModel(): PixivFileSubmission {
method calculateImageResize (line 79) | calculateImageResize(): ImageResizeProps {
method onPostFileSubmission (line 83) | async onPostFileSubmission(
method getContentRating (line 183) | private getContentRating(rating: SubmissionRating) {
FILE: apps/client-server/src/app/websites/implementations/pleroma/pleroma.website.ts
class Pleroma (line 35) | class Pleroma extends MegalodonWebsite {
method getMegalodonInstanceType (line 36) | protected getMegalodonInstanceType(): 'pleroma' {
method getDefaultMaxDescriptionLength (line 40) | protected getDefaultMaxDescriptionLength(): number {
FILE: apps/client-server/src/app/websites/implementations/sofurry/models/sofurry-account-data.ts
type SofurryAccountData (line 3) | type SofurryAccountData = {
FILE: apps/client-server/src/app/websites/implementations/sofurry/models/sofurry-categories.ts
function getCategoryFromType (line 108) | function getCategoryFromType(typeId: string): string {
function getDefaultTypeForCategory (line 116) | function getDefaultTypeForCategory(categoryId: string): string {
FILE: apps/client-server/src/app/websites/implementations/sofurry/models/sofurry-file-submission.ts
class SofurryFileSubmission (line 22) | class SofurryFileSubmission extends BaseWebsiteOptions {
method processTag (line 128) | processTag(tag: string): string {
FILE: apps/client-server/src/app/websites/implementations/sofurry/sofurry.website.ts
type SofurrySubmissionResponse (line 26) | interface SofurrySubmissionResponse {
type SoFurryFileUploadResponse (line 57) | interface SoFurryFileUploadResponse {
type SoFurryThumbnailUploadResponse (line 69) | interface SoFurryThumbnailUploadResponse {
class Sofurry (line 103) | class Sofurry
method onLogin (line 114) | public async onLogin(): Promise<ILoginState> {
method fetchCsrfToken (line 147) | private async fetchCsrfToken(): Promise<string | undefined> {
method getFolders (line 161) | private async getFolders(csrfToken: string): Promise<void> {
method createFileModel (line 188) | createFileModel(): SofurryFileSubmission {
method calculateImageResize (line 192) | calculateImageResize(): ImageResizeProps {
method getRating (line 196) | private getRating(rating: SubmissionRating): number {
method getDefaultCategoryAndType (line 209) | private getDefaultCategoryAndType(fileType: FileType): {
method onPostFileSubmission (line 226) | async onPostFileSubmission(
FILE: apps/client-server/src/app/websites/implementations/subscribe-star/base-subscribe-star.website.ts
type SubscribeStarSession (line 24) | type SubscribeStarSession = {
type SubscribeStarUploadData (line 29) | type SubscribeStarUploadData = {
type SubscribeStarPostResponse (line 35) | type SubscribeStarPostResponse = {
type SubscribeStarUploadItem (line 44) | type SubscribeStarUploadItem = {
type SubscribeStarProcessFileResponse (line 59) | type SubscribeStarProcessFileResponse = {
method onLogin (line 79) | public async onLogin(): Promise<ILoginState> {
method loadTiers (line 112) | private loadTiers($: HTMLElement) {
method createFileModel (line 149) | createFileModel(): SubscribeStarFileSubmission {
method calculateImageResize (line 153) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method getPostData (line 157) | private async getPostData(): Promise<SubscribeStarUploadData | undefined> {
method fallbackS3TokenLoader (line 200) | private async fallbackS3TokenLoader(
method uploadFile (line 247) | private async uploadFile(
method onPostFileSubmission (line 343) | async onPostFileSubmission(
method onValidateFileSubmission (line 415) | async onValidateFileSubmission(
method createMessageModel (line 423) | createMessageModel(): SubscribeStarMessageSubmission {
method onPostMessageSubmission (line 427) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 475) | async onValidateMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/subscribe-star/models/subscribe-star-account-data.ts
type SubscribeStarAccountData (line 3) | type SubscribeStarAccountData = {
FILE: apps/client-server/src/app/websites/implementations/subscribe-star/models/subscribe-star-file-submission.ts
class SubscribeStarFileSubmission (line 6) | class SubscribeStarFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/subscribe-star/models/subscribe-star-message-submission.ts
class SubscribeStarMessageSubmission (line 6) | class SubscribeStarMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/subscribe-star/subscribe-star-adult.website.ts
class SubscribeStarAdult (line 87) | class SubscribeStarAdult extends BaseSubscribeStar {
FILE: apps/client-server/src/app/websites/implementations/subscribe-star/subscribe-star.website.ts
class SubscribeStar (line 87) | class SubscribeStar extends BaseSubscribeStar {
FILE: apps/client-server/src/app/websites/implementations/telegram/models/telegram-file-submission.ts
class TelegramFileSubmission (line 15) | class TelegramFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/telegram/models/telegram-message-submission.ts
class TelegramMessageSubmission (line 3) | class TelegramMessageSubmission extends TelegramFileSubmission {}
FILE: apps/client-server/src/app/websites/implementations/telegram/telegram.website.ts
class Telegram (line 67) | class Telegram
method getTelegramClient (line 88) | private async getTelegramClient(
method resolveProxySettings (line 166) | private async resolveProxySettings() {
method loadChannels (line 223) | private async loadChannels(telegram: TelegramClient) {
method canSendMediaInChat (line 251) | private canSendMediaInChat(chat: Entity): chat is Api.Channel | Api.Ch...
method onLogin (line 266) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 288) | createFileModel(): TelegramFileSubmission {
method calculateImageResize (line 292) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 299) | async onPostFileSubmission(
method getSourceFromResponse (line 453) | private getSourceFromResponse(response?: Api.TypeUpdates) {
method getPeer (line 469) | private getPeer(channel: string) {
method validateDescription (line 484) | private async validateDescription(
method onValidateFileSubmission (line 503) | async onValidateFileSubmission(
method createMessageModel (line 513) | createMessageModel(): TelegramMessageSubmission {
method onPostMessageSubmission (line 517) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 547) | async onValidateMessageSubmission(
method getDescriptionConverter (line 557) | getDescriptionConverter(): BaseConverter {
class TelegramConverter (line 562) | class TelegramConverter extends HtmlConverter {
method getBlockSeparator (line 563) | protected getBlockSeparator(): string {
method convertBlocks (line 567) | convertBlocks(nodes: TipTapNode[], context: ConversionContext): string {
method fromJson (line 588) | static fromJson(json: string) {
method fromHtml (line 594) | private static fromHtml(html: string) {
FILE: apps/client-server/src/app/websites/implementations/test/models/test-file-submission.ts
class TestFileSubmission (line 5) | class TestFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/test/models/test-message-submission.ts
class TestMessageSubmission (line 3) | class TestMessageSubmission extends BaseWebsiteOptions {}
FILE: apps/client-server/src/app/websites/implementations/test/test.website.ts
class TestWebsite (line 34) | class TestWebsite
method onLogin (line 47) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 57) | createFileModel(): TestFileSubmission {
method createMessageModel (line 61) | createMessageModel(): TestMessageSubmission {
method calculateImageResize (line 65) | calculateImageResize(): ImageResizeProps {
method onPostFileSubmission (line 69) | async onPostFileSubmission(
method onValidateFileSubmission (line 80) | async onValidateFileSubmission(
method onPostMessageSubmission (line 89) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 114) | async onValidateMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/toyhouse/models/toyhouse-account-data.ts
type ToyhouseAccountData (line 3) | type ToyhouseAccountData = {
FILE: apps/client-server/src/app/websites/implementations/toyhouse/models/toyhouse-file-submission.ts
class ToyhouseFileSubmission (line 26) | class ToyhouseFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/toyhouse/toyhouse.website.ts
class Toyhouse (line 36) | class Toyhouse
method onLogin (line 47) | public async onLogin(): Promise<ILoginState> {
method loadAllCharacters (line 80) | private async loadAllCharacters(firstPage: HTMLElement) {
method createFileModel (line 108) | createFileModel(): ToyhouseFileSubmission {
method calculateImageResize (line 112) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 116) | async onPostFileSubmission(
method onValidateFileSubmission (line 210) | async onValidateFileSubmission(
method getCharacters (line 218) | private getCharacters($: HTMLElement) {
function isLoggedIn (line 237) | function isLoggedIn(res: HttpResponse<string>) {
function parseIsSexual (line 245) | function parseIsSexual(rating: SubmissionRating) {
function parseArtistInfo (line 259) | function parseArtistInfo(submission: PostFields<ToyhouseFileSubmission>) {
FILE: apps/client-server/src/app/websites/implementations/tumblr/models/tumblr-account-data.ts
type TumblrAccountData (line 3) | type TumblrAccountData = {
FILE: apps/client-server/src/app/websites/implementations/tumblr/models/tumblr-file-submission.ts
class TumblrFileSubmission (line 17) | class TumblrFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/tumblr/models/tumblr-message-submission.ts
class TumblrMessageSubmission (line 3) | class TumblrMessageSubmission extends TumblrFileSubmission {}
FILE: apps/client-server/src/app/websites/implementations/tumblr/tumblr.website.ts
type TumblrSessionData (line 35) | type TumblrSessionData = {
type TumblrPostResponse (line 41) | type TumblrPostResponse = {
class Tumblr (line 84) | class Tumblr
method onLogin (line 98) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 144) | createFileModel(): TumblrFileSubmission {
method calculateImageResize (line 148) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 152) | async onPostFileSubmission(
method onValidateFileSubmission (line 228) | async onValidateFileSubmission(
method createMessageModel (line 236) | createMessageModel(): TumblrMessageSubmission {
method getDescriptionConverter (line 240) | getDescriptionConverter(): BaseConverter {
method onPostMessageSubmission (line 244) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 302) | async onValidateMessageSubmission(
method getCommunityLabelCategories (line 310) | private getCommunityLabelCategories(
method uploadFiles (line 337) | private async uploadFiles(
method getMediaType (line 380) | private getMediaType(file: PostingFile): string {
method uploadSingleFile (line 398) | private async uploadSingleFile(
FILE: apps/client-server/src/app/websites/implementations/twitter/models/twitter-file-submission.ts
class TwitterFileSubmission (line 5) | class TwitterFileSubmission extends TwitterMessageSubmission {
FILE: apps/client-server/src/app/websites/implementations/twitter/models/twitter-message-submission.ts
class TwitterMessageSubmission (line 9) | class TwitterMessageSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/twitter/twitter-api-service/twitter-api-service.ts
type TwitterAuthLinkResult (line 13) | interface TwitterAuthLinkResult {
type TwitterAccessTokenResult (line 19) | interface TwitterAccessTokenResult {
type TwitterAccessKeys (line 26) | interface TwitterAccessKeys {
type TweetResultMeta (line 33) | interface TweetResultMeta {
type ContentBlurValue (line 44) | type ContentBlurValue =
class TwitterApiServiceV2 (line 58) | class TwitterApiServiceV2 {
method Logger (line 61) | private static get Logger() {
method createClient (line 73) | private static createClient(auth: TwitterAccessKeys): TwitterApi {
method uploadMediaFiles (line 85) | static async uploadMediaFiles(
method getUserInfo (line 144) | static async getUserInfo(client: TwitterApi): Promise<string | undefin...
method toMediaIdsTuple (line 158) | private static toMediaIdsTuple(
method postTweet (line 183) | static async postTweet(
method generateAuthLink (line 218) | static async generateAuthLink(
method login (line 235) | static async login(
method postStatus (line 259) | static async postStatus(
method postMedia (line 297) | static async postMedia(
method deleteFailedReplyChain (line 387) | public static async deleteFailedReplyChain(
FILE: apps/client-server/src/app/websites/implementations/twitter/twitter.website.ts
class Twitter (line 67) | class Twitter
method onLogin (line 166) | public async onLogin(): Promise<ILoginState> {
method createFileModel (line 174) | createFileModel(): TwitterFileSubmission {
method calculateImageResize (line 178) | calculateImageResize(file: ISubmissionFile): ImageResizeProps {
method onPostFileSubmission (line 182) | async onPostFileSubmission(
method cleanUpFailedPost (line 224) | private async cleanUpFailedPost(
method onValidateFileSubmission (line 258) | async onValidateFileSubmission(
method createMessageModel (line 277) | createMessageModel(): TwitterMessageSubmission {
method onPostMessageSubmission (line 281) | async onPostMessageSubmission(
method onValidateMessageSubmission (line 311) | async onValidateMessageSubmission(
FILE: apps/client-server/src/app/websites/implementations/weasyl/models/weasyl-account-data.ts
type WeasylAccountData (line 3) | type WeasylAccountData = {
FILE: apps/client-server/src/app/websites/implementations/weasyl/models/weasyl-file-submission.ts
class WeasylFileSubmission (line 6) | class WeasylFileSubmission extends BaseWebsiteOptions {
FILE: apps/client-server/src/app/websites/implementations/weasyl/models/weasyl-message-submission.ts
class WeasylMessageSubmission (line 3) | class WeasylMessageSubmission extends BaseWebsiteOptions {}
FILE: apps/client-server/src/app/websites/implementations/weasyl/weasyl.website.ts
class Weasyl (line 66) | class Weasyl
method onLogin (line 79) | public async onLogin(): Promise<ILoginState> {
method getFolders (line 97) | private async getFolders(username: string): Promise<void> {
method createFileModel (line 139) | createFileModel(): WeasylFileSubmission {
method calculateImageResize (line 143) | calculateImageResize(): ImageResizeProps {
method modifyDescription (line 147) | private modifyDescription(html: string) {
method convertRating (line 157) | private convertRating(rating: SubmissionRating) {
method getContentType (line 170) | private getContentType(type: FileType) {
method onPostFileSubmission (line 183) | async onPostFileSubmission(
method createMessageModel (line 295) | createMessageModel(): WeasylMessageSubmission {
method onPostMessageSubmission (line 299) | async onPostMessageSubmission(
FILE: apps/client-server/src/app/websites/models/base-website-options.spec.ts
class ExtendedWebsiteOptions (line 48) | class ExtendedWebsiteOptions extends BaseWebsiteOptions {
method processTag (line 170) | protected processTag(tag: string): string {
class ExtendedWebsiteOptions (line 162) | class ExtendedWebsiteOptions extends BaseWebsiteOptions {
method processTag (line 170) | protected processTag(tag: string): string {
FILE: apps/client-server/src/app/websites/models/base-website-options.ts
class BaseWebsiteOptions (line 27) | class BaseWebsiteOptions implements IWebsiteFormFields {
method constructor (line 69) | constructor(options: Partial<BaseWebsiteOptions | IWebsiteFormFields> ...
method mergeDefaults (line 79) | public mergeDefaults(options: DefaultWebsiteOptions): this {
method getFormFields (line 117) | public getFormFields(params: Record<string, never> = {}) {
method getFormFieldFor (line 126) | public getFormFieldFor(key: keyof IWebsiteFormFields) {
method getProcessedTags (line 135) | public async getProcessedTags(
method processTag (line 173) | protected processTag(tag: string): string {
FILE: apps/client-server/src/app/websites/models/data-property-accessibility.ts
type DataPropertyAccessibility (line 1) | type DataPropertyAccessibility<T> = {
FILE: apps/client-server/src/app/websites/models/default-website-options.ts
class DefaultWebsiteOptions (line 5) | class DefaultWebsiteOptions extends BaseWebsiteOptions {
method constructor (line 17) | constructor(options: Partial<DefaultWebsiteOptions> = {}) {
FILE: apps/client-server/src/app/websites/models/website-modifiers/file-website.ts
type ImplementedFileWebsite (line 16) | type ImplementedFileWebsite = FileWebsite & UnknownWebsite;
type PostBatchData (line 18) | interface PostBatchData {
type FileWebsite (line 28) | interface FileWebsite<
function isFileWebsite (line 56) | function isFileWebsite(
FILE: apps/client-server/src/app/websites/models/website-modifiers/message-website.ts
type MessageWebsite (line 18) | interface MessageWebsite<
function isMessageWebsite (line 33) | function isMessageWebsite(
FILE: apps/client-server/src/app/websites/models/website-modifiers/oauth-website.ts
type OAuthWebsite (line 8) | interface OAuthWebsite<T extends OAuthRoutes> {
FILE: apps/client-server/src/app/websites/models/website-modifiers/with-custom-description-parser.ts
type WithCustomDescriptionParser (line 3) | interface WithCustomDescriptionParser {
function isWithCustomDescriptionParser (line 11) | function isWithCustomDescriptionParser(
FILE: apps/client-server/src/app/websites/models/website-modifiers/with-dynamic-file-size-limits.ts
type DynamicFileSizeLimits (line 3) | type DynamicFileSizeLimits = WebsiteFileOptions['acceptedFileSizes'];
type WithDynamicFileSizeLimits (line 5) | interface WithDynamicFileSizeLimits {
function isWithDynamicFileSizeLimits (line 9) | function isWithDynamicFileSizeLimits(
function getDynamicFileSizeLimits (line 19) | function getDynamicFileSizeLimits(website: unknown) {
FILE: apps/client-server/src/app/websites/models/website-modifiers/with-runtime-description-parser.ts
type WithRuntimeDescriptionParser (line 3) | interface WithRuntimeDescriptionParser {
function isWithRuntimeDescriptionParser (line 7) | function isWithRuntimeDescriptionParser(
FILE: apps/client-server/src/app/websites/website-data-manager.spec.ts
function populateAccount (line 25) | function populateAccount(): Promise<Account> {
FILE: apps/client-server/src/app/websites/website-data-manager.ts
class WebsiteDataManager (line 11) | class WebsiteDataManager<T extends DynamicObject> {
method constructor (line 22) | constructor(userAccount: IAccount) {
method createOrLoadWebsiteData (line 28) | private async createOrLoadWebsiteData() {
method saveData (line 40) | private async saveData() {
method initialize (line 50) | public async initialize(repository: PostyBirbDatabase<'WebsiteDataSche...
method isInitialized (line 58) | public isInitialized(): boolean {
method clearData (line 65) | public async clearData(recreateEntity = true) {
method getData (line 81) | public getData(): T {
method setData (line 93) | public async setData(data: T) {
FILE: apps/client-server/src/app/websites/website-registry.service.ts
type WebsiteInstances (line 29) | type WebsiteInstances = Record<string, Record<string, UnknownWebsite>>;
class WebsiteRegistryService (line 36) | class WebsiteRegistryService {
method constructor (line 54) | constructor(
method emit (line 92) | public async emit() {
method markAsInitialized (line 105) | public markAsInitialized(): void {
method isRegistryInitialized (line 119) | public isRegistryInitialized(): boolean {
method waitForInitialization (line 129) | public async waitForInitialization(timeoutMs?: number): Promise<void> {
method getRepository (line 151) | getRepository() {
method canCreate (line 165) | public canCreate(websiteName: string): boolean {
method create (line 173) | public async create(account: Account): Promise<UnknownWebsite> {
method findInstance (line 204) | public findInstance(account: IAccount): UnknownWebsite | undefined {
method getInstancesOf (line 217) | public getInstancesOf(website: Class<UnknownWebsite>): UnknownWebsite[] {
method getAll (line 231) | public getAll(): UnknownWebsite[] {
method getAvailableWebsites (line 242) | public getAvailableWebsites(): Class<UnknownWebsite>[] {
method getWebsiteInfo (line 250) | public async getWebsiteInfo(): Promise<IWebsiteInfoDto[]> {
method remove (line 284) | public async remove(account: IAccount): Promise<void> {
method performOAuthStep (line 298) | public async performOAuthStep(
FILE: apps/client-server/src/app/websites/website.events.ts
type WebsiteEventTypes (line 5) | type WebsiteEventTypes = WebsiteUpdateEvent;
class WebsiteUpdateEvent (line 7) | class WebsiteUpdateEvent implements WebsocketEvent<IWebsiteInfoDto[]> {
FILE: apps/client-server/src/app/websites/website.spec.ts
function populateAccount (line 33) | function populateAccount(): Promise<Account> {
FILE: apps/client-server/src/app/websites/website.ts
type UnknownWebsite (line 30) | type UnknownWebsite = Website<any>;
method id (line 104) | public get id(): string {
method accountId (line 114) | public get accountId(): string {
method supportsFile (line 124) | public get supportsFile(): boolean {
method supportsMessage (line 134) | public get supportsMessage(): boolean {
method getModelFor (line 141) | getModelFor(type: SubmissionType) {
method createValidator (line 156) | protected createValidator<T extends IWebsiteFormFields = never>() {
method constructor (line 160) | constructor(userAccount: Account) {
method clearLoginStateAndData (line 170) | public async clearLoginStateAndData(forWebsiteDeletion = false) {
method getWebsiteData (line 183) | public getWebsiteData(): D {
method getFormProperties (line 200) | public getFormProperties(): DynamicObject {
method getLoginState (line 212) | public getLoginState() {
method getSupportedTypes (line 219) | public getSupportedTypes(): SubmissionType[] {
method setWebsiteData (line 237) | public async setWebsiteData(data: D) {
method onWebsiteDataChange (line 248) | protected async onWebsiteDataChange(newData: D) {
method login (line 264) | public async login(): Promise<ILoginState> {
method executeLogin (line 297) | private async executeLogin(): Promise<void> {
method onInitialize (line 316) | public async onInitialize(
method cycleCookies (line 326) | private async cycleCookies(): Promise<void> {
method onBeforeLogin (line 344) | protected async onBeforeLogin() {
FILE: apps/client-server/src/app/websites/websites.controller.ts
class WebsitesController (line 15) | class WebsitesController {
method constructor (line 16) | constructor(
method performOAuthStep (line 27) | performOAuthStep(
method getWebsiteLoginInfo (line 35) | getWebsiteLoginInfo() {
method handleInstagramCallback (line 45) | handleInstagramCallback(
FILE: apps/client-server/src/app/websites/websites.module.ts
class WebsitesModule (line 11) | class WebsitesModule {}
FILE: apps/client-server/src/assets/sharp-worker.js
function load (line 33) | function load(buffer) {
function applyOutputFormat (line 85) | function applyOutputFormat(instance, outputMimeType) {
function resizeImage (line 107) | async function resizeImage(inputBuffer, width, height) {
function scaleDownImage (line 173) | async function scaleDownImage(
function encodeAtScale (line 304) | async function encodeAtScale(
function generateThumbnail (line 350) | async function generateThumbnail(
FILE: apps/client-server/src/main.ts
class CustomClassSerializer (line 21) | class CustomClassSerializer extends ClassSerializerInterceptor {
method serialize (line 22) | serialize(
function bootstrap (line 34) | async function bootstrap() {
FILE: apps/postybirb-cloud-server/src/functions/upload.ts
constant CONTAINER_NAME (line 10) | const CONTAINER_NAME = 'instagram';
constant MAX_FILE_SIZE (line 11) | const MAX_FILE_SIZE = 30 * 1024 * 1024;
constant RATE_LIMIT_WINDOW_MS (line 15) | const RATE_LIMIT_WINDOW_MS = 60 * 1000;
constant RATE_LIMIT_MAX (line 16) | const RATE_LIMIT_MAX = 20;
function getRateLimitInfo (line 31) | function getRateLimitInfo(ip: string): {
function getContainerClient (line 60) | function getContainerClient() {
function upload (line 70) | async function upload(
FILE: apps/postybirb-ui/src/api/account.api.ts
class AccountApi (line 12) | class AccountApi extends BaseApi<
method constructor (line 17) | constructor() {
method updateRemoteCookies (line 21) | private async updateRemoteCookies(accountId: AccountId) {
method clear (line 33) | async clear(id: AccountId) {
method setWebsiteData (line 38) | setWebsiteData<T>(request: ISetWebsiteDataRequestDto<T>) {
method refreshLogin (line 42) | async refreshLogin(id: AccountId) {
FILE: apps/postybirb-ui/src/api/base.api.ts
class BaseApi (line 5) | class BaseApi<
method constructor (line 12) | constructor(basePath: string) {
method get (line 16) | public get(id: EntityId) {
method getAll (line 20) | public getAll() {
method create (line 24) | public create(createDto: CreateType) {
method update (line 28) | public update(id: EntityId, updateDto: UpdateType) {
method remove (line 32) | public remove(ids: EntityId[]) {
FILE: apps/postybirb-ui/src/api/custom-shortcut.api.ts
class CustomShortcutsApi (line 8) | class CustomShortcutsApi extends BaseApi<
method constructor (line 13) | constructor() {
FILE: apps/postybirb-ui/src/api/directory-watchers.api.ts
constant FILE_COUNT_WARNING_THRESHOLD (line 8) | const FILE_COUNT_WARNING_THRESHOLD = 10;
type CheckPathResult (line 10) | interface CheckPathResult {
class DirectoryWatchersApi (line 17) | class DirectoryWatchersApi extends BaseApi<
method constructor (line 22) | constructor() {
method checkPath (line 26) | public checkPath(path: string) {
FILE: apps/postybirb-ui/src/api/file-submission.api.ts
type FileUpdateTarget (line 10) | type FileUpdateTarget = 'file' | 'thumbnail';
class FileSubmissionsApi (line 12) | class FileSubmissionsApi {
method appendFiles (line 15) | appendFiles(id: SubmissionId, target: FileUpdateTarget, files: Blob[]) {
method replaceFile (line 21) | replaceFile(
method removeFile (line 34) | removeFile(id: SubmissionId, fileId: EntityId, target: FileUpdateTarge...
method getAltText (line 40) | getAltText(id: EntityId) {
method updateAltText (line 44) | updateAltText(altFileId: EntityId, text: string) {
method updateMetadata (line 48) | updateMetadata(id: EntityId, update: SubmissionFileMetadata) {
method reorder (line 52) | reorder(update: IReorderSubmissionFilesDto) {
function getRemoveFileUrl (line 59) | function getRemoveFileUrl(
function getReplaceFileUrl (line 67) | function getReplaceFileUrl(
function getAppendFileUrl (line 75) | function getAppendFileUrl(
FILE: apps/postybirb-ui/src/api/form-generator.api.ts
class FormGeneratorApi (line 5) | class FormGeneratorApi {
method getForm (line 8) | getForm(dto: IFormGenerationRequestDto) {
FILE: apps/postybirb-ui/src/api/legacy-database-importer.api.ts
type LegacyImportDto (line 3) | interface LegacyImportDto {
type LegacyImportResponse (line 11) | interface LegacyImportResponse {
class LegacyDatabaseImporterApi (line 15) | class LegacyDatabaseImporterApi {
method constructor (line 18) | constructor() {
method import (line 22) | public import(importRequest: LegacyImportDto) {
FILE: apps/postybirb-ui/src/api/notification.api.ts
class NotificationApi (line 8) | class NotificationApi extends BaseApi<
method constructor (line 13) | constructor() {
FILE: apps/postybirb-ui/src/api/post-manager.api.ts
class PostManagerApi (line 4) | class PostManagerApi {
method constructor (line 7) | constructor() {
method cancelIfRunning (line 11) | cancelIfRunning(submissionId: SubmissionId) {
method isPosting (line 15) | isPosting(submissionType: SubmissionType) {
FILE: apps/postybirb-ui/src/api/post-queue.api.ts
class PostQueueApi (line 4) | class PostQueueApi extends BaseApi<
method constructor (line 9) | constructor() {
method enqueue (line 13) | enqueue(submissionIds: string[], resumeMode?: PostRecordResumeMode) {
method dequeue (line 17) | dequeue(submissionIds: string[]) {
method getAll (line 21) | getAll() {
method isPaused (line 25) | isPaused() {
method pause (line 29) | pause() {
method resume (line 33) | resume() {
FILE: apps/postybirb-ui/src/api/post.api.ts
class PostApi (line 4) | class PostApi extends BaseApi<
method constructor (line 9) | constructor() {
FILE: apps/postybirb-ui/src/api/remote.api.ts
class RemoteApi (line 9) | class RemoteApi {
method testPing (line 15) | async testPing() {
method setCookies (line 46) | async setCookies(accountId: AccountId) {
FILE: apps/postybirb-ui/src/api/settings.api.ts
class SettingsApi (line 5) | class SettingsApi {
method getAll (line 9) | getAll() {
method getStartupOptions (line 13) | getStartupOptions() {
method update (line 17) | update(id: EntityId, dto: IUpdateSettingsDto) {
method updateSystemStartupSettings (line 21) | updateSystemStartupSettings(
FILE: apps/postybirb-ui/src/api/submission.api.ts
type CreateFileSubmissionOptions (line 19) | interface CreateFileSubmissionOptions {
type ApplyTemplateOptionsDto (line 33) | interface ApplyTemplateOptionsDto {
type ApplyTemplateOptionsResult (line 50) | interface ApplyTemplateOptionsResult {
class SubmissionsApi (line 56) | class SubmissionsApi extends BaseApi<
method constructor (line 61) | constructor() {
method createMessageSubmission (line 65) | createMessageSubmission(name: string) {
method duplicate (line 72) | duplicate(id: SubmissionId) {
method updateTemplateName (line 76) | updateTemplateName(id: SubmissionId, dto: IUpdateSubmissionTemplateNam...
method createFileSubmission (line 88) | createFileSubmission(
method reorder (line 119) | reorder(id: SubmissionId, targetId: SubmissionId, position: 'before' |...
method applyToMultipleSubmissions (line 123) | applyToMultipleSubmissions(dto: IApplyMultiSubmissionDto) {
method applyTemplate (line 127) | applyTemplate(id: SubmissionId, templateId: SubmissionId) {
method applyTemplateOptions (line 131) | applyTemplateOptions(dto: ApplyTemplateOptionsDto) {
method unarchive (line 138) | unarchive(id: SubmissionId) {
method archive (line 142) | archive(id: SubmissionId) {
FILE: apps/postybirb-ui/src/api/tag-converters.api.ts
class TagConvertersApi (line 8) | class TagConvertersApi extends BaseApi<
method constructor (line 13) | constructor() {
FILE: apps/postybirb-ui/src/api/tag-groups.api.ts
class TagGroupsApi (line 8) | class TagGroupsApi extends BaseApi<
method constructor (line 13) | constructor() {
FILE: apps/postybirb-ui/src/api/update.api.ts
class UpdateApi (line 4) | class UpdateApi {
method checkForUpdates (line 7) | checkForUpdates() {
method startUpdate (line 11) | startUpdate() {
method installUpdate (line 15) | installUpdate() {
FILE: apps/postybirb-ui/src/api/user-converters.api.ts
class UserConvertersApi (line 8) | class UserConvertersApi extends BaseApi<
method constructor (line 13) | constructor() {
FILE: apps/postybirb-ui/src/api/user-specified-website-options.api.ts
class UserSpecifiedWebsiteOptionsApi (line 8) | class UserSpecifiedWebsiteOptionsApi extends BaseApi<
method constructor (line 13) | constructor() {
FILE: apps/postybirb-ui/src/api/website-options.api.ts
class WebsiteOptionsApi (line 14) | class WebsiteOptionsApi extends BaseApi<
method constructor (line 19) | constructor() {
method validate (line 23) | validate(dto: IValidateWebsiteOptionsDto) {
method validateSubmission (line 27) | validateSubmission(submissionId: SubmissionId) {
method previewDescription (line 31) | previewDescription(dto: IPreviewDescriptionDto) {
method updateSubmissionOptions (line 38) | updateSubmissionOptions(
method modifySubmission (line 45) | modifySubmission(
FILE: apps/postybirb-ui/src/api/websites.api.ts
class WebsitesApi (line 9) | class WebsitesApi {
method getWebsiteInfo (line 12) | getWebsiteInfo() {
method performOAuthStep (line 16) | async performOAuthStep<T extends OAuthRoutes, R extends keyof T = keyo...
FILE: apps/postybirb-ui/src/app-insights-ui.ts
function initializeAppInsightsUI (line 10) | function initializeAppInsightsUI(): void {
function trackUIException (line 78) | function trackUIException(
function trackUIEvent (line 90) | function trackUIEvent(
function trackUIPageView (line 102) | function trackUIPageView(name?: string, uri?: string): void {
function getAppInsightsUI (line 111) | function getAppInsightsUI(): ApplicationInsights | null {
FILE: apps/postybirb-ui/src/components/confirm-action-modal/confirm-action-modal.tsx
type ConfirmActionModalProps (line 16) | interface ConfirmActionModalProps {
function ConfirmActionModal (line 41) | function ConfirmActionModal({
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/app-settings-section.tsx
function AppSettingsSection (line 21) | function AppSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/appearance-settings-section.tsx
constant COLOR_SCHEME_OPTIONS (line 28) | const COLOR_SCHEME_OPTIONS = [
function capitalize (line 67) | function capitalize(str: string): string {
function AppearanceSettingsSection (line 71) | function AppearanceSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/data-settings-section.tsx
function DataSettingsSection (line 15) | function DataSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/description-settings-section.tsx
function DescriptionSettingsSection (line 10) | function DescriptionSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/import-settings-section.tsx
function ImportSettingsSection (line 11) | function ImportSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/notifications-settings-section.tsx
function NotificationsSettingsSection (line 11) | function NotificationsSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/remote-settings-section.tsx
function RemoteSettingsSection (line 40) | function RemoteSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/spellchecker-settings-section.tsx
function SpellcheckerSettings (line 16) | function SpellcheckerSettings() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/sections/tags-settings-section.tsx
function TagsSettingsSection (line 10) | function TagsSettingsSection() {
FILE: apps/postybirb-ui/src/components/dialogs/settings-dialog/settings-dialog.tsx
type SettingsSection (line 48) | type SettingsSection =
type NavItem (line 59) | interface NavItem {
constant NAV_ITEMS (line 69) | const NAV_ITEMS: NavItem[] = [
function SettingsDialog (line 121) | function SettingsDialog() {
FILE: apps/postybirb-ui/src/components/disclaimer/disclaimer.tsx
type DisclaimerProps (line 16) | type DisclaimerProps = {
constant DISCLAIMER_KEY (line 21) | const DISCLAIMER_KEY = 'pb_disclaimer_accepted';
function attemptAppQuit (line 24) | function attemptAppQuit(): boolean {
function DisclaimerDisplay (line 52) | function DisclaimerDisplay({ onAccepted, onDeclined }: DisclaimerProps) {
function Disclaimer (line 142) | function Disclaimer({ children }: PropsWithChildren<{}>) {
FILE: apps/postybirb-ui/src/components/drawers/converter-drawer/converter-drawer.tsx
type ConverterRecord (line 58) | interface ConverterRecord extends BaseRecord {
type ConverterApi (line 66) | interface ConverterApi<TCreateDto, TUpdateDto> {
type ConverterDrawerConfig (line 75) | interface ConverterDrawerConfig<
type WebsiteOption (line 104) | interface WebsiteOption {
function useConverterSearch (line 116) | function useConverterSearch<TRecord extends ConverterRecord>(
function WebsiteConversionRow (line 161) | function WebsiteConversionRow({
function AddWebsiteDropdown (line 219) | function AddWebsiteDropdown({
function WebsiteConversionsEditor (line 296) | function WebsiteConversionsEditor({
function DeleteSelectedButton (line 524) | function DeleteSelectedButton({
function CreateConverterForm (line 572) | function CreateConverterForm({
type ConverterDrawerProps (line 644) | interface ConverterDrawerProps<
function ConverterDrawer (line 663) | function ConverterDrawer<
FILE: apps/postybirb-ui/src/components/drawers/custom-shortcuts-drawer/custom-shortcuts-drawer.tsx
type CustomShortcutsDrawerProps (line 54) | interface CustomShortcutsDrawerProps {
function useShortcutSearch (line 68) | function useShortcutSearch(
type ShortcutCardProps (line 100) | interface ShortcutCardProps {
type CreateShortcutFormProps (line 237) | interface CreateShortcutFormProps {
function CreateShortcutForm (line 243) | function CreateShortcutForm({
function CustomShortcutsDrawer (line 322) | function CustomShortcutsDrawer({
FILE: apps/postybirb-ui/src/components/drawers/drawers.tsx
function CustomShortcutsDrawer (line 41) | function CustomShortcutsDrawer() {
FILE: apps/postybirb-ui/src/components/drawers/file-watcher-drawer/file-watcher-drawer.tsx
constant DRAWER_KEY (line 52) | const DRAWER_KEY = 'fileWatchers' as const;
type FolderConfirmModalProps (line 58) | interface FolderConfirmModalProps {
function FolderConfirmModal (line 69) | function FolderConfirmModal({
type FileWatcherCardProps (line 114) | interface FileWatcherCardProps {
function FileWatcherCard (line 121) | function FileWatcherCard({ watcher }: FileWatcherCardProps) {
function CreateWatcherButton (line 324) | function CreateWatcherButton() {
type WatcherListProps (line 360) | interface WatcherListProps {
function WatcherList (line 367) | function WatcherList({ watchers }: WatcherListProps) {
function FileWatcherDrawer (line 399) | function FileWatcherDrawer() {
function FileWatcherDrawerContent (line 411) | function FileWatcherDrawerContent({ onClose }: { onClose: () => void }) {
FILE: apps/postybirb-ui/src/components/drawers/notifications-drawer/notifications-drawer.tsx
type ReadFilterValue (line 56) | type ReadFilterValue = 'all' | 'unread' | 'read';
type TypeFilterValue (line 57) | type TypeFilterValue = 'all' | 'error' | 'warning' | 'success' | 'info';
function getTypeColor (line 66) | function getTypeColor(type: NotificationRecord['type']): string {
function getTypeIcon (line 83) | function getTypeIcon(type: NotificationRecord['type']): React.ReactNode {
function useNotificationFilters (line 105) | function useNotificationFilters() {
function ReadStatusFilter (line 151) | function ReadStatusFilter({
function TypeFilter (line 191) | function TypeFilter({
function BulkActions (line 257) | function BulkActions({
function NotificationList (line 446) | function NotificationList({
function NotificationsDrawer (line 499) | function NotificationsDrawer() {
function NotificationsDrawerContent (line 511) | function NotificationsDrawerContent({ onClose }: { onClose: () => void }) {
FILE: apps/postybirb-ui/src/components/drawers/schedule-drawer/schedule-calendar.tsx
function ScheduleCalendar (line 47) | function ScheduleCalendar() {
FILE: apps/postybirb-ui/src/components/drawers/schedule-drawer/schedule-drawer.tsx
function ScheduleDrawer (line 20) | function ScheduleDrawer() {
FILE: apps/postybirb-ui/src/components/drawers/schedule-drawer/submission-list.tsx
function DraggableSubmissionItem (line 28) | function DraggableSubmissionItem({
function SubmissionList (line 65) | function SubmissionList() {
FILE: apps/postybirb-ui/src/components/drawers/section-drawer.tsx
type SectionDrawerProps (line 15) | interface SectionDrawerProps {
function SectionDrawer (line 36) | function SectionDrawer({
FILE: apps/postybirb-ui/src/components/drawers/tag-converter-drawer/tag-converter-drawer.tsx
constant DRAWER_KEY (line 27) | const DRAWER_KEY = 'tagConverters';
function TagConverterDrawer (line 33) | function TagConverterDrawer() {
function TagConverterDrawerContent (line 45) | function TagConverterDrawerContent({ onClose }: { onClose: () => void }) {
FILE: apps/postybirb-ui/src/components/drawers/tag-group-drawer/tag-group-drawer.tsx
function useTagGroupSearch (line 44) | function useTagGroupSearch(searchQuery: string) {
function EditableNameCell (line 76) | function EditableNameCell({
function EditableTagsCell (line 141) | function EditableTagsCell({
function DeleteSelectedButton (line 188) | function DeleteSelectedButton({
function CreateTagGroupForm (line 232) | function CreateTagGroupForm() {
function TagGroupsTable (line 327) | function TagGroupsTable({
function TagGroupDrawer (line 386) | function TagGroupDrawer() {
function TagGroupDrawerContent (line 398) | function TagGroupDrawerContent({ onClose }: { onClose: () => void }) {
FILE: apps/postybirb-ui/src/components/drawers/user-converter-drawer/user-converter-drawer.tsx
constant DRAWER_KEY (line 27) | const DRAWER_KEY = 'userConverters';
function UserConverterDrawer (line 33) | function UserConverterDrawer() {
function UserConverterDrawerContent (line 45) | function UserConverterDrawerContent({ onClose }: { onClose: () => void }) {
FILE: apps/postybirb-ui/src/components/empty-state/empty-state.tsx
type EmptyStatePreset (line 14) | type EmptyStatePreset =
type EmptyStateProps (line 20) | interface EmptyStateProps {
function getPresetIcon (line 36) | function getPresetIcon(preset: EmptyStatePreset): ReactNode {
function getPresetMessage (line 54) | function getPresetMessage(preset: EmptyStatePreset): ReactNode {
function getSizes (line 71) | function getSizes(size: 'sm' | 'md' | 'lg') {
function EmptyState (line 103) | function EmptyState({
FILE: apps/postybirb-ui/src/components/error-boundary/error-boundary.tsx
function CopyableErrorDetails (line 26) | function CopyableErrorDetails({
type ErrorBoundaryState (line 122) | interface ErrorBoundaryState {
type ErrorBoundaryProps (line 128) | interface ErrorBoundaryProps {
class ErrorBoundary (line 141) | class ErrorBoundary extends Component<
method constructor (line 147) | constructor(props: ErrorBoundaryProps) {
method getDerivedStateFromError (line 152) | static getDerivedStateFromError(error: Error): ErrorBoundaryState {
method componentDidCatch (line 156) | componentDidCatch(error: Error, errorInfo: { componentStack: string }) {
method componentDidUpdate (line 192) | componentDidUpdate(prevProps: ErrorBoundaryProps) {
method render (line 227) | render() {
method renderDefaultFallback (line 244) | private renderDefaultFallback(
FILE: apps/postybirb-ui/src/components/error-boundary/specialized-error-boundaries.tsx
function FormErrorFallback (line 15) | function FormErrorFallback({
function PageErrorBoundary (line 55) | function PageErrorBoundary({ children }: { children: ReactNode }) {
function FormErrorBoundary (line 73) | function FormErrorBoundary({
function ComponentErrorBoundary (line 98) | function ComponentErrorBoundary({ children }: { children: ReactNode }) {
function RouteErrorBoundary (line 115) | function RouteErrorBoundary({
FILE: apps/postybirb-ui/src/components/hold-to-confirm/hold-to-confirm.tsx
constant DEFAULT_HOLD_DURATION (line 10) | const DEFAULT_HOLD_DURATION = 1000;
constant PROGRESS_INTERVAL (line 12) | const PROGRESS_INTERVAL = 16;
type UseHoldToConfirmOptions (line 14) | interface UseHoldToConfirmOptions {
type UseHoldToConfirmReturn (line 23) | interface UseHoldToConfirmReturn {
function useHoldToConfirm (line 37) | function useHoldToConfirm({
type HoldToConfirmButtonProps (line 86) | interface HoldToConfirmButtonProps
FILE: apps/postybirb-ui/src/components/language-picker/language-picker.tsx
type LanguagePickerProps (line 27) | interface LanguagePickerProps {
function LanguagePicker (line 37) | function LanguagePicker({ collapsed = false, kbd }: LanguagePickerProps) {
FILE: apps/postybirb-ui/src/components/layout/content-area.tsx
function ContentArea (line 14) | function ContentArea({ children, loading = false }: ContentAreaProps) {
FILE: apps/postybirb-ui/src/components/layout/content-navbar.tsx
function ContentNavbar (line 14) | function ContentNavbar({ config, onPageChange }: ContentNavbarProps) {
FILE: apps/postybirb-ui/src/components/layout/layout.tsx
function Layout (line 36) | function Layout() {
FILE: apps/postybirb-ui/src/components/layout/primary-content.tsx
type PrimaryContentProps (line 16) | interface PrimaryContentProps {
function ViewContent (line 27) | function ViewContent({ viewState }: PrimaryContentProps) {
function PrimaryContent (line 61) | function PrimaryContent({
FILE: apps/postybirb-ui/src/components/layout/section-panel.tsx
type SectionPanelProps (line 15) | interface SectionPanelProps {
function SectionContent (line 23) | function SectionContent({ viewState }: SectionPanelProps) {
function SectionPanel (line 52) | function SectionPanel({ viewState }: SectionPanelProps) {
FILE: apps/postybirb-ui/src/components/layout/side-nav.tsx
function TourButton (line 33) | function TourButton({ collapsed }: { collapsed: boolean }) {
function NavItemRenderer (line 60) | function NavItemRenderer({
function SideNav (line 166) | function SideNav({ items, collapsed, onCollapsedChange }: SideNavProps) {
FILE: apps/postybirb-ui/src/components/onboarding-tour/mantine-tooltip.tsx
function MantineTooltip (line 19) | function MantineTooltip({
FILE: apps/postybirb-ui/src/components/onboarding-tour/tour-provider.tsx
function useTourSteps (line 28) | function useTourSteps(tourId: string | null) {
function TourProvider (line 77) | function TourProvider({ children }: { children: React.ReactNode }) {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/accounts-tour.tsx
constant ACCOUNTS_TOUR_ID (line 10) | const ACCOUNTS_TOUR_ID = 'accounts';
function useAccountsTourSteps (line 15) | function useAccountsTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/custom-shortcuts-tour.tsx
constant CUSTOM_SHORTCUTS_TOUR_ID (line 10) | const CUSTOM_SHORTCUTS_TOUR_ID = 'custom-shortcuts';
function useCustomShortcutsTourSteps (line 15) | function useCustomShortcutsTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/file-watchers-tour.tsx
constant FILE_WATCHERS_TOUR_ID (line 10) | const FILE_WATCHERS_TOUR_ID = 'file-watchers';
function useFileWatchersTourSteps (line 15) | function useFileWatchersTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/home-tour.tsx
constant HOME_TOUR_ID (line 10) | const HOME_TOUR_ID = 'home';
function useHomeTourSteps (line 15) | function useHomeTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/layout-tour.tsx
function useLayoutTourSteps (line 14) | function useLayoutTourSteps(): Step[] {
constant LAYOUT_TOUR_ID (line 257) | const LAYOUT_TOUR_ID = 'layout';
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/notifications-tour.tsx
constant NOTIFICATIONS_TOUR_ID (line 10) | const NOTIFICATIONS_TOUR_ID = 'notifications';
function useNotificationsTourSteps (line 15) | function useNotificationsTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/schedule-tour.tsx
constant SCHEDULE_TOUR_ID (line 10) | const SCHEDULE_TOUR_ID = 'schedule';
function useScheduleTourSteps (line 15) | function useScheduleTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/submission-edit-tour.tsx
constant SUBMISSION_EDIT_TOUR_ID (line 10) | const SUBMISSION_EDIT_TOUR_ID = 'submission-edit';
function useSubmissionEditTourSteps (line 15) | function useSubmissionEditTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/submissions-tour.tsx
constant SUBMISSIONS_TOUR_ID (line 11) | const SUBMISSIONS_TOUR_ID = 'submissions';
function useSubmissionsTourSteps (line 16) | function useSubmissionsTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/tag-converters-tour.tsx
constant TAG_CONVERTERS_TOUR_ID (line 10) | const TAG_CONVERTERS_TOUR_ID = 'tag-converters';
function useTagConvertersTourSteps (line 15) | function useTagConvertersTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/tag-groups-tour.tsx
constant TAG_GROUPS_TOUR_ID (line 10) | const TAG_GROUPS_TOUR_ID = 'tag-groups';
function useTagGroupsTourSteps (line 15) | function useTagGroupsTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/templates-tour.tsx
constant TEMPLATES_TOUR_ID (line 10) | const TEMPLATES_TOUR_ID = 'templates';
function useTemplatesTourSteps (line 15) | function useTemplatesTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/onboarding-tour/tours/user-converters-tour.tsx
constant USER_CONVERTERS_TOUR_ID (line 10) | const USER_CONVERTERS_TOUR_ID = 'user-converters';
function useUserConvertersTourSteps (line 15) | function useUserConvertersTourSteps(): Step[] {
FILE: apps/postybirb-ui/src/components/sections/accounts-section/account-section-header.tsx
function AccountSectionHeader (line 27) | function AccountSectionHeader() {
FILE: apps/postybirb-ui/src/components/sections/accounts-section/accounts-content.tsx
type AccountsContentProps (line 28) | interface AccountsContentProps {
type AccountHeaderProps (line 36) | interface AccountHeaderProps {
function AccountHeader (line 42) | function AccountHeader({
type UserLoginContentProps (line 83) | interface UserLoginContentPro
Condensed preview — 1230 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (4,980K chars).
[
{
"path": ".editorconfig",
"chars": 245,
"preview": "# Editor configuration, see http://editorconfig.org\nroot = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = "
},
{
"path": ".eslintignore",
"chars": 89,
"preview": "# Add files here to ignore them in eslint\n\nnode_modules\n*.js\n*.jsx\n*.spec.ts \n*.spec.tsx\n"
},
{
"path": ".eslintrc.js",
"chars": 2412,
"preview": "// @ts-check\n/** @type {import('eslint').ESLint.ConfigData} */\nconst config = {\n root: true,\n ignorePatterns: ['**/*']"
},
{
"path": ".github/workflows/build.yml",
"chars": 5993,
"preview": "name: Build/Release\n\non:\n workflow_dispatch:\n\njobs:\n build:\n name: ${{ matrix.name }}\n runs-on: ${{ matrix.os }}"
},
{
"path": ".github/workflows/ci.yml",
"chars": 2716,
"preview": "name: CI\non:\n workflow_dispatch:\n pull_request:\n push:\n branches:\n - main\n\n# Needed for nx-set-shas when run "
},
{
"path": ".github/workflows/i18n.yml.disabled",
"chars": 1390,
"preview": "name: i18n CI\non:\n push:\n branches:\n - main\n\npermissions:\n actions: read\n contents: write\n checks: write\n p"
},
{
"path": ".gitignore",
"chars": 717,
"preview": "# See http://help.github.com/ignore-files/ for more about ignoring files.\n\n# yarn\n.yarn/*\n!.yarn/patches\n!.yarn/plugins\n"
},
{
"path": ".husky/commit-msg",
"chars": 40,
"preview": "npx --no-install commitlint --edit \"$1\"\n"
},
{
"path": ".husky/post-merge",
"chars": 5,
"preview": "yarn\n"
},
{
"path": ".prettierignore",
"chars": 123,
"preview": "# Add files here to ignore them from prettier formatting\n\n/dist/**\n/coverage/**\n/.nx/cache/**\n/.nx/workspace-data/**\n\n*."
},
{
"path": ".prettierrc",
"chars": 49,
"preview": "{\n \"singleQuote\": true,\n \"endOfLine\": \"crlf\"\n}\n"
},
{
"path": ".vscode/extensions.json",
"chars": 154,
"preview": "{\n \"recommendations\": [\n \"nrwl.angular-console\",\n \"esbenp.prettier-vscode\",\n \"firsttris.vscode-jest-runner\",\n "
},
{
"path": ".vscode/launch.json",
"chars": 1895,
"preview": "{\n \"version\": \"0.2.0\",\n \"configurations\": [\n {\n \"name\": \"Debug Node App\",\n \"type\": \"node\",\n \"request"
},
{
"path": ".vscode/settings.json",
"chars": 80,
"preview": "{\n \"editor.codeActionsOnSave\": {\n \"source.organizeImports\": \"always\"\n }\n}\n\n"
},
{
"path": ".yarn/patches/@handlewithcare-prosemirror-inputrules-npm-0.1.3-897e37b56f.patch",
"chars": 341,
"preview": "diff --git a/package.json b/package.json\nindex 20bc41090c166b7b27756b8d8e0fb67525d48f38..a539a8e91499f831bd77b4fa43e98f3"
},
{
"path": ".yarn/patches/@tiptap-html-npm-3.15.3-a9641901db.patch",
"chars": 939,
"preview": "diff --git a/package.json b/package.json\nindex 1d1aa65a70e7f3e898be608610693a9b98152b8c..e27af24e756e7c6b76651d504304f28"
},
{
"path": ".yarn/patches/jest-snapshot-npm-29.7.0-15ef0a4ad6.patch",
"chars": 505,
"preview": "diff --git a/build/InlineSnapshots.js b/build/InlineSnapshots.js\nindex 3481ad99885c847156afdef148d3075dcc9c68ca..44c91da"
},
{
"path": ".yarn/patches/strong-log-transformer-npm-2.1.0-45addd9278.patch",
"chars": 490,
"preview": "diff --git a/lib/logger.js b/lib/logger.js\nindex 69218a870e6022289a73c27cb2d4f166bf1691d9..8d6554bce9dd8892c945ef0740b0e"
},
{
"path": ".yarnrc.yml",
"chars": 471,
"preview": "logFilters:\n # NX packages peer dependencies make a lot of warnings that does not affect code in any way\n - code: YN00"
},
{
"path": "Dockerfile",
"chars": 1454,
"preview": "# Multi stage build to make image smaller\nFROM node:24-bookworm-slim AS builder\n\n# For ca-certificates\nRUN apt-get updat"
},
{
"path": "LICENSE",
"chars": 1523,
"preview": "BSD 3-Clause License\n\nCopyright (c) 2024, Michael DiCarlo\nAll rights reserved.\n\nRedistribution and use in source and bin"
},
{
"path": "README.md",
"chars": 3275,
"preview": "# Postybirb\n\n<div style='flex: 1'>\n<a href=\"https://discord.com/invite/FUdN7JCr2f\">\n<img alt=\"Static Badge\" src=\"https:/"
},
{
"path": "TRANSLATION.md",
"chars": 440,
"preview": "# Translation guide\n\n## How to contribute to translation\n\nGo to [PostyBirb site](https://hosted.weblate.org/projects/pos"
},
{
"path": "apps/client-server/.eslintrc.json",
"chars": 304,
"preview": "{\n \"extends\": [\"../../.eslintrc.js\"],\n \"ignorePatterns\": [\"!**/*\"],\n \"overrides\": [\n {\n \"files\": [\"*.ts\", \"*."
},
{
"path": "apps/client-server/jest.config.ts",
"chars": 300,
"preview": "/* eslint-disable */\nexport default {\n displayName: 'client-server',\n preset: '../../jest.preset.js',\n globals: {},\n "
},
{
"path": "apps/client-server/project.json",
"chars": 1590,
"preview": "{\n \"name\": \"client-server\",\n \"$schema\": \"../../node_modules/nx/schemas/project-schema.json\",\n \"sourceRoot\": \"apps/cli"
},
{
"path": "apps/client-server/src/app/account/account.controller.ts",
"chars": 2268,
"preview": "import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';\nimport {\n ApiBadRequestResponse,\n ApiNotFo"
},
{
"path": "apps/client-server/src/app/account/account.events.ts",
"chars": 363,
"preview": "import { ACCOUNT_UPDATES } from '@postybirb/socket-events';\nimport { IAccountDto } from '@postybirb/types';\nimport { Web"
},
{
"path": "apps/client-server/src/app/account/account.module.ts",
"chars": 382,
"preview": "import { Module } from '@nestjs/common';\nimport { WebsitesModule } from '../websites/websites.module';\nimport { AccountC"
},
{
"path": "apps/client-server/src/app/account/account.service.spec.ts",
"chars": 10637,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase } from '@postybirb/database';\nimport { NUL"
},
{
"path": "apps/client-server/src/app/account/account.service.ts",
"chars": 9799,
"preview": "import {\n BadRequestException,\n Injectable,\n OnModuleInit,\n Optional,\n} from '@nestjs/common';\nimport { Cron, CronEx"
},
{
"path": "apps/client-server/src/app/account/dtos/create-account.dto.ts",
"chars": 449,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { ICreateAccountDto } from '@postybirb/types';\nimport { IsArray, I"
},
{
"path": "apps/client-server/src/app/account/dtos/set-website-data-request.dto.ts",
"chars": 419,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n AccountId,\n DynamicObject,\n ISetWebsiteDataRequestDto,\n} fro"
},
{
"path": "apps/client-server/src/app/account/dtos/update-account.dto.ts",
"chars": 358,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { IUpdateAccountDto } from '@postybirb/types';\nimport { IsArray, I"
},
{
"path": "apps/client-server/src/app/account/login-state-poller.ts",
"chars": 2989,
"preview": "import { Logger } from '@postybirb/logger';\nimport { AccountId, ILoginState } from '@postybirb/types';\nimport { UnknownW"
},
{
"path": "apps/client-server/src/app/app.controller.ts",
"chars": 265,
"preview": "import { Controller, Get } from '@nestjs/common';\n\nimport { AppService } from './app.service';\n\n@Controller()\nexport cla"
},
{
"path": "apps/client-server/src/app/app.module.ts",
"chars": 3073,
"preview": "import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';\n\nimport { ScheduleModule } from '@nestjs/schedu"
},
{
"path": "apps/client-server/src/app/app.service.ts",
"chars": 279,
"preview": "import { Injectable } from '@nestjs/common';\nimport { app } from 'electron';\n\n@Injectable()\nexport class AppService {\n "
},
{
"path": "apps/client-server/src/app/common/controller/postybirb-controller.ts",
"chars": 1224,
"preview": "import { Delete, Get, Param, Query } from '@nestjs/common';\nimport { ApiOkResponse } from '@nestjs/swagger';\nimport { Sc"
},
{
"path": "apps/client-server/src/app/common/service/postybirb-service.ts",
"chars": 2383,
"preview": "import { BadRequestException, Injectable } from '@nestjs/common';\nimport { SchemaKey } from '@postybirb/database';\nimpor"
},
{
"path": "apps/client-server/src/app/constants.ts",
"chars": 88,
"preview": "// Constant variables\nexport const WEBSITE_IMPLEMENTATIONS = 'WEBSITE_IMPLEMENTATIONS';\n"
},
{
"path": "apps/client-server/src/app/custom-shortcuts/custom-shortcut.events.ts",
"chars": 400,
"preview": "import { CUSTOM_SHORTCUT_UPDATES } from '@postybirb/socket-events';\nimport { ICustomShortcut } from '@postybirb/types';\n"
},
{
"path": "apps/client-server/src/app/custom-shortcuts/custom-shortcuts.controller.ts",
"chars": 1236,
"preview": "import { Body, Controller, Param, Patch, Post } from '@nestjs/common';\nimport { ApiOkResponse, ApiTags } from '@nestjs/s"
},
{
"path": "apps/client-server/src/app/custom-shortcuts/custom-shortcuts.module.ts",
"chars": 357,
"preview": "import { Module } from '@nestjs/common';\nimport { CustomShortcutsController } from './custom-shortcuts.controller';\nimpo"
},
{
"path": "apps/client-server/src/app/custom-shortcuts/custom-shortcuts.service.ts",
"chars": 1943,
"preview": "import { Injectable, Optional } from '@nestjs/common';\nimport { CUSTOM_SHORTCUT_UPDATES } from '@postybirb/socket-events"
},
{
"path": "apps/client-server/src/app/custom-shortcuts/dtos/create-custom-shortcut.dto.ts",
"chars": 213,
"preview": "import { ICreateCustomShortcutDto } from '@postybirb/types';\nimport { IsString } from 'class-validator';\n\nexport class C"
},
{
"path": "apps/client-server/src/app/custom-shortcuts/dtos/update-custom-shortcut.dto.ts",
"chars": 276,
"preview": "import { Description, IUpdateCustomShortcutDto } from '@postybirb/types';\nimport { IsObject, IsString } from 'class-vali"
},
{
"path": "apps/client-server/src/app/directory-watchers/directory-watcher.events.ts",
"chars": 436,
"preview": "import { DIRECTORY_WATCHER_UPDATES } from '@postybirb/socket-events';\nimport { DirectoryWatcherDto } from '@postybirb/ty"
},
{
"path": "apps/client-server/src/app/directory-watchers/directory-watchers.controller.ts",
"chars": 1782,
"preview": "import { Body, Controller, Param, Patch, Post } from '@nestjs/common';\nimport {\n ApiBadRequestResponse,\n ApiNotFou"
},
{
"path": "apps/client-server/src/app/directory-watchers/directory-watchers.module.ts",
"chars": 531,
"preview": "import { Module } from '@nestjs/common';\nimport { NotificationsModule } from '../notifications/notifications.module';\nim"
},
{
"path": "apps/client-server/src/app/directory-watchers/directory-watchers.service.spec.ts",
"chars": 6785,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase } from '@postybirb/database';\nimport { Dir"
},
{
"path": "apps/client-server/src/app/directory-watchers/directory-watchers.service.ts",
"chars": 14244,
"preview": "import { BadRequestException, Injectable, Optional } from '@nestjs/common';\nimport { Cron, CronExpression } from '@nestj"
},
{
"path": "apps/client-server/src/app/directory-watchers/dtos/check-path.dto.ts",
"chars": 243,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { IsNotEmpty, IsString } from 'class-validator';\n\nexport class Che"
},
{
"path": "apps/client-server/src/app/directory-watchers/dtos/create-directory-watcher.dto.ts",
"chars": 503,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n DirectoryWatcherImportAction,\n ICreateDirectoryWatcherDto,\n} "
},
{
"path": "apps/client-server/src/app/directory-watchers/dtos/update-directory-watcher.dto.ts",
"chars": 673,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n DirectoryWatcherImportAction,\n IUpdateDirectoryWatcherDto,\n "
},
{
"path": "apps/client-server/src/app/drizzle/models/account.entity.ts",
"chars": 1622,
"preview": "import { IAccount, IAccountDto } from '@postybirb/types';\nimport { Exclude, instanceToPlain, Type } from 'class-transfor"
},
{
"path": "apps/client-server/src/app/drizzle/models/custom-shortcut.entity.ts",
"chars": 755,
"preview": "import {\n DefaultDescription,\n Description,\n ICustomShortcut,\n ICustomShortcutDto,\n} from '@postybirb/types';\nimport"
},
{
"path": "apps/client-server/src/app/drizzle/models/database-entity.spec.ts",
"chars": 1910,
"preview": "import { IEntity, IEntityDto } from '@postybirb/types';\nimport { instanceToPlain } from 'class-transformer';\nimport 'ref"
},
{
"path": "apps/client-server/src/app/drizzle/models/database-entity.ts",
"chars": 1266,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { EntityId, IEntity, IEntityDto } from '@postybirb/types'"
},
{
"path": "apps/client-server/src/app/drizzle/models/directory-watcher.entity.ts",
"chars": 760,
"preview": "import {\n DirectoryWatcherDto,\n DirectoryWatcherImportAction,\n IDirectoryWatcher,\n SubmissionId,\n} from '@postybirb/"
},
{
"path": "apps/client-server/src/app/drizzle/models/file-buffer.entity.ts",
"chars": 783,
"preview": "import { EntityId, FileBufferDto, IFileBuffer } from '@postybirb/types';\nimport { Exclude, instanceToPlain, Type } from "
},
{
"path": "apps/client-server/src/app/drizzle/models/index.ts",
"chars": 640,
"preview": "export * from './account.entity';\nexport * from './database-entity';\nexport * from './directory-watcher.entity';\nexport "
},
{
"path": "apps/client-server/src/app/drizzle/models/notification.entity.ts",
"chars": 597,
"preview": "import { INotification } from '@postybirb/types';\nimport { instanceToPlain } from 'class-transformer';\nimport { Database"
},
{
"path": "apps/client-server/src/app/drizzle/models/post-event.entity.ts",
"chars": 1088,
"preview": "import {\n AccountId,\n EntityId,\n IPostEvent,\n IPostEventError,\n IPostEventMetadata,\n PostEventDto,\n PostEventType"
},
{
"path": "apps/client-server/src/app/drizzle/models/post-queue-record.entity.ts",
"chars": 801,
"preview": "import {\n EntityId,\n IPostQueueRecord,\n PostQueueRecordDto,\n SubmissionId\n} from '@postybirb/types';\nimport { instan"
},
{
"path": "apps/client-server/src/app/drizzle/models/post-record.entity.ts",
"chars": 1713,
"preview": "import {\n EntityId,\n IPostRecord,\n PostRecordDto,\n PostRecordResumeMode,\n PostRecordState,\n SubmissionId,\n} from '"
},
{
"path": "apps/client-server/src/app/drizzle/models/settings.entity.ts",
"chars": 579,
"preview": "import {\n ISettings,\n ISettingsOptions,\n SettingsConstants,\n SettingsDto,\n} from '@postybirb/types';\nimport { instan"
},
{
"path": "apps/client-server/src/app/drizzle/models/submission-file.entity.ts",
"chars": 2427,
"preview": "import {\n EntityId,\n FileSubmissionMetadata,\n ISubmissionFile,\n ISubmissionFileDto,\n SubmissionFileMetadata,\n} from"
},
{
"path": "apps/client-server/src/app/drizzle/models/submission.entity.ts",
"chars": 1901,
"preview": "import {\n ISubmission,\n ISubmissionDto,\n ISubmissionMetadata,\n ISubmissionScheduleInfo,\n ScheduleType,\n "
},
{
"path": "apps/client-server/src/app/drizzle/models/tag-converter.entity.ts",
"chars": 648,
"preview": "import { ITagConverter, TagConverterDto } from '@postybirb/types';\nimport { instanceToPlain } from 'class-transformer';\n"
},
{
"path": "apps/client-server/src/app/drizzle/models/tag-group.entity.ts",
"chars": 479,
"preview": "import { ITagGroup, TagGroupDto } from '@postybirb/types';\nimport { instanceToPlain } from 'class-transformer';\nimport {"
},
{
"path": "apps/client-server/src/app/drizzle/models/user-converter.entity.ts",
"chars": 662,
"preview": "import { IUserConverter, UserConverterDto } from '@postybirb/types';\nimport { instanceToPlain } from 'class-transformer'"
},
{
"path": "apps/client-server/src/app/drizzle/models/user-specified-website-options.entity.ts",
"chars": 817,
"preview": "import {\n AccountId,\n DynamicObject,\n IUserSpecifiedWebsiteOptions,\n SubmissionType,\n UserSpecifiedWebsiteOptionsDt"
},
{
"path": "apps/client-server/src/app/drizzle/models/website-data.entity.ts",
"chars": 661,
"preview": "import {\n DynamicObject,\n IWebsiteData,\n IWebsiteDataDto\n} from '@postybirb/types';\nimport { instanceToPlain, Type } "
},
{
"path": "apps/client-server/src/app/drizzle/models/website-options.entity.ts",
"chars": 1123,
"preview": "import {\n AccountId,\n IWebsiteFormFields,\n IWebsiteOptions,\n SubmissionId,\n WebsiteOptionsDto,\n} from '@postybirb/t"
},
{
"path": "apps/client-server/src/app/drizzle/postybirb-database/find-options.type.ts",
"chars": 58,
"preview": "export type FindOptions = {\n failOnMissing?: boolean;\n};\n"
},
{
"path": "apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.spec.ts",
"chars": 3851,
"preview": "import { clearDatabase, Schemas } from '@postybirb/database';\nimport { eq as equals } from 'drizzle-orm';\nimport 'reflec"
},
{
"path": "apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.ts",
"chars": 7976,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { NotFoundException } from '@nestjs/common';\nimport {\n g"
},
{
"path": "apps/client-server/src/app/drizzle/postybirb-database/postybirb-database.util.ts",
"chars": 1218,
"preview": "/* eslint-disable no-param-reassign */\nimport { SchemaKey, Schemas } from '@postybirb/database';\nimport { DatabaseEntity"
},
{
"path": "apps/client-server/src/app/drizzle/postybirb-database/schema-entity-map.ts",
"chars": 2297,
"preview": "import { SchemaKey } from '@postybirb/database';\nimport {\n Account,\n DirectoryWatcher,\n FileBuffer,\n PostEve"
},
{
"path": "apps/client-server/src/app/drizzle/transaction-context.ts",
"chars": 3851,
"preview": "import type { PostyBirbDatabaseType, SchemaKey } from '@postybirb/database';\nimport { Schemas } from '@postybirb/databas"
},
{
"path": "apps/client-server/src/app/file/file.controller.ts",
"chars": 1549,
"preview": "import {\n BadRequestException,\n Controller,\n Get,\n Param,\n Res,\n} from '@nestjs/common';\nimport { ApiNotFoundRespon"
},
{
"path": "apps/client-server/src/app/file/file.module.ts",
"chars": 562,
"preview": "import { Module } from '@nestjs/common';\nimport { ImageProcessingModule } from '../image-processing/image-processing.mod"
},
{
"path": "apps/client-server/src/app/file/file.service.spec.ts",
"chars": 11559,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase } from '@postybirb/database';\nimport { Pos"
},
{
"path": "apps/client-server/src/app/file/file.service.ts",
"chars": 5953,
"preview": "/* eslint-disable no-param-reassign */\nimport { BadRequestException, Injectable } from '@nestjs/common';\nimport { read }"
},
{
"path": "apps/client-server/src/app/file/models/multer-file-info.ts",
"chars": 497,
"preview": "/**\n * Matches a multer file info object.\n * TypeScript is not importing multer types correctly.\n *\n * @interface Multer"
},
{
"path": "apps/client-server/src/app/file/models/task-type.enum.ts",
"chars": 165,
"preview": "/**\n * Defines the requested TaskType\n */\nexport enum TaskType {\n CREATE = 'CREATE', // Creating a new file entity\n UP"
},
{
"path": "apps/client-server/src/app/file/models/task.ts",
"chars": 875,
"preview": "import { EntityId, FileSubmission } from '@postybirb/types';\nimport { MulterFileInfo } from './multer-file-info';\nimport"
},
{
"path": "apps/client-server/src/app/file/services/create-file.service.ts",
"chars": 11119,
"preview": "import * as rtf from '@iarna/rtf-to-html';\nimport { Injectable } from '@nestjs/common';\nimport { Insert, Select } from '"
},
{
"path": "apps/client-server/src/app/file/services/update-file.service.ts",
"chars": 11011,
"preview": "/* eslint-disable no-param-reassign */\nimport * as rtf from '@iarna/rtf-to-html';\nimport {\n BadRequestException,\n "
},
{
"path": "apps/client-server/src/app/file/utils/image.util.ts",
"chars": 587,
"preview": "/**\n * Utility class for image-related checks.\n *\n * NOTE: Sharp image processing has been moved to SharpInstanceManager"
},
{
"path": "apps/client-server/src/app/file-converter/converters/file-converter.ts",
"chars": 680,
"preview": "import { IFileBuffer } from '@postybirb/types';\n\nexport interface IFileConverter {\n /**\n * Determines if the file can"
},
{
"path": "apps/client-server/src/app/file-converter/converters/text-file-converter.ts",
"chars": 4337,
"preview": "import { IFileBuffer } from '@postybirb/types';\nimport { htmlToText } from 'html-to-text';\nimport { TurndownService } fr"
},
{
"path": "apps/client-server/src/app/file-converter/file-converter.module.ts",
"chars": 228,
"preview": "import { Module } from '@nestjs/common';\nimport { FileConverterService } from './file-converter.service';\n\n@Module({\n p"
},
{
"path": "apps/client-server/src/app/file-converter/file-converter.service.spec.ts",
"chars": 584,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase } from '@postybirb/database';\nimport { Fil"
},
{
"path": "apps/client-server/src/app/file-converter/file-converter.service.ts",
"chars": 982,
"preview": "import { Injectable } from '@nestjs/common';\nimport { IFileBuffer } from '@postybirb/types';\nimport { IFileConverter } f"
},
{
"path": "apps/client-server/src/app/form-generator/dtos/form-generation-request.dto.ts",
"chars": 525,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport {\n AccountId,\n IFormGenerationRequestDto,\n SubmissionType,\n} fr"
},
{
"path": "apps/client-server/src/app/form-generator/form-generator.controller.ts",
"chars": 1003,
"preview": "import { Body, Controller, Post } from '@nestjs/common';\nimport { ApiResponse, ApiTags } from '@nestjs/swagger';\nimport "
},
{
"path": "apps/client-server/src/app/form-generator/form-generator.module.ts",
"chars": 666,
"preview": "import { Module } from '@nestjs/common';\nimport { AccountModule } from '../account/account.module';\nimport { UserSpecifi"
},
{
"path": "apps/client-server/src/app/form-generator/form-generator.service.spec.ts",
"chars": 10042,
"preview": "import { NotFoundException } from '@nestjs/common';\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { clea"
},
{
"path": "apps/client-server/src/app/form-generator/form-generator.service.ts",
"chars": 3746,
"preview": "import {\n BadRequestException,\n Injectable,\n NotFoundException,\n} from '@nestjs/common';\nimport { FormBuilderMetadata"
},
{
"path": "apps/client-server/src/app/image-processing/image-processing.module.ts",
"chars": 440,
"preview": "import { Global, Module } from '@nestjs/common';\nimport { SharpInstanceManager } from './sharp-instance-manager';\n\n/**\n "
},
{
"path": "apps/client-server/src/app/image-processing/index.ts",
"chars": 217,
"preview": "export { ImageProcessingModule } from './image-processing.module';\nexport { SharpInstanceManager } from './sharp-instanc"
},
{
"path": "apps/client-server/src/app/image-processing/sharp-instance-manager.ts",
"chars": 10027,
"preview": "import { Injectable, OnModuleDestroy } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport { Image"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-converter.ts",
"chars": 2580,
"preview": "/* eslint-disable no-underscore-dangle */\nimport { SchemaKey } from '@postybirb/database';\nimport { Logger } from '@post"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-custom-shortcut.converter.spec.ts",
"chars": 17946,
"preview": "import { clearDatabase } from '@postybirb/database';\nimport { ensureDirSync, PostyBirbDirectories, writeSync } from '@po"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-custom-shortcut.converter.ts",
"chars": 402,
"preview": "import { SchemaKey } from '@postybirb/database';\nimport { LegacyCustomShortcut } from '../legacy-entities/legacy-custom-"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-tag-converter.converter.spec.ts",
"chars": 3537,
"preview": "import { clearDatabase } from '@postybirb/database';\nimport { ensureDirSync, PostyBirbDirectories, writeSync } from '@po"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-tag-converter.converter.ts",
"chars": 390,
"preview": "import { SchemaKey } from '@postybirb/database';\nimport { LegacyTagConverter } from '../legacy-entities/legacy-tag-conve"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-tag-group.converter.spec.ts",
"chars": 4046,
"preview": "import { clearDatabase } from '@postybirb/database';\nimport { ensureDirSync, PostyBirbDirectories, writeSync } from '@po"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-tag-group.converter.ts",
"chars": 366,
"preview": "import { SchemaKey } from '@postybirb/database';\nimport { LegacyTagGroup } from '../legacy-entities/legacy-tag-group';\ni"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-user-account.converter.spec.ts",
"chars": 2760,
"preview": "import { clearDatabase } from '@postybirb/database';\nimport { ensureDirSync, PostyBirbDirectories, writeSync } from '@po"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-user-account.converter.ts",
"chars": 376,
"preview": "import { SchemaKey } from '@postybirb/database';\nimport { LegacyUserAccount } from '../legacy-entities/legacy-user-accou"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-website-data.converter.spec.ts",
"chars": 8290,
"preview": "import { clearDatabase } from '@postybirb/database';\nimport { ensureDirSync, PostyBirbDirectories, writeSync } from '@po"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/converters/legacy-website-data.converter.ts",
"chars": 1104,
"preview": "import { SchemaKey } from '@postybirb/database';\nimport { LegacyWebsiteData } from '../legacy-entities/legacy-website-da"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/dtos/legacy-import.dto.ts",
"chars": 315,
"preview": "import { IsBoolean, IsOptional, IsString } from 'class-validator';\n\nexport class LegacyImportDto {\n @IsBoolean()\n cust"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-database-importer.controller.ts",
"chars": 546,
"preview": "import { Body, Controller, Post } from '@nestjs/common';\nimport { LegacyImportDto } from './dtos/legacy-import.dto';\nimp"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-database-importer.module.ts",
"chars": 473,
"preview": "import { Module } from '@nestjs/common';\nimport { AccountModule } from '../account/account.module';\nimport { LegacyDatab"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-database-importer.service.ts",
"chars": 3308,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport { app } from 'electron';"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-converter-entity.ts",
"chars": 260,
"preview": "import { IEntity } from '@postybirb/types';\n\nexport type MinimalEntity<T extends IEntity> = Omit<\n T,\n 'createdAt' | '"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-custom-shortcut.ts",
"chars": 10803,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\n/* eslint-disable import/no-extraneous-dependencies */\nimport { "
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-tag-converter.ts",
"chars": 1164,
"preview": "import { ITagConverter } from '@postybirb/types';\nimport { WebsiteNameMapper } from '../utils/website-name-mapper';\nimpo"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-tag-group.ts",
"chars": 709,
"preview": "import { ITagGroup } from '@postybirb/types';\nimport {\n LegacyConverterEntity,\n MinimalEntity,\n} from './legacy-co"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-user-account.ts",
"chars": 1088,
"preview": "import { IAccount } from '@postybirb/types';\nimport { WebsiteNameMapper } from '../utils/website-name-mapper';\nimport {\n"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/legacy-entities/legacy-website-data.ts",
"chars": 2299,
"preview": "import { Logger } from '@postybirb/logger';\nimport { IWebsiteData } from '@postybirb/types';\nimport { WebsiteDataTransfo"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/bluesky-data-transformer.ts",
"chars": 969,
"preview": "import { BlueskyAccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website-d"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/custom-data-transformer.ts",
"chars": 1780,
"preview": "import { CustomAccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website-da"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/discord-data-transformer.ts",
"chars": 1076,
"preview": "import { DiscordAccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website-d"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/e621-data-transformer.ts",
"chars": 901,
"preview": "import { E621AccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website-data"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/index.ts",
"chars": 353,
"preview": "export * from './bluesky-data-transformer';\nexport * from './custom-data-transformer';\nexport * from './discord-data-tra"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/inkbunny-data-transformer.ts",
"chars": 1021,
"preview": "import { InkbunnyAccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website-"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/megalodon-data-transformer.ts",
"chars": 1930,
"preview": "import { MegalodonAccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/telegram-data-transformer.ts",
"chars": 1465,
"preview": "import { TelegramAccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website-"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/implementations/twitter-data-transformer.ts",
"chars": 1445,
"preview": "import { TwitterAccountData } from '@postybirb/types';\nimport { LegacyWebsiteDataTransformer } from '../legacy-website-d"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/index.ts",
"chars": 105,
"preview": "export * from './legacy-website-data-transformer';\nexport * from './website-data-transformer-registry';\n\n"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/legacy-website-data-transformer.ts",
"chars": 922,
"preview": "/**\n * Interface for transforming legacy website-specific account data\n * to modern WebsiteData format.\n */\n// eslint-di"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/transformers/website-data-transformer-registry.ts",
"chars": 2566,
"preview": "import { BlueskyDataTransformer } from './implementations/bluesky-data-transformer';\nimport { CustomDataTransformer } fr"
},
{
"path": "apps/client-server/src/app/legacy-database-importer/utils/ndjson-parser.ts",
"chars": 2300,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport { promises as fs } from "
},
{
"path": "apps/client-server/src/app/legacy-database-importer/utils/website-name-mapper.ts",
"chars": 2426,
"preview": "/**\n * Maps legacy PostyBirb Plus website names to new PostyBirb V4 website IDs.\n * The website ID in V4 is the value fr"
},
{
"path": "apps/client-server/src/app/logs/logs.controller.ts",
"chars": 862,
"preview": "import { Controller, Get, Res } from '@nestjs/common';\nimport { ApiOkResponse, ApiTags } from '@nestjs/swagger';\nimport "
},
{
"path": "apps/client-server/src/app/logs/logs.module.ts",
"chars": 267,
"preview": "import { Module } from '@nestjs/common';\nimport { LogsController } from './logs.controller';\nimport { LogsService } from"
},
{
"path": "apps/client-server/src/app/logs/logs.service.ts",
"chars": 3496,
"preview": "import { Injectable } from '@nestjs/common';\nimport { PostyBirbDirectories } from '@postybirb/fs';\nimport { Logger } fro"
},
{
"path": "apps/client-server/src/app/notifications/dtos/create-notification.dto.ts",
"chars": 585,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { ICreateNotificationDto } from '@postybirb/types';\nimport { IsArr"
},
{
"path": "apps/client-server/src/app/notifications/dtos/update-notification.dto.ts",
"chars": 332,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { IUpdateNotificationDto } from '@postybirb/types';\nimport { IsBoo"
},
{
"path": "apps/client-server/src/app/notifications/notification.events.ts",
"chars": 382,
"preview": "import { NOTIFICATION_UPDATES } from '@postybirb/socket-events';\nimport { INotification } from '@postybirb/types';\nimpor"
},
{
"path": "apps/client-server/src/app/notifications/notifications.controller.ts",
"chars": 1792,
"preview": "import {\n Body,\n Controller,\n Delete,\n Get,\n Param,\n Patch,\n Post,\n Query,\n} from '@nestjs/common';\nimport {\n A"
},
{
"path": "apps/client-server/src/app/notifications/notifications.module.ts",
"chars": 763,
"preview": "import { Module } from '@nestjs/common';\nimport { AccountModule } from '../account/account.module';\nimport { SettingsMod"
},
{
"path": "apps/client-server/src/app/notifications/notifications.service.spec.ts",
"chars": 3189,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase } from '@postybirb/database';\nimport { Set"
},
{
"path": "apps/client-server/src/app/notifications/notifications.service.ts",
"chars": 5390,
"preview": "import { Injectable, Optional } from '@nestjs/common';\nimport { Cron, CronExpression } from '@nestjs/schedule';\nimport {"
},
{
"path": "apps/client-server/src/app/post/dtos/post-queue-action.dto.ts",
"chars": 497,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { IPostQueueActionDto, PostRecordResumeMode } from '@postybirb/typ"
},
{
"path": "apps/client-server/src/app/post/dtos/queue-post-record.dto.ts",
"chars": 355,
"preview": "import { ApiProperty } from '@nestjs/swagger';\nimport { IQueuePostRecordRequestDto, SubmissionId } from '@postybirb/type"
},
{
"path": "apps/client-server/src/app/post/errors/index.ts",
"chars": 45,
"preview": "export * from './invalid-post-chain.error';\n\n"
},
{
"path": "apps/client-server/src/app/post/errors/invalid-post-chain.error.ts",
"chars": 1802,
"preview": "import { EntityId, PostRecordResumeMode } from '@postybirb/types';\n\n/**\n * Error thrown when attempting to create a Post"
},
{
"path": "apps/client-server/src/app/post/models/cancellable-token.ts",
"chars": 468,
"preview": "import { CancellationError } from './cancellation-error';\n\n/**\n * CancellableToken is a simple class that can be used to"
},
{
"path": "apps/client-server/src/app/post/models/cancellation-error.ts",
"chars": 440,
"preview": "/**\n * CancellationError is thrown when a task is cancelled.\n * @class CancellationError\n */\nexport class CancellationEr"
},
{
"path": "apps/client-server/src/app/post/models/posting-file.ts",
"chars": 1919,
"preview": "import { FormFile } from '@postybirb/http';\nimport {\n FileType,\n IFileBuffer,\n SubmissionFileId,\n SubmissionFileMeta"
},
{
"path": "apps/client-server/src/app/post/post.controller.ts",
"chars": 963,
"preview": "import { Controller, Get, Param } from '@nestjs/common';\nimport { ApiOkResponse, ApiTags } from '@nestjs/swagger';\nimpor"
},
{
"path": "apps/client-server/src/app/post/post.module.ts",
"chars": 1999,
"preview": "import { Module } from '@nestjs/common';\nimport { FileConverterModule } from '../file-converter/file-converter.module';\n"
},
{
"path": "apps/client-server/src/app/post/post.service.ts",
"chars": 1231,
"preview": "import { Injectable, Optional } from '@nestjs/common';\nimport { EntityId, PostEventDto } from '@postybirb/types';\nimport"
},
{
"path": "apps/client-server/src/app/post/services/post-file-resizer/post-file-resizer.service.spec.ts",
"chars": 4243,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport {\n DefaultSubmissionFileMetadata,\n ISubmission,\n ISubmi"
},
{
"path": "apps/client-server/src/app/post/services/post-file-resizer/post-file-resizer.service.ts",
"chars": 3265,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport {\n FileType,\n IFileBuf"
},
{
"path": "apps/client-server/src/app/post/services/post-manager/post-manager.controller.ts",
"chars": 1089,
"preview": "import { Controller, Get, Param, Post } from '@nestjs/common';\nimport { ApiOkResponse, ApiTags } from '@nestjs/swagger';"
},
{
"path": "apps/client-server/src/app/post/services/post-manager-v2/base-post-manager.service.ts",
"chars": 18235,
"preview": "import {\n Logger,\n trackEvent,\n trackException,\n trackMetric,\n} from '@postybirb/logger';\nimport {\n AccountId,\n En"
},
{
"path": "apps/client-server/src/app/post/services/post-manager-v2/file-submission-post-manager.service.spec.ts",
"chars": 33224,
"preview": "import { clearDatabase } from '@postybirb/database';\nimport {\n AccountId,\n DefaultSubmissionFileMetadata,\n EntityId,\n"
},
{
"path": "apps/client-server/src/app/post/services/post-manager-v2/file-submission-post-manager.service.ts",
"chars": 14978,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport {\n AccountId,\n FileSub"
},
{
"path": "apps/client-server/src/app/post/services/post-manager-v2/index.ts",
"chars": 210,
"preview": "export * from './base-post-manager.service';\nexport * from './file-submission-post-manager.service';\nexport * from './me"
},
{
"path": "apps/client-server/src/app/post/services/post-manager-v2/message-submission-post-manager.service.spec.ts",
"chars": 11623,
"preview": "import { clearDatabase } from '@postybirb/database';\nimport {\n AccountId,\n EntityId,\n IPostResponse,\n PostData,\n Po"
},
{
"path": "apps/client-server/src/app/post/services/post-manager-v2/message-submission-post-manager.service.ts",
"chars": 3352,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport {\n AccountId,\n Pos"
},
{
"path": "apps/client-server/src/app/post/services/post-manager-v2/post-manager-registry.service.ts",
"chars": 3450,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport { EntityId, SubmissionTy"
},
{
"path": "apps/client-server/src/app/post/services/post-queue/post-queue.controller.ts",
"chars": 1480,
"preview": "import { Body, Controller, Get, Post } from '@nestjs/common';\nimport { ApiOkResponse, ApiTags } from '@nestjs/swagger';\n"
},
{
"path": "apps/client-server/src/app/post/services/post-queue/post-queue.service.spec.ts",
"chars": 7887,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase } from '@postybirb/database';\nimport {\n A"
},
{
"path": "apps/client-server/src/app/post/services/post-queue/post-queue.service.ts",
"chars": 22455,
"preview": "import {\n Injectable,\n InternalServerErrorException,\n OnModuleInit,\n Optional,\n} from '@nestjs/common';\nimport { Cro"
},
{
"path": "apps/client-server/src/app/post/services/post-record-factory/index.ts",
"chars": 89,
"preview": "export * from './post-event.repository';\nexport * from './post-record-factory.service';\n\n"
},
{
"path": "apps/client-server/src/app/post/services/post-record-factory/post-event.repository.ts",
"chars": 2628,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Insert } from '@postybirb/database';\nimport {\n AccountId,\n Entit"
},
{
"path": "apps/client-server/src/app/post/services/post-record-factory/post-record-factory.service.spec.ts",
"chars": 36818,
"preview": "import { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase } from '@postybirb/database';\nimport {\n E"
},
{
"path": "apps/client-server/src/app/post/services/post-record-factory/post-record-factory.service.ts",
"chars": 18253,
"preview": "import { Injectable } from '@nestjs/common';\nimport { Logger } from '@postybirb/logger';\nimport {\n AccountId,\n EntityI"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/converters/base-converter.ts",
"chars": 4704,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { UsernameShortcut } from '@postybirb/types';\nimport { Co"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/converters/bbcode-converter.ts",
"chars": 5911,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { ConversionContext } from '../description-node.base';\nim"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/converters/custom-converter.ts",
"chars": 1366,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { ConversionContext } from '../description-node.base';\nim"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/converters/html-converter.ts",
"chars": 7172,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { encode } from 'html-entities';\nimport { ConversionConte"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/converters/npf-converter.spec.ts",
"chars": 6956,
"preview": "import { NPFTextBlock, TipTapNode } from '@postybirb/types';\nimport { ConversionContext } from '../description-node.base"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/converters/npf-converter.ts",
"chars": 11444,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport {\n NPFContentBlock,\n NPFImageBlock,\n NPFInlineFormatti"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/converters/plaintext-converter.ts",
"chars": 3578,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { ConversionContext } from '../description-node.base';\nim"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/description-node-tree.ts",
"chars": 6319,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport TurndownService from 'turndown';\nimport { BaseConverter }"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/description-node.base.ts",
"chars": 530,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { UsernameShortcut } from '@postybirb/types';\nimport { Ti"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node/description-node.types.ts",
"chars": 2045,
"preview": "/* eslint-disable @typescript-eslint/no-explicit-any */\nimport { TipTapMark, TipTapNode } from '@postybirb/types';\n\n/**\n"
},
{
"path": "apps/client-server/src/app/post-parsers/models/description-node.spec.ts",
"chars": 34118,
"preview": "import { TipTapNode } from '@postybirb/types';\nimport { DescriptionNodeTree } from './description-node/description-node-"
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/content-warning-parser.ts",
"chars": 835,
"preview": "import { Injectable } from '@nestjs/common';\nimport { BaseWebsiteOptions } from '../../websites/models/base-website-opti"
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/description-parser.service.spec.ts",
"chars": 26596,
"preview": "/* eslint-disable max-classes-per-file */\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase "
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/description-parser.service.ts",
"chars": 8153,
"preview": "import { Inject, Injectable } from '@nestjs/common';\nimport {\n DescriptionType,\n TipTapNode,\n UsernameShortcut,\n} fro"
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/rating-parser.spec.ts",
"chars": 1602,
"preview": "import { IWebsiteOptions, SubmissionRating } from '@postybirb/types';\nimport { BaseWebsiteOptions } from '../../websites"
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/rating-parser.ts",
"chars": 571,
"preview": "import { SubmissionRating } from '@postybirb/types';\nimport { BaseWebsiteOptions } from '../../websites/models/base-webs"
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/tag-parser.service.spec.ts",
"chars": 6346,
"preview": "/* eslint-disable max-classes-per-file */\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase "
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/tag-parser.service.ts",
"chars": 827,
"preview": "import { Injectable } from '@nestjs/common';\nimport { TagConvertersService } from '../../tag-converters/tag-converters.s"
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/title-parser.spec.ts",
"chars": 3045,
"preview": "/* eslint-disable max-classes-per-file */\nimport { Test, TestingModule } from '@nestjs/testing';\nimport { clearDatabase "
},
{
"path": "apps/client-server/src/app/post-parsers/parsers/title-parser.ts",
"chars": 787,
"preview": "import { Injectable } from '@nestjs/common';\nimport { BaseWebsiteOptions } from '../../websites/models/base-website-opti"
},
{
"path": "apps/client-server/src/app/post-parsers/post-parsers.module.ts",
"chars": 1128,
"preview": "import { Module, forwardRef } from '@nestjs/common';\nimport { CustomShortcutsModule } from '../custom-shortcuts/custom-s"
},
{
"path": "apps/client-server/src/app/post-parsers/post-parsers.service.ts",
"chars": 2213,
"preview": "import { Injectable } from '@nestjs/common';\nimport {\n ISubmission,\n IWebsiteFormFields,\n IWebsiteOptions,\n PostData"
},
{
"path": "apps/client-server/src/app/remote/models/update-cookies-remote.dto.ts",
"chars": 241,
"preview": "import { UpdateCookiesRemote } from '@postybirb/types';\nimport { IsString } from 'class-validator';\n\nexport class Update"
},
{
"path": "apps/client-server/src/app/remote/remote.controller.ts",
"chars": 590,
"preview": "import { Body, Controller, Get, Param, Post } from '@nestjs/common';\nimport { UpdateCookiesRemoteDto } from './models/up"
}
]
// ... and 1030 more files (download for full content)
About this extraction
This page contains the full source code of the mvdicarlo/postybirb GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 1230 files (4.4 MB), approximately 1.2M tokens, and a symbol index with 3277 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.