23 Commits

Author SHA1 Message Date
0611ceb3ec Release v26.3.16
All checks were successful
Production deployment / Build (release) Successful in 1m4s
Staging deployment / Build (release) Successful in 1m24s
CI / Checks (push) Successful in 4m36s
Staging deployment / Deploy (release) Successful in 2m16s
Production deployment / Deploy (release) Successful in 3m1s
2026-03-16 20:16:07 +01:00
3c71464663 BTHLABS-82: Spring 2026 Refresh
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2026-03-16 19:09:38 +00:00
c842657766 BTHLABS-83: View association view raises 403 when opened as anonymous
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2026-03-12 16:26:54 +00:00
e2b2455bea BTHLABS-0000: Add build-opera-source task in _extension_. 2026-01-15 20:54:37 +01:00
1b775a130b Release 26.1.14.post0
All checks were successful
Production deployment / Build (release) Successful in 1m58s
Staging deployment / Build (release) Successful in 2m0s
CI / Checks (push) Successful in 4m43s
Staging deployment / Deploy (release) Successful in 1m56s
Production deployment / Deploy (release) Successful in 2m25s
2026-01-14 21:24:09 +01:00
bcef0d2d09 BTHLABS-0000: Fixed DB host resolution post VPN migration 2026-01-14 21:23:15 +01:00
9f121a0ceb Release v26.1.14
Some checks failed
CI / Checks (push) Successful in 1m50s
Production deployment / Build (release) Successful in 58s
Staging deployment / Build (release) Successful in 59s
Staging deployment / Deploy (release) Successful in 1m45s
Production deployment / Deploy (release) Failing after 4m29s
2026-01-14 20:59:08 +01:00
d16f0cd957 BTHLABS-81: OG Properties on Share Association page
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2026-01-14 20:53:46 +01:00
98e5e1891a Release v25.12.04
All checks were successful
CI / Checks (push) Successful in 2m14s
Production deployment / Build (release) Successful in 26s
Staging deployment / Build (release) Successful in 50s
Staging deployment / Deploy (release) Successful in 1m19s
Production deployment / Deploy (release) Successful in 2m17s
2025-12-04 20:57:48 +01:00
06343e6ed3 BTHLABS-0000: Tweaking association card's layout 2025-12-04 20:55:55 +01:00
82a3b612ec BTHLABS-0000: Fix YT embed code 2025-12-04 20:46:38 +01:00
1e549d3fc2 Release v25.11.26
All checks were successful
Production deployment / Build (release) Successful in 32s
CI / Checks (push) Successful in 2m20s
Staging deployment / Build (release) Successful in 1m9s
Staging deployment / Deploy (release) Successful in 1m10s
Production deployment / Deploy (release) Successful in 2m25s
2025-11-27 17:52:16 +01:00
55126f4af6 BTHLABS-66: Prepping for public release: Take five
AKA "Using Apple reviewers as QA for your project". Thanks, y'all! :)
2025-11-27 17:51:19 +01:00
cca49f2292 BTHLABS-66: Prepping for public release: Take four
Apple gonna Apple... ;)
2025-11-25 12:54:52 +01:00
23f8296659 BTHLABS-66: Prepping for public release: Take three
I smell a drastic change to auth flow in the Mac app... Let's see if it
gets approved this time :D.
2025-11-21 21:04:28 +01:00
9abed01e53 BTHLABS-0000: Code cleanup 2025-11-20 20:34:42 +01:00
3b1aba9672 Release v25.11.19
Some checks failed
CI / Checks (push) Failing after 2m22s
Production deployment / Build (release) Successful in 32s
Staging deployment / Build (release) Successful in 1m26s
Production deployment / Deploy (release) Successful in 1m47s
Staging deployment / Deploy (release) Successful in 1m40s
2025-11-19 20:31:02 +01:00
22061486a8 BTHLABS-66: Prepping for public release: Take two 2025-11-19 20:28:31 +01:00
38785ccf92 BTHLABS-0000: Fixing Share extension builds in Release configs 2025-11-19 07:19:19 +01:00
1f78a4a079 Release v25.11.18
All checks were successful
CI / Checks (push) Successful in 3m39s
Production deployment / Build (release) Successful in 31s
Staging deployment / Build (release) Successful in 52s
Staging deployment / Deploy (release) Successful in 1m52s
Production deployment / Deploy (release) Successful in 2m34s
2025-11-18 20:47:54 +01:00
20fa33abeb BTHLABS-66: Prepping for public release: Take one 2025-11-18 20:47:07 +01:00
16a9c73624 Release v25.11.12
All checks were successful
Production deployment / Build (release) Successful in 38s
Staging deployment / Build (release) Successful in 1m10s
CI / Checks (push) Successful in 4m24s
Production deployment / Deploy (release) Successful in 2m15s
Staging deployment / Deploy (release) Successful in 1m55s
2025-11-12 20:55:56 +01:00
b358ef6686 BTHLABS-65: Implement support for Win 11 payload in PWA share sheet endpoint
Co-authored-by: Tomek Wójcik <labs@tomekwojcik.pl>
Co-committed-by: Tomek Wójcik <labs@tomekwojcik.pl>
2025-11-12 19:30:33 +00:00
142 changed files with 2254 additions and 979 deletions

View File

@@ -75,6 +75,8 @@ jobs:
--push \
--platform "${{ inputs.platform }}" \
--build-arg IMAGE_ID="${{ inputs.target }}.${SHORT_SHA}" \
--build-arg IMAGE_VERSION="${VERSION}" \
--build-arg IMAGE_REVISION="${SHORT_SHA}" \
-f services/backend/Dockerfile \
--target "${{ inputs.target }}" \
-t "${{ inputs.registry }}/hotpocket/backend:${{ inputs.target }}-${VERSION}-${BUILD}" \

View File

@@ -68,7 +68,7 @@ jobs:
set -x
(
cd deployment/hotpocket.bthlab ;
cd deployment/hotpocket_bthlab ;
export KUBECONFIG="/opt/k8s/etc/kubeconfig" ;
/opt/k8s/bin/kubectl config use-context ${KUBERNETES_CLUSTER} ;
/opt/k8s/bin/kubectl -n ${KUBERNETES_NAMESPACE} apply -f resources/backend/config-map-local-deps.yaml ;
@@ -83,7 +83,7 @@ jobs:
run: |
set -x
(
cd deployment/hotpocket.bthlab ;
cd deployment/hotpocket_bthlab ;
export KUBECONFIG="/opt/k8s/etc/kubeconfig" ;
/opt/k8s/bin/kubectl config use-context ${KUBERNETES_CLUSTER} ;
/opt/k8s/bin/kustomize edit set image hotpocket-backend=nexus.bthlab.bthlabs.net:8002/hotpocket/backend:${BACKEND_TAG} ;

View File

@@ -1,13 +1,30 @@
# HotPocket by BTHLabs
This repository contains the _HotPocket_ project.
Minimal self-hosted bookmarking app :).
## The what, the why and the ugly
HotPocket is a minimal self-hosted bookmarking app. It combines a Web
application, companion apps, browser extensions to give you a way to quickly
save links for later.
HotPocket came to be to fill in the blank left by Pocket, after Mozilla shut it
down. I looked at the existing alternatives and found them either too
feature-rich, too involved to self-host or otherwise not to my liking. So I
decided to sit down and build something for myself.
With the what and why out of the way, let's talk about the ugly... At its core
HotPocket is a personal project. I built it by myself and for myself. It may
or may not fit your needs. If it does, happy saving!
If you're feeling up for an adventure, continue reading below :).
## Development setup
### Requirements:
* Python 3.12,
* Poetry 1.8.3,
* Python 3.13,
* Poetry 2.2.1,
* `git-crypt`,
* Docker with Docker Compose and Buildx.
@@ -66,7 +83,7 @@ $ docker run --rm -it \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME=hotpocket \
-e HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD=hotpocketm4st3r \
-p 8000:8000 \
docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.11.06-01
hotpocket/backend:aio-v26.3.16-01
```
The command above will set up and start the application. The SQLite file will
@@ -128,6 +145,7 @@ that can be used to configure the services.
| `HOTPOCKET_BACKEND_RUN_MIGRATIONS` | `false` or `true` | Set to `true` to run database muigrations when the container starts. |
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME` | N/A | Username for the initial account. |
| `HOTPOCKET_BACKEND_INITIAL_ACCOUNT_PASSWORD` | N/A | Password for the initial account. |
| `HOTPOCKET_BACKEND_OPERATOR_EMAIL` | N/A | Instance operator's e-mail. Used to display extra language on login page. |
**Env and App settings**
@@ -158,6 +176,15 @@ method's name in the UI and defaults to `OIDC`.
**NOTE:** Currently, only Keycloak has been tested with this login method.
### Volumes
Both images declare `/srv/run` to be a volume. It's intended to keep the
service's runtime data, including but not limited to PID files, UNIX sockets
etc. It's recommended to persist this volume.
Additionally, the `deployment` image declares `/srv/uploads` to be a volume.
It's recommeded to persist this volume.
## Author
_HotPocket_ is developed by [BTHLabs](https://www.bthlabs.pl/).

View File

@@ -1,6 +1,6 @@
services:
backend:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:aio-v25.11.06-01"
image: "bthlabs/hotpocket:aio-v26.3.16-01"
environment:
HOTPOCKET_BACKEND_SECRET_KEY: "thisisntright"
HOTPOCKET_BACKEND_INITIAL_ACCOUNT_USERNAME: "hotpocket"

View File

@@ -8,7 +8,7 @@ x-backend-environment: &x-backend-environment
services:
webapp:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.11.06-01"
image: "bthlabs/hotpocket:deployment-v26.3.16-01"
environment:
<<: *x-backend-environment
HOTPOCKET_BACKEND_ALLOWED_HOSTS: "app.staging.hotpocket.bthlab.bthlabs.net"
@@ -21,7 +21,7 @@ services:
restart: "unless-stopped"
admin:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.11.06-01"
image: "bthlabs/hotpocket:deployment-v26.3.16-01"
environment:
<<: *x-backend-environment
HOTPOCKET_BACKEND_APP: "admin"
@@ -35,7 +35,7 @@ services:
restart: "unless-stopped"
celery-worker:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.11.06-01"
image: "bthlabs/hotpocket:deployment-v26.3.16-01"
command:
- "/srv/venv/bin/celery"
- "-A"
@@ -57,7 +57,7 @@ services:
restart: "unless-stopped"
celery-beat:
image: "docker-hosted.nexus.bthlabs.pl/hotpocket/backend:deployment-v25.11.06-01"
image: "bthlabs/hotpocket:deployment-v26.3.16-01"
command:
- "/srv/venv/bin/celery"
- "-A"

View File

@@ -8,7 +8,7 @@ hotpocket_app:
node: "home.vm.snakeweb.net"
docker:
extra_hosts:
- "home.vm:10.0.1.2"
- "home.vm:10.16.1.100"
backend:
image_tag: "{{ hotpocket_app_image_tag|default('deployment-v25.10.21-01') }}"
database:

6
poetry.lock generated
View File

@@ -5,7 +5,7 @@ name = "hotpocket-workspace-tools"
version = "1.0.0.dev0"
description = "HotPocket Workspace Tools"
optional = false
python-versions = "^3.12"
python-versions = "^3.13"
groups = ["main"]
files = []
develop = true
@@ -31,5 +31,5 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "a7028d4a0260c82012077d9cc4b324b0ef5ab8ed24aa283a51cf941ba09685a9"
python-versions = "^3.13"
content-hash = "175bf795c7148fe40af7e095d6f41918fa14cf4c71be87444a4d6c467fbd38d2"

View File

@@ -1,13 +1,13 @@
[tool.poetry]
name = "hotpocket-workspace"
version = "25.11.06"
version = "26.3.16"
description = "HotPocket Workspace"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
python = "^3.13"
hotpocket-workspace-tools = {path = "services/packages/workspace_tools", develop = true}
[build-system]

View File

@@ -92,3 +92,5 @@ Shared (Extension)/Resources/content-bundle.js
Shared (Extension)/Resources/manifest.json
Shared (Extension)/Resources/preauth.html
Shared (Extension)/Resources/preauth.js
Shared (Extension)/Resources/options.html
Shared (Extension)/Resources/options.js

View File

@@ -2,7 +2,7 @@ ARG APP_USER_UID=1000
ARG APP_USER_GID=1000
ARG IMAGE_ID=development.00000000
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-node-20251014-01 AS development
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-node-20251114-01 AS development
ARG APP_USER_UID
ARG APP_USER_GID

View File

@@ -85,8 +85,8 @@
4C70F3142E886A8F00320048 /* HPSharedItem.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HPSharedItem.m; sourceTree = "<group>"; };
4C70F3172E886ADD00320048 /* HPSharedItemsContainer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HPSharedItemsContainer.h; sourceTree = "<group>"; };
4C70F3182E886ADD00320048 /* HPSharedItemsContainer.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = HPSharedItemsContainer.m; sourceTree = "<group>"; };
4CABCAB02E56F0C900D8A354 /* HotPocket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HotPocket.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAC62E56F0C900D8A354 /* HotPocket.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = HotPocket.app; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAB02E56F0C900D8A354 /* HotPocket Development.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "HotPocket Development.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAC62E56F0C900D8A354 /* HotPocket Development.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "HotPocket Development.app"; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HotPocket Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "HotPocket Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Save to HotPocket.appex"; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -107,6 +107,7 @@
HPAPI.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSBundle+HotPocketExtensions.m",
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
);
@@ -123,6 +124,7 @@
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
MultilineLabel.m,
UnameLabel.m,
);
target = 4C2F0C5D2E851BBD0033F5C2 /* iOS (Share Extension) */;
};
@@ -134,6 +136,7 @@
HPAuthFlow.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSBundle+HotPocketExtensions.m",
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
);
@@ -161,6 +164,7 @@
HPAuthFlow.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSBundle+HotPocketExtensions.m",
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
);
@@ -181,6 +185,8 @@
"Resources/content-bundle.js",
Resources/images,
Resources/manifest.json,
Resources/options.html,
Resources/options.js,
Resources/preauth.html,
Resources/preauth.js,
SafariWebExtensionHandler.m,
@@ -195,6 +201,8 @@
"Resources/content-bundle.js",
Resources/images,
Resources/manifest.json,
Resources/options.html,
Resources/options.js,
Resources/preauth.html,
Resources/preauth.js,
SafariWebExtensionHandler.m,
@@ -215,6 +223,7 @@
HPAPI.m,
HPCredentialsHelper.m,
HPRPCClient.m,
"NSBundle+HotPocketExtensions.m",
"NSURL+HotPocketExtensions.m",
"Resources/icon-mac-384.png",
);
@@ -384,8 +393,8 @@
4CABCAB12E56F0C900D8A354 /* Products */ = {
isa = PBXGroup;
children = (
4CABCAB02E56F0C900D8A354 /* HotPocket.app */,
4CABCAC62E56F0C900D8A354 /* HotPocket.app */,
4CABCAB02E56F0C900D8A354 /* HotPocket Development.app */,
4CABCAC62E56F0C900D8A354 /* HotPocket Development.app */,
4CABCAD52E56F0C900D8A354 /* HotPocket Extension.appex */,
4CABCADF2E56F0C900D8A354 /* HotPocket Extension.appex */,
4CBCEA4F2E81CB9500722009 /* Save to HotPocket.appex */,
@@ -441,7 +450,7 @@
packageProductDependencies = (
);
productName = "HotPocket (iOS)";
productReference = 4CABCAB02E56F0C900D8A354 /* HotPocket.app */;
productReference = 4CABCAB02E56F0C900D8A354 /* HotPocket Development.app */;
productType = "com.apple.product-type.application";
};
4CABCAC52E56F0C900D8A354 /* HotPocket (macOS) */ = {
@@ -466,7 +475,7 @@
packageProductDependencies = (
);
productName = "HotPocket (macOS)";
productReference = 4CABCAC62E56F0C900D8A354 /* HotPocket.app */;
productReference = 4CABCAC62E56F0C900D8A354 /* HotPocket Development.app */;
productType = "com.apple.product-type.application";
};
4CABCAD42E56F0C900D8A354 /* HotPocket Extension (iOS) */ = {
@@ -713,7 +722,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
@@ -726,7 +735,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
SDKROOT = iphoneos;
@@ -746,7 +755,7 @@
buildSettings = {
CODE_SIGN_ENTITLEMENTS = "iOS (Share Extension)/iOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Share Extension)/Info.plist";
@@ -759,7 +768,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
SDKROOT = iphoneos;
@@ -779,7 +788,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -792,7 +801,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -814,7 +823,7 @@
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (Extension)/Info.plist";
@@ -827,7 +836,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -853,7 +862,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist";
@@ -873,7 +882,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -881,7 +890,7 @@
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
PRODUCT_NAME = HotPocket;
PRODUCT_NAME = "HotPocket Development";
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
@@ -899,7 +908,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "iOS (App)/HotPocket (iOS).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "iOS (App)/Info.plist";
@@ -919,7 +928,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -945,7 +954,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -960,7 +969,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -980,7 +989,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Extension)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_USER_SCRIPT_SANDBOXING = NO;
@@ -995,7 +1004,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -1017,7 +1026,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -1033,7 +1042,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -1041,7 +1050,7 @@
WebKit,
);
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket;
PRODUCT_NAME = HotPocket;
PRODUCT_NAME = "HotPocket Development";
REGISTER_APP_GROUPS = YES;
SDKROOT = macosx;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -1056,7 +1065,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (App)/HotPocket.entitlements";
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -1072,7 +1081,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
OTHER_LDFLAGS = (
"-framework",
SafariServices,
@@ -1206,7 +1215,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -1220,7 +1229,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
REGISTER_APP_GROUPS = YES;
@@ -1236,7 +1245,7 @@
CODE_SIGN_ENTITLEMENTS = "macOS (Share Extension)/macOS (Share Extension).entitlements";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 2025110601;
CURRENT_PROJECT_VERSION = 2026031601;
DEVELOPMENT_TEAM = 648728X64K;
ENABLE_HARDENED_RUNTIME = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -1250,7 +1259,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MARKETING_VERSION = 25.11.06;
MARKETING_VERSION = 26.3.16;
PRODUCT_BUNDLE_IDENTIFIER = pl.bthlabs.HotPocket.ShareExtension;
PRODUCT_NAME = "Save to HotPocket";
REGISTER_APP_GROUPS = YES;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -10,6 +10,7 @@
#import "HPAPI.h"
#import "HPCredentialsHelper.h"
#import "HPRPCClient.h"
#import "NSBundle+HotPocketExtensions.h"
#import "NSURL+HotPocketExtensions.h"
@implementation HPAuthParams
@@ -77,18 +78,19 @@
return nil;
}
NSDictionary *postAuthenticateURLParams = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"];
if (postAuthenticateURLParams == nil) {
NSString *expectedScheme = [NSBundle postAuthenticateURLScheme];
NSString *expectedHost = [NSBundle postAuthenticateURLHost];
if (expectedScheme == nil || expectedHost == nil) {
return nil;
}
NSURLComponents *urlComponents = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO];
if ([urlComponents.scheme isEqualToString:[postAuthenticateURLParams valueForKey:@"scheme"]] == NO) {
if ([urlComponents.scheme isEqualToString:expectedScheme] == NO) {
return nil;
}
if ([urlComponents.host isEqualToString:[postAuthenticateURLParams valueForKey:@"host"]] == NO) {
if ([urlComponents.host isEqualToString:expectedHost] == NO) {
return nil;
}
@@ -109,6 +111,8 @@
}
-(BOOL)handleAuthParams:(HPAuthParams *)authParams {
[[NSNotificationCenter defaultCenter] postNotificationName:@"AuthFlowDidReceiveAuthParams" object:self];
HPRPCClient *rpcClient = [[HPRPCClient alloc] initWithBaseURL:self.baseURL accessToken:nil];
NSArray *callParams = @[
@@ -120,7 +124,7 @@
method:@"accounts.access_tokens.create"
params:callParams endopoint:@"/accounts/rpc/"
completionHandler:^(NSString *callId, HPRPCCallResult *result) {
dispatch_async(dispatch_get_main_queue(), ^{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
if (result.error != nil) {
NSLog(@"-[HPAuthFlow handleAuthParams:] error=`%@`", result.error);
} else {

View File

@@ -0,0 +1,21 @@
//
// NSBundle+HotPocketExtensions.h
// HotPocket
//
// Created by Tomek Wójcik on 17/11/2025.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSBundle (HotPocketExtensions)
+(NSString *)uname;
+(NSDictionary *)postAuthenticateURLParams;
+(NSString *)postAuthenticateURLScheme;
+(NSString *)postAuthenticateURLHost;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,36 @@
//
// NSBundle+HotPocketExtensions.m
// HotPocket
//
// Created by Tomek Wójcik on 17/11/2025.
//
#import "NSBundle+HotPocketExtensions.h"
@implementation NSBundle (HotPocketExtensions)
+(NSString *)uname {
NSBundle *mainBundle = [NSBundle mainBundle];
return [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
}
+(NSDictionary *)postAuthenticateURLParams {
NSDictionary *result = [[[NSBundle mainBundle] infoDictionary] valueForKey:@"HPAuthFlowPostAuthenticateURLParts"];
if (result == nil) {
return [NSDictionary dictionary];
}
return result;
}
+(NSString *)postAuthenticateURLScheme {
NSDictionary *params = [self postAuthenticateURLParams];
return [params valueForKey:@"scheme"];
}
+(NSString *)postAuthenticateURLHost {
NSDictionary *params = [self postAuthenticateURLParams];
return [params valueForKey:@"host"];
}
@end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 68 KiB

View File

@@ -5,10 +5,10 @@
// Created by Tomek Wójcik on 21/08/2025.
//
#import "SafariWebExtensionHandler.h"
#import <SafariServices/SafariServices.h>
#import "SafariWebExtensionHandler.h"
@implementation SafariWebExtensionHandler
- (void)beginRequestWithExtensionContext:(NSExtensionContext *)context {

View File

@@ -6,12 +6,19 @@
//
#import <UIKit/UIKit.h>
#import <AuthenticationServices/AuthenticationServices.h>
NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationProgressViewController : UIViewController
@class MultilineLabel;
@interface AuthorizationProgressViewController : UIViewController<ASWebAuthenticationPresentationContextProviding>
@property IBOutlet UIActivityIndicatorView *progressIndicator;
@property IBOutlet MultilineLabel *progressLabel;
@property (strong, nullable) NSURL *authorizationURL;
@property (strong, nullable) ASWebAuthenticationSession *webAuthenticationSession;
@property BOOL userCancelledSession;
@end

View File

@@ -8,20 +8,36 @@
#import "AuthorizationProgressViewController.h"
#import "AppDelegate.h"
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h"
#import "MultilineLabel.h"
#import "NSBundle+HotPocketExtensions.h"
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
#pragma mark - Private interface
-(void)presentAuthorizationError;
@end
@implementation AuthorizationProgressViewController
#pragma mark - View lifecycle
-(instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
self.authorizationURL = nil;
self.webAuthenticationSession = nil;
self.userCancelledSession = NO;
}
return self;
}
-(void)viewDidLoad {
[super viewDidLoad];
self.progressLabel.text = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser...");
}
-(void)viewWillAppear:(BOOL)animated {
@@ -29,29 +45,74 @@
[self.progressIndicator startAnimating];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAuthFlowDidFinish:) name:@"AuthFlowDidFinish" object:appDelegate.authFlow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidFinish:)
name:@"AuthFlowDidFinish"
object:appDelegate.authFlow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidReceiveAuthParams:)
name:@"AuthFlowDidReceiveAuthParams"
object:appDelegate.authFlow];
}
-(void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[[NSNotificationCenter defaultCenter] removeObserver:self];
-(void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
#ifdef DEBUG
NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error);
#endif
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
self.userCancelledSession = YES;
}
[self presentAuthorizationError];
} else {
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams != nil) {
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
} else {
[self presentAuthorizationError];
}
}
self.webAuthenticationSession = nil;
});
};
ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]];
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL
callback:callback
completionHandler:completionHandler];
self.webAuthenticationSession.presentationContextProvider = self;
#ifdef DEBUG
self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES;
#endif
if (self.webAuthenticationSession.canStart == NO) {
[self presentAuthorizationError];
return;
}
[self.webAuthenticationSession start];
}
-(void)viewDidDisappear:(BOOL)animated {
[super viewDidDisappear:animated];
self.webAuthenticationSession = nil;
[self.progressIndicator stopAnimating];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Notification handlers
# pragma mark - Private interface
-(void)onAuthFlowDidFinish:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
#ifdef DEBUG
NSLog(@"-[AuthorizationViewController onAuthFlowDidFinish:] notification=`%@`", notification);
#endif
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
if (credentials.usable == NO) {
-(void)presentAuthorizationError {
if (self.userCancelledSession == NO) {
UIAlertController *alert = [UIAlertController alertControllerWithTitle:NSLocalizedString(@"Oops!", @"Oops!")
message:NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.")
preferredStyle:UIAlertControllerStyleAlert];
@@ -65,10 +126,36 @@
}]];
[self presentViewController:alert animated:YES completion:nil];
} else {
[self.navigationController popViewControllerAnimated:YES];
}
}
#pragma mark - Notification handlers
-(void)onAuthFlowDidFinish:(NSNotification *)notification {
dispatch_async(dispatch_get_main_queue(), ^{
#ifdef DEBUG
NSLog(@"-[AuthorizationViewController onAuthFlowDidFinish:] notification=`%@`", notification);
#endif
HPCredentials *credentials = [[HPCredentialsHelper sharedHelper] getCredentials];
if (credentials.usable == NO) {
[self presentAuthorizationError];
} else {
[self.navigationController popToRootViewControllerAnimated:YES];
}
});
}
-(void)onAuthFlowDidReceiveAuthParams:(NSNotification *)notification {
self.progressLabel.text = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
}
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return self.view.window;
}
@end

View File

@@ -9,11 +9,14 @@
NS_ASSUME_NONNULL_BEGIN
@class UnameLabel;
@interface AuthorizationViewController : UIViewController
@property UIImageView *invalidURLWarningView;
@property IBOutlet UITextField *instanceURLField;
@property IBOutlet UnameLabel *unameLabel;
-(IBAction)doStartAuthorizationFlow:(id)sender;

View File

@@ -12,7 +12,9 @@
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h"
#import "MainViewController.h"
#import "NSBundle+HotPocketExtensions.h"
#import "NSURL+HotPocketExtensions.h"
#import "UnameLabel.h"
@interface AuthorizationViewController (AuthorizationViewControllerPrivate)
@@ -31,6 +33,8 @@
self.invalidURLWarningView.contentMode = UIViewContentModeScaleAspectFit;
self.invalidURLWarningView.frame = CGRectMake(0, 0, 16, 16);
self.invalidURLWarningView.tintColor = [UIColor colorNamed:@"WarningColor"];
self.unameLabel.text = [NSBundle uname];
}
-(void)viewWillAppear:(BOOL)animated {
@@ -69,14 +73,9 @@
return;
}
if ([application canOpenURL:authURL] == YES) {
[application openURL:authURL options:@{} completionHandler:^(BOOL result) {
if (result == YES) {
AuthorizationProgressViewController *authorizationProgressViewController = [self.storyboard instantiateViewControllerWithIdentifier:@"AuthorizationProgressViewController"];
authorizationProgressViewController.authorizationURL = authURL;
[self.navigationController pushViewController:authorizationProgressViewController animated:YES];
}
}];
}
}
#pragma mark - Event handlers

View File

@@ -77,6 +77,13 @@
<action selector="doLogOut:" destination="BYZ-38-t0r" eventType="primaryActionTriggered" id="iq7-wK-GMu"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SD4-ZJ-wLU" userLabel="uname Label" customClass="UnameLabel">
<rect key="frame" x="20" y="855" width="374" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" name="BackgroundColor"/>
@@ -86,6 +93,7 @@
<connections>
<outlet property="instanceURLButton" destination="OPO-AY-zgd" id="1Wr-H9-eZ6"/>
<outlet property="logoutButton" destination="wQZ-n6-b0o" id="vco-vP-zvy"/>
<outlet property="unameLabel" destination="SD4-ZJ-wLU" id="LLk-wO-epu"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
@@ -136,7 +144,7 @@
<action selector="doStartAuthorizationFlow:" destination="1Il-xJ-X5Y" eventType="primaryActionTriggered" id="Rd9-1f-N6Z"/>
</connections>
</textField>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Enter the URL to your HotPocket instance, e.g. https://my.hotpocket.app" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tn1-fl-daL" customClass="MultilineLabel">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Enter the URL to your HotPocket instance, e.g. https://hotpocket.yourcompany.com/" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tn1-fl-daL" customClass="MultilineLabel">
<rect key="frame" x="20" y="348" width="374" height="64"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
@@ -152,6 +160,13 @@
<action selector="doStartAuthorizationFlow:" destination="1Il-xJ-X5Y" eventType="primaryActionTriggered" id="U0V-Pp-M2x"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="gId-nt-VtS" userLabel="uname Label" customClass="UnameLabel">
<rect key="frame" x="20" y="855" width="374" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="dL2-4T-yXY"/>
<color key="backgroundColor" name="BackgroundColor"/>
@@ -159,6 +174,7 @@
</view>
<connections>
<outlet property="instanceURLField" destination="v5s-Uh-qWU" id="hRQ-r8-3Dz"/>
<outlet property="unameLabel" destination="gId-nt-VtS" id="ust-gO-7YN"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="m6b-Bm-Ty7" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
@@ -213,8 +229,8 @@
<rect key="frame" x="189" y="306" width="37" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Awaiting authentication response..." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qiJ-yx-nMd">
<rect key="frame" x="20" y="359" width="374" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Awaiting authentication response..." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="qiJ-yx-nMd" customClass="MultilineLabel">
<rect key="frame" x="20" y="359" width="374" height="64"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -226,6 +242,7 @@
</view>
<connections>
<outlet property="progressIndicator" destination="DNy-gf-n60" id="hJF-jc-ZJ0"/>
<outlet property="progressLabel" destination="qiJ-yx-nMd" id="1Wu-em-XsK"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="N3D-cM-5Ro" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>

View File

@@ -9,10 +9,13 @@
NS_ASSUME_NONNULL_BEGIN
@class UnameLabel;
@interface MainViewController : UIViewController
@property IBOutlet UIButton *instanceURLButton;
@property IBOutlet UIButton *logoutButton;
@property IBOutlet UnameLabel *unameLabel;
-(IBAction)doOpenInstanceURL:(id)sender;
-(IBAction)doLogOut:(id)sender;

View File

@@ -7,9 +7,10 @@
#import "MainViewController.h"
#import "HPCredentialsHelper.h"
#import "AuthorizationViewController.h"
#import "HPCredentialsHelper.h"
#import "NSBundle+HotPocketExtensions.h"
#import "UnameLabel.h"
@interface MainViewController (MainViewControllerPrivate)
@@ -27,6 +28,8 @@
[self.instanceURLButton setTitle:@"" forState:UIControlStateNormal];
self.instanceURLButton.enabled = NO;
self.unameLabel.text = [NSBundle uname];
self.logoutButton.enabled = NO;
}

View File

@@ -0,0 +1,16 @@
//
// UnameLabel.h
// HotPocket
//
// Created by Tomek Wójcik on 17/11/2025.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UnameLabel : UILabel
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,16 @@
//
// UnameLabel.m
// HotPocket
//
// Created by Tomek Wójcik on 17/11/2025.
//
#import "UnameLabel.h"
NS_ASSUME_NONNULL_BEGIN
@implementation UnameLabel
@end
NS_ASSUME_NONNULL_END

View File

@@ -6,6 +6,7 @@
//
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {

View File

@@ -8,6 +8,7 @@
#import <UIKit/UIKit.h>
@class HPAPI;
@class UnameLabel;
@interface ShareViewController : UIViewController
@@ -19,7 +20,7 @@
@property IBOutlet UIView *doneView;
@property IBOutlet UIView *errorView;
@property IBOutlet UIView *unprocessableEntityView;
@property IBOutlet UILabel *unameLabel;
@property IBOutlet UnameLabel *unameLabel;
-(IBAction)doCancel:(id)sender;
-(IBAction)doClose:(id)sender;

View File

@@ -12,6 +12,8 @@
#import "HPAPI.h"
#import "HPCredentialsHelper.h"
#import "HPShareExtensionHelper.h"
#import "NSBundle+HotPocketExtensions.h"
#import "UnameLabel.h"
@implementation ShareViewController (ShareViewControllerPrivate)
@@ -68,8 +70,7 @@
self.errorView.hidden = YES;
self.unprocessableEntityView.hidden = YES;
NSBundle *mainBundle = [NSBundle mainBundle];
self.unameLabel.text = [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
self.unameLabel.text = [NSBundle uname];
self.api = [[HPAPI alloc] init];
if (self.api.rpcClient.hasCredentials == YES) {

View File

@@ -21,18 +21,6 @@
}
-(void)application:(NSApplication *)application openURLs:(NSArray<NSURL *> *)urls {
HPAuthParams *receivedAuthParams = nil;
for (NSURL *url in urls) {
receivedAuthParams = [self.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams != nil) {
break;
}
}
if (receivedAuthParams != nil) {
[self.authFlow handleAuthParams:receivedAuthParams];
}
}
@end

View File

@@ -6,12 +6,19 @@
//
#import <Cocoa/Cocoa.h>
#import <AuthenticationServices/AuthenticationServices.h>
NS_ASSUME_NONNULL_BEGIN
@interface AuthorizationProgressViewController : NSViewController
@interface AuthorizationProgressViewController : NSViewController<ASWebAuthenticationPresentationContextProviding>
@property IBOutlet NSProgressIndicator *progressIndicator;
@property NSString *progressLabelTitle;
@property (nullable, strong) NSURL *authorizationURL;
@property (nullable, strong) ASWebAuthenticationSession *webAuthenticationSession;
@property BOOL userCancelledSession;
-(IBAction)doCancel:(id)sender;
@end

View File

@@ -9,37 +9,133 @@
#import "AppDelegate.h"
#import "AuthorizationViewController.h"
#import "HPAuthFlow.h"
#import "HPCredentialsHelper.h"
#import "MainViewController.h"
#import "NSBundle+HotPocketExtensions.h"
#import "ReplaceAnimator.h"
@interface AuthorizationProgressViewController (AuthorizationProgressViewControllerPrivate)
#pragma mark - Private interface
-(void)presentAuthorizationError;
@end
@implementation AuthorizationProgressViewController
#pragma mark - View lifecycle
-(instancetype)initWithCoder:(NSCoder *)coder {
if (self = [super initWithCoder:coder]) {
self.authorizationURL = nil;
self.webAuthenticationSession = nil;
self.userCancelledSession = NO;
}
return self;
}
-(void)viewDidLoad {
[super viewDidLoad];
self.progressLabelTitle = NSLocalizedString(@"Continue to sign in in your browser...", @"Continue to sign in in your browser...");
}
-(void)viewWillAppear {
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(onAuthFlowDidFinish:) name:@"AuthFlowDidFinish" object:appDelegate.authFlow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidFinish:)
name:@"AuthFlowDidFinish"
object:appDelegate.authFlow];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(onAuthFlowDidReceiveAuthParams:)
name:@"AuthFlowDidReceiveAuthParams"
object:appDelegate.authFlow];
[self.progressIndicator startAnimation:self];
}
-(void)viewDidAppear {
[super viewDidAppear];
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
ASWebAuthenticationSessionCompletionHandler completionHandler = ^(NSURL *url, NSError *error) {
dispatch_async(dispatch_get_main_queue(), ^{
if (error != nil) {
#ifdef DEBUG
NSLog(@"[AuthorizationViewController.session completionHandler] error=`%@`", error);
#endif
if (error.code == ASWebAuthenticationSessionErrorCodeCanceledLogin) {
self.userCancelledSession = YES;
}
[self presentAuthorizationError];
} else {
HPAuthParams *receivedAuthParams = [appDelegate.authFlow handlePostAuthenticateURL:url];
if (receivedAuthParams != nil) {
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
[appDelegate.authFlow handleAuthParams:receivedAuthParams];
} else {
[self presentAuthorizationError];
}
}
self.webAuthenticationSession = nil;
});
};
ASWebAuthenticationSessionCallback *callback = [ASWebAuthenticationSessionCallback callbackWithCustomScheme:[NSBundle postAuthenticateURLScheme]];
self.webAuthenticationSession = [[ASWebAuthenticationSession alloc] initWithURL:self.authorizationURL
callback:callback
completionHandler:completionHandler];
self.webAuthenticationSession.presentationContextProvider = self;
#ifdef DEBUG
self.webAuthenticationSession.prefersEphemeralWebBrowserSession = YES;
#endif
if (self.webAuthenticationSession.canStart == NO) {
[self presentAuthorizationError];
return;
}
[self.webAuthenticationSession start];
}
-(void)viewDidDisappear {
[super viewDidDisappear];
self.webAuthenticationSession = nil;
[self.progressIndicator stopAnimation:self];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
#pragma mark - Actions
-(IBAction)doCancel:(id)sender {
[self.webAuthenticationSession cancel];
}
#pragma mark - Private interface
-(void)presentAuthorizationError {
if (self.userCancelledSession == NO) {
NSAlert *alert = [[NSAlert alloc] init];
alert.alertStyle = NSAlertStyleCritical;
alert.messageText = NSLocalizedString(@"Oops!", @"Oops!");
alert.informativeText = NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.");
[alert beginSheetModalForWindow:self.view.window completionHandler:^(NSModalResponse response) {
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
}];
} else {
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
}
}
#pragma mark - Notification handlers
-(void)onAuthFlowDidFinish:(NSNotification *)notification {
@@ -50,14 +146,7 @@
[[NSApplication sharedApplication] activateIgnoringOtherApps:YES];
if (credentials.usable == NO) {
NSAlert *alert = [[NSAlert alloc] init];
alert.alertStyle = NSAlertStyleCritical;
alert.messageText = NSLocalizedString(@"Oops!", @"Oops!");
alert.informativeText = NSLocalizedString(@"HotPocket couldn't complete this operation.", @"HotPocket couldn't complete this operation.");
[alert runModal];
AuthorizationViewController *authorizationViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationViewController"];
[self presentViewController:authorizationViewController animator:[[ReplaceAnimator alloc] init]];
[self presentAuthorizationError];
} else {
MainViewController *mainViewController = [self.storyboard instantiateControllerWithIdentifier:@"MainViewController"];
[self presentViewController:mainViewController animator:[[ReplaceAnimator alloc] init]];
@@ -65,4 +154,14 @@
});
}
-(void)onAuthFlowDidReceiveAuthParams:(NSNotification *)notification {
self.progressLabelTitle = NSLocalizedString(@"Processing authorization...", @"Processing authorization...");
}
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return self.view.window;
}
@end

View File

@@ -8,8 +8,8 @@
#import "AuthorizationViewController.h"
#import "AppDelegate.h"
#import "HPAuthFlow.h"
#import "AuthorizationProgressViewController.h"
#import "HPAuthFlow.h"
#import "ReplaceAnimator.h"
@interface AuthorizationViewController (AuthorizationViewControllerPrivate)
@@ -31,19 +31,24 @@
#pragma mark - Actions
-(IBAction)doStartAuthorizationFlow:(id)sender {
AppDelegate *appDeleate = [[NSApplication sharedApplication] delegate];
appDeleate.authFlow.baseURL = [NSURL URLWithString:self.baseURL];
AppDelegate *appDelegate = [[NSApplication sharedApplication] delegate];
appDelegate.authFlow.baseURL = [NSURL URLWithString:self.baseURL];
NSURL *authURL = [appDeleate.authFlow start];
NSURL *authURL = [appDelegate.authFlow start];
if (authURL == nil) {
NSBeep();
return;
}
AuthorizationProgressViewController *authProgressViewController = [self.storyboard instantiateControllerWithIdentifier:@"AuthorizationProgressViewController"];
authProgressViewController.authorizationURL = authURL;
[self presentViewController:authProgressViewController animator:[[ReplaceAnimator alloc] init]];
}
[[NSWorkspace sharedWorkspace] openURL:authURL];
# pragma mark - ASWebAuthenticationPresentationContextProviding implementation
-(ASPresentationAnchor)presentationAnchorForWebAuthenticationSession:(ASWebAuthenticationSession *)session {
return self.view.window;
}
@end

View File

@@ -103,7 +103,7 @@
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7sM-F3-Zzf">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7sM-F3-Zzf">
<rect key="frame" x="18" y="153" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="HotPocket Instance URL" id="XwM-DV-kei">
@@ -112,7 +112,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ygC-xe-m6y">
<textField verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="ygC-xe-m6y">
<rect key="frame" x="20" y="124" width="385" height="21"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" drawsBackground="YES" id="rHK-hP-yWO">
@@ -128,10 +128,10 @@
</binding>
</connections>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DIc-8O-uoQ">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="DIc-8O-uoQ">
<rect key="frame" x="18" y="68" width="389" height="48"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="Enter the URL to your HotPocket instance, e.g. https://my.hotpocket.app" id="Y0q-a1-oBP">
<textFieldCell key="cell" selectable="YES" title="Enter the URL to your HotPocket instance, e.g. https://hotpocket.yourcompany.com/" id="Y0q-a1-oBP">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
@@ -154,7 +154,7 @@
<action selector="doStartAuthorizationFlow:" target="XfG-lQ-9wD" id="AOi-Wt-gmL"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mQc-Ea-NNN">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="mQc-Ea-NNN">
<rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="NTZ-zl-yhk">
@@ -177,7 +177,7 @@
<rect key="frame" x="0.0" y="0.0" width="425" height="325"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yRj-hC-QYS">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="yRj-hC-QYS">
<rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="F4l-2Z-D79">
@@ -195,15 +195,32 @@
<rect key="frame" x="196" y="113" width="32" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
</progressIndicator>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g9a-gR-c7o">
<rect key="frame" x="18" y="81" width="389" height="16"/>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="g9a-gR-c7o">
<rect key="frame" x="18" y="49" width="389" height="48"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="Awaiting authorization response..." id="3oi-LK-vKv">
<font key="font" metaFont="system"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
<connections>
<binding destination="OX4-Oj-1cw" name="value" keyPath="self.progressLabelTitle" id="ydU-jy-p3F"/>
</connections>
</textField>
<button verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Sip-bU-V5i">
<rect key="frame" x="175" y="14" width="76" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="nYJ-LO-V7O">
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
<font key="font" metaFont="system"/>
<string key="keyEquivalent" base64-UTF8="YES">
Gw
</string>
</buttonCell>
<connections>
<action selector="doCancel:" target="OX4-Oj-1cw" id="nAC-If-aib"/>
</connections>
</button>
</subviews>
</view>
<connections>
@@ -227,7 +244,7 @@
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="icon-mac-384" id="fae-mz-0sj"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T7q-KB-3Ut">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="T7q-KB-3Ut">
<rect key="frame" x="18" y="185" width="389" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="r5O-Sk-IdK">
@@ -236,7 +253,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2h7-bN-dsa">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="2h7-bN-dsa">
<rect key="frame" x="18" y="153" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="HotPocket is configured and ready." id="5fh-mh-WR1">
@@ -245,7 +262,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uci-UC-wxo">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="uci-UC-wxo">
<rect key="frame" x="18" y="89" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Instance URL" id="azk-ea-KeN">
@@ -267,8 +284,8 @@
<binding destination="r5D-xE-cNT" name="enabled" keyPath="self.logoutButtonEnabled" id="gTs-BO-USz"/>
</connections>
</button>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8H3-oU-acU" customClass="LinkLabel">
<rect key="frame" x="18" y="65" width="389" height="16"/>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="8H3-oU-acU" customClass="LinkLabel">
<rect key="frame" x="18" y="69" width="389" height="16"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" allowsEditingTextAttributes="YES" id="EoA-mM-phM">
<font key="font" metaFont="system"/>
@@ -276,7 +293,7 @@
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9pl-Ap-yxc">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9pl-Ap-yxc">
<rect key="frame" x="18" y="113" width="389" height="32"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" title="Safari and Share Extensions are installed." id="dy7-bw-DYh">

View File

@@ -7,8 +7,8 @@
#import "MainViewController.h"
#import "HPCredentialsHelper.h"
#import "AuthorizationViewController.h"
#import "HPCredentialsHelper.h"
#import "ReplaceAnimator.h"
@interface MainViewController (MainViewControllerPrivate)

View File

@@ -23,7 +23,7 @@
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxX="YES" flexibleMinY="YES"/>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyDown" image="icon-mac-384" id="NT0-XU-t9f"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KZW-gY-pvX">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="KZW-gY-pvX">
<rect key="frame" x="18" y="138" width="352" height="28"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" lineBreakMode="clipping" alignment="center" title="HotPocket by BTHLabs" id="urI-Z1-yMm">
@@ -50,7 +50,7 @@ Gw
<action selector="cancel:" target="-2" id="yRt-GR-jQ6"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5X8-4n-wWm">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="5X8-4n-wWm">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="HotPocket couldn't complete this operation." id="fmg-RT-3FA">
@@ -94,7 +94,7 @@ Gw
<action selector="close:" target="-2" id="3aP-Lu-EzX"/>
</connections>
</button>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Mfx-pW-oi2">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Mfx-pW-oi2">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="Your link has been saved!" id="JhJ-K4-UFb">
@@ -118,7 +118,7 @@ Gw
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="exclamationmark.circle.fill" catalog="system" id="3kO-Gq-csg"/>
<color key="contentTintColor" name="WarningColor"/>
</imageView>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YLC-Bx-qKZ">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="YLC-Bx-qKZ">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="Open the HotPocket application to set it up." id="eYb-eq-cbo">
@@ -183,7 +183,7 @@ Gw
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" animates="YES" imageScaling="proportionallyUpOrDown" image="exclamationmark.circle.fill" catalog="system" id="66K-cT-2Vw"/>
<color key="contentTintColor" name="WarningColor"/>
</imageView>
<textField focusRingType="none" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LS4-qN-h75">
<textField verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="LS4-qN-h75">
<rect key="frame" x="-2" y="28" width="352" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" selectable="YES" alignment="center" title="This item couldn't be shared :(." id="b0i-Lf-21f">
@@ -211,7 +211,7 @@ Gw
<binding destination="-2" name="hidden" keyPath="self.unprocessableEntityViewHidden" id="lqC-lO-ll8"/>
</connections>
</customView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1yJ-sU-Spr" userLabel="uname Label">
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="1yJ-sU-Spr" userLabel="uname Label">
<rect key="frame" x="6" y="4" width="376" height="14"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" flexibleMinY="YES"/>
<textFieldCell key="cell" controlSize="small" lineBreakMode="clipping" alignment="center" id="nQ0-Es-oIB">
@@ -233,7 +233,7 @@ Gw
<image name="icon-mac-384" width="384" height="384"/>
<image name="multiply.circle.fill" catalog="system" width="15" height="15"/>
<namedColor name="DangerColor">
<color red="0.93300002813339233" green="0.3919999897480011" blue="0.46299999952316284" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.93333333333333335" green="0.39215686274509803" blue="0.46274509803921571" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
<namedColor name="SuccessColor">
<color red="0.054901960784313725" green="0.65490196078431373" blue="0.40392156862745099" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>

View File

@@ -9,6 +9,7 @@
#import "HPAPI.h"
#import "HPShareExtensionHelper.h"
#import "NSBundle+HotPocketExtensions.h"
@implementation ShareViewController (ShareViewControllerPrivate)
@@ -69,8 +70,7 @@
self.errorViewHidden = YES;
self.unprocessableEntityViewHidden = YES;
NSBundle *mainBundle = [NSBundle mainBundle];
self.uname = [NSString stringWithFormat:@"HotPocket v%@ (%@)", [mainBundle.infoDictionary valueForKey:@"CFBundleShortVersionString"], [mainBundle.infoDictionary valueForKey:@"CFBundleVersion"]];
self.uname = [NSBundle uname];
self.api = [[HPAPI alloc] init];
if (self.api.rpcClient.hasCredentials == YES) {

View File

@@ -56,40 +56,6 @@ files = [
[package.extras]
tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich ; python_version >= \"3.11\""]
[[package]]
name = "factory-boy"
version = "3.3.3"
description = "A versatile test fixtures replacement based on thoughtbot's factory_bot for Ruby."
optional = false
python-versions = ">=3.8"
groups = ["dev"]
files = [
{file = "factory_boy-3.3.3-py2.py3-none-any.whl", hash = "sha256:1c39e3289f7e667c4285433f305f8d506efc2fe9c73aaea4151ebd5cdea394fc"},
{file = "factory_boy-3.3.3.tar.gz", hash = "sha256:866862d226128dfac7f2b4160287e899daf54f2612778327dd03d0e2cb1e3d03"},
]
[package.dependencies]
Faker = ">=0.7.0"
[package.extras]
dev = ["Django", "Pillow", "SQLAlchemy", "coverage", "flake8", "isort", "mongoengine", "mongomock", "mypy", "tox", "wheel (>=0.32.0)", "zest.releaser[recommended]"]
doc = ["Sphinx", "sphinx-rtd-theme", "sphinxcontrib-spelling"]
[[package]]
name = "faker"
version = "37.11.0"
description = "Faker is a Python package that generates fake data for you."
optional = false
python-versions = ">=3.9"
groups = ["dev"]
files = [
{file = "faker-37.11.0-py3-none-any.whl", hash = "sha256:1508d2da94dfd1e0087b36f386126d84f8583b3de19ac18e392a2831a6676c57"},
{file = "faker-37.11.0.tar.gz", hash = "sha256:22969803849ba0618be8eee2dd01d0d9e2cd3b75e6ff1a291fa9abcdb34da5e6"},
]
[package.dependencies]
tzdata = "*"
[[package]]
name = "flake8"
version = "7.3.0"
@@ -127,7 +93,7 @@ name = "hotpocket-workspace-tools"
version = "1.0.0.dev0"
description = "HotPocket Workspace Tools"
optional = false
python-versions = "^3.12"
python-versions = "^3.13"
groups = ["dev"]
files = []
develop = true
@@ -169,35 +135,35 @@ ipython = {version = ">=7.31.1", markers = "python_version >= \"3.11\""}
[[package]]
name = "ipython"
version = "9.6.0"
version = "9.7.0"
description = "IPython: Productive Interactive Computing"
optional = false
python-versions = ">=3.11"
groups = ["dev"]
files = [
{file = "ipython-9.6.0-py3-none-any.whl", hash = "sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196"},
{file = "ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731"},
{file = "ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f"},
{file = "ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
decorator = "*"
ipython-pygments-lexers = "*"
jedi = ">=0.16"
matplotlib-inline = "*"
colorama = {version = ">=0.4.4", markers = "sys_platform == \"win32\""}
decorator = ">=4.3.2"
ipython-pygments-lexers = ">=1.0.0"
jedi = ">=0.18.1"
matplotlib-inline = ">=0.1.5"
pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
prompt_toolkit = ">=3.0.41,<3.1.0"
pygments = ">=2.4.0"
stack_data = "*"
pygments = ">=2.11.0"
stack_data = ">=0.6.0"
traitlets = ">=5.13.0"
[package.extras]
all = ["ipython[doc,matplotlib,test,test-extra]"]
black = ["black"]
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[matplotlib,test]", "setuptools (>=61.2)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"]
matplotlib = ["matplotlib (>3.7)"]
test = ["packaging", "pytest", "pytest-asyncio", "testpath"]
test-extra = ["curio", "ipykernel", "ipython[matplotlib]", "ipython[test]", "jupyter_ai", "nbclient", "nbformat", "numpy (>=1.25)", "pandas (>2.0)", "trio"]
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[matplotlib,test]", "setuptools (>=70.0)", "sphinx (>=8.0)", "sphinx-rtd-theme (>=0.1.8)", "sphinx_toml (==0.0.4)", "typing_extensions"]
matplotlib = ["matplotlib (>3.9)"]
test = ["packaging (>=20.1.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=1.0.0)", "setuptools (>=61.2)", "testpath (>=0.2)"]
test-extra = ["curio", "ipykernel (>6.30)", "ipython[matplotlib]", "ipython[test]", "jupyter_ai", "nbclient", "nbformat", "numpy (>=1.27)", "pandas (>2.1)", "trio (>=0.1.0)"]
[[package]]
name = "ipython-pygments-lexers"
@@ -523,18 +489,6 @@ files = [
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
[[package]]
name = "tzdata"
version = "2025.2"
description = "Provider of IANA time zone data"
optional = false
python-versions = ">=2"
groups = ["dev"]
files = [
{file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"},
{file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"},
]
[[package]]
name = "wcwidth"
version = "0.2.14"
@@ -549,5 +503,5 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = "^3.12"
content-hash = "b03ef0369277d183d033049206a4cfd5f450473179995a4a79165fbb84d81fa0"
python-versions = "^3.13"
content-hash = "b38eaf0455b1239589b6f8d89e40bee3410324fcf073d4513912166429713dfe"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "hotpocket-apple"
version = "25.11.06"
version = "26.3.16"
description = "HotPocket Apple Integrations"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0"
@@ -8,15 +8,14 @@ readme = "README.md"
package-mode = false
[tool.poetry.dependencies]
python = "^3.12"
python = "^3.13"
[tool.poetry.group.dev.dependencies]
factory-boy = "3.3.3"
flake8 = "7.3.0"
flake8-commas = "4.0.0"
hotpocket-workspace-tools = {path = "../packages/workspace_tools", develop = true}
ipdb = "0.13.13"
ipython = "9.6.0"
ipython = "9.7.0"
isort = "7.0.0"
mypy = "1.18.2"

View File

@@ -29,7 +29,7 @@ def flake8(ctx: Context):
@task
def isort(ctx, check=False, diff=False):
def isort(ctx: Context, check=False, diff=False):
command_parts = [
'isort',
]

View File

@@ -1,8 +1,10 @@
ARG APP_USER_UID=1000
ARG APP_USER_GID=1000
ARG IMAGE_ID=development.00000000
ARG IMAGE_VERSION=v00.00.00
ARG IMAGE_REVISION=00000000
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-node-20251014-01 AS development
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-node-20251114-01 AS development
ARG APP_USER_UID
ARG APP_USER_GID
@@ -12,7 +14,7 @@ COPY --chown=$APP_USER_UID:$APP_USER_GID backend/ops/bin/*.sh /srv/bin/
VOLUME ["/srv/node_modules", "/srv/venv"]
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-python-20251014-01 AS deployment-build
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:build-python-20251114-01 AS deployment-build
ARG APP_USER_UID
ARG APP_USER_GID
@@ -25,13 +27,13 @@ COPY --chown=$APP_USER_UID:$APP_USER_GID packages/common/ /srv/packages/common/
COPY --chown=$APP_USER_UID:$APP_USER_GID packages/soa/ /srv/packages/soa/
RUN poetry install --only main,deployment && \
minify -i --css-precision 0 --js-precision 0 --js-version 2022 hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend*.css hotpocket_backend/apps/ui/static/ui/js/hotpocket*.js && \
minify -i --css-precision 0 --js-precision 0 --js-version 2022 hotpocket_backend/apps/ui/static/ui/css/hotpocket-backend*.css hotpocket_backend/apps/ui/static/ui/js/hotpocket-backend*.js && \
./manage.py collectstatic --settings hotpocket_backend.settings.deployment.build --noinput && \
find hotpocket_backend/static/ -name "*.map*" -delete && \
rm -f hotpocket_backend/settings/deployment/build.py && \
rm -rf node_modules/
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:base-20251014-01 AS deployment-base
FROM docker-hosted.nexus.bthlabs.pl/hotpocket/base:base-20251114-01 AS deployment-base
ARG APP_USER_UID
ARG APP_USER_GID
@@ -63,6 +65,20 @@ CMD ["/srv/venv/bin/gunicorn", "-c", "/srv/lib/gunicorn.conf.py", "hotpocket_bac
FROM deployment-base AS deployment
ARG IMAGE_VERSION
ARG IMAGE_REVISION
LABEL org.opencontainers.image.authors="Tomek Wójcik <contact@bthlabs.pl>"
LABEL org.opencontainers.image.url="https://git.bthlabs.pl/tomekwojcik/hotpocket"
LABEL org.opencontainers.image.documentation="https://git.bthlabs.pl/tomekwojcik/hotpocket"
LABEL org.opencontainers.image.source="https://git.bthlabs.pl/tomekwojcik/hotpocket.git"
LABEL org.opencontainers.image.version="${IMAGE_VERSION}"
LABEL org.opencontainers.image.revision="${IMAGE_REVISION}"
LABEL org.opencontainers.image.vendor="BTHLabs <contact@bthlabs.pl>"
LABEL org.opencontainers.image.title="HotPocket by BTHLabs"
LABEL org.opencontainers.image.description="Minimal self-hosted bookmarking app :)"
LABEL org.opencontainers.image.licenses="Apache-2.0"
ARG APP_USER_UID
ARG APP_USER_GID
ARG IMAGE_ID
@@ -77,6 +93,20 @@ VOLUME ["/srv/run", "/srv/uploads"]
FROM deployment-base AS aio
ARG IMAGE_VERSION
ARG IMAGE_REVISION
LABEL org.opencontainers.image.authors="Tomek Wójcik <contact@bthlabs.pl>"
LABEL org.opencontainers.image.url="https://git.bthlabs.pl/tomekwojcik/hotpocket"
LABEL org.opencontainers.image.documentation="https://git.bthlabs.pl/tomekwojcik/hotpocket"
LABEL org.opencontainers.image.source="https://git.bthlabs.pl/tomekwojcik/hotpocket.git"
LABEL org.opencontainers.image.version="${IMAGE_VERSION}"
LABEL org.opencontainers.image.revision="${IMAGE_REVISION}"
LABEL org.opencontainers.image.vendor="BTHLabs <contact@bthlabs.pl>"
LABEL org.opencontainers.image.title="BTHLabs Docker Bastion"
LABEL org.opencontainers.image.description="Minimal self-hosted bookmarking app :)"
LABEL org.opencontainers.image.licenses="Apache-2.0"
ARG APP_USER_UID
ARG APP_USER_GID
ARG IMAGE_ID

View File

@@ -1,4 +1,4 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
version = '25.11.06'
version = '26.3.16'

View File

@@ -1,2 +1,3 @@
from .access_token import AccessToken # noqa: F401
from .account import AccountAdmin # noqa: F401
from .auth_key import AuthKey # noqa: F401

View File

@@ -2,16 +2,41 @@
from __future__ import annotations
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.accounts.models import AccessToken
class AccessTokenAdmin(admin.ModelAdmin):
list_display = ('pk', 'account_uuid', 'origin', 'created_at', 'is_active')
list_display = (
'pk', 'account_uuid', 'origin', 'created_at', 'render_is_active',
)
search_fields = ('pk', 'account_uuid', 'key', 'origin')
readonly_fields = (
'pk',
'account_uuid',
'key',
'origin',
'meta',
'created_at',
'deleted_at',
)
ordering = ['-created_at']
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-deleted_at',
)
def render_is_active(self, obj: AccessToken | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(AccessToken, AccessTokenAdmin)

View File

@@ -3,15 +3,26 @@ from __future__ import annotations
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.accounts.models import Account
class AccountAdmin(UserAdmin):
list_display = (*UserAdmin.list_display, 'is_active')
list_display = (*UserAdmin.list_display, 'render_is_active')
ordering = ['username']
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-is_active',
)
def render_is_active(self, obj: Account | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(Account, AccountAdmin)

View File

@@ -0,0 +1,41 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.accounts.models import AuthKey
class AuthKeyAdmin(admin.ModelAdmin):
list_display = (
'pk', 'account_uuid', 'key', 'created_at', 'consumed_at', 'render_is_active',
)
search_fields = ('pk', 'account_uuid', 'key')
readonly_fields = (
'pk',
'account_uuid',
'key',
'consumed_at',
'created_at',
'deleted_at',
)
ordering = ['-created_at']
def has_change_permission(self, request, obj=None):
return False
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-deleted_at',
)
def render_is_active(self, obj: AuthKey | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(AuthKey, AuthKeyAdmin)

View File

@@ -79,3 +79,17 @@ class AuthKeysService:
raise self.NotFound(
f'Auth Key not found: key=`{key}`',
) from exception
def clean_expired_auth_keys(self) -> int:
current_timestamp = now()
cutoff_timestamp = current_timestamp - datetime.timedelta(
seconds=(settings.AUTH_KEY_TTL + 5),
)
deleted, _ = AuthKey.active_objects.\
filter(
created_at__lte=cutoff_timestamp,
).\
delete()
return deleted

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import logging
from celery import shared_task
from django import db
from hotpocket_backend.apps.accounts.services import AuthKeysService
LOGGER = logging.getLogger(__name__)
@shared_task
def clean_expired_auth_keys():
with db.transaction.atomic():
deleted_count = AuthKeysService().clean_expired_auth_keys()
LOGGER.debug(
'Deleted expired AuthKey objects: deleted_count=`%d`', deleted_count,
)

View File

@@ -38,3 +38,5 @@ class PSettings(typing.Protocol):
UI_PAGE_HEAD_INCLUDES: list
UI_PAGE_SCRIPT_INCLUDES: list
OPERATOR_EMAIL: str | None

View File

@@ -1 +1,2 @@
from . import association # noqa: F401
from . import save # noqa: F401

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
from django.contrib import admin
from django.utils.translation import gettext_lazy as _
from hotpocket_backend.apps.saves.models import Association
class AssociationAdmin(admin.ModelAdmin):
list_display = (
'pk', 'account_uuid', 'target', 'created_at', 'render_is_active',
)
search_fields = ('pk', 'account_uuid')
fields = (
'pk',
'account_uuid',
'archived_at',
'starred_at',
'target_meta',
'target_title',
'target_description',
'created_at',
'deleted_at',
)
readonly_fields = (
'pk',
'account_uuid',
'target_meta',
'target',
'archived_at',
'starred_at',
'created_at',
'deleted_at',
)
ordering = ['-created_at']
def has_delete_permission(self, request, obj=None):
return request.user.is_superuser
@admin.display(
description=_('Is Active?'), boolean=True, ordering='-deleted_at',
)
def render_is_active(self, obj: Association | None = None) -> bool | None:
if obj is None:
return None
return obj.is_active
admin.site.register(Association, AssociationAdmin)

View File

@@ -11,6 +11,26 @@ class SaveAdmin(admin.ModelAdmin):
list_display = (
'pk', 'key', 'account_uuid', 'created_at', 'render_is_active',
)
search_fields = ('pk', 'account_uuid', 'url')
fields = (
'pk',
'account_uuid',
'key',
'url',
'title',
'description',
'last_processed_at',
'is_netloc_banned',
'created_at',
'deleted_at',
)
readonly_fields = (
'pk',
'account_uuid',
'key',
'created_at',
'deleted_at',
)
ordering = ['-created_at']
def has_delete_permission(self, request, obj=None):

View File

@@ -0,0 +1,29 @@
# Generated by Django 5.2.8 on 2025-11-18 14:13
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('saves', '0008_alter_save_url'),
]
operations = [
migrations.AlterField(
model_name='save',
name='description',
field=models.CharField(blank=True, db_index=True, default=None, null=True),
),
migrations.AlterField(
model_name='save',
name='title',
field=models.CharField(blank=True, db_index=True, default=None, null=True),
),
migrations.AlterField(
model_name='save',
name='url',
field=models.CharField(db_index=True, default=None, validators=[django.core.validators.URLValidator(schemes=['http', 'https'])]),
),
]

View File

@@ -20,7 +20,7 @@ class Save(Model):
blank=False, null=False, default=None, db_index=True,
)
url = models.CharField(
blank=False, null=False, default=None,
blank=False, null=False, default=None, db_index=True,
validators=[
validators.URLValidator(schemes=['http', 'https']),
],
@@ -29,10 +29,10 @@ class Save(Model):
blank=True, null=True, default=None, editable=False,
)
title = models.CharField(
blank=True, null=True, default=None,
blank=True, null=True, default=None, db_index=True,
)
description = models.CharField(
blank=True, null=True, default=None,
blank=True, null=True, default=None, db_index=True,
)
last_processed_at = models.DateTimeField(
auto_now=False,

View File

@@ -100,3 +100,9 @@ def page_includes(request: HttpRequest) -> dict:
'script': settings.UI_PAGE_SCRIPT_INCLUDES,
},
}
def operator_email(request: HttpRequest) -> dict:
return {
'OPERATOR_EMAIL': settings.OPERATOR_EMAIL,
}

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import annotations
import uuid
import pydantic
class SavesCreateOut(pydantic.BaseModel):
id: uuid.UUID
target_uuid: uuid.UUID
url: pydantic.AnyHttpUrl
def to_rpc(self) -> dict:
return {
'id': self.id,
'target_uuid': self.target_uuid,
'url': str(self.url),
}

View File

@@ -151,7 +151,6 @@ class SettingsForm(Form):
('cosmo', _('Cosmo')),
('solar', _('Solar')),
('sandstone', _('Sandstone')),
('sketchy', _('Sketchy')),
],
)
light_mode = forms.BooleanField(

View File

@@ -3,19 +3,28 @@ from __future__ import annotations
from bthlabs_jsonrpc_core import register_method
from django.http import HttpRequest
from django.urls import reverse
from hotpocket_backend.apps.core.rpc import wrap_soa_errors
from hotpocket_backend.apps.ui.dto.rpc import SavesCreateOut
from hotpocket_backend.apps.ui.services.workflows import CreateSaveWorkflow
from hotpocket_soa.dto.associations import AssociationOut
@register_method(method='saves.create')
@wrap_soa_errors
def create(request: HttpRequest, url: str) -> AssociationOut:
def create(request: HttpRequest, url: str) -> SavesCreateOut:
association = CreateSaveWorkflow().run_rpc(
request=request,
account=request.user,
url=url,
)
return association
result = SavesCreateOut.model_validate({
'id': association.pk,
'target_uuid': association.target_uuid,
'url': request.build_absolute_uri(reverse(
'ui.associations.view', args=(association.pk,),
)),
})
return result

View File

@@ -122,3 +122,7 @@ body.ui-mode-standalone #offcanvas-controls .offcanvas-body {
position: relative;
}
}
.ui-login-logo {
width: 128px;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

View File

@@ -45,7 +45,6 @@
return null;
}
onLoad = (event) => {
console.log('HotPocketApp.onLoad()', event);
for (let pluginSpec of this.plugins) {
if (pluginSpec[1].onLoad) {
pluginSpec[1].onLoad(event);

View File

@@ -1,6 +1,6 @@
{% extends "ui/base.html" %}
{% load crispy_forms_tags i18n ui %}
{% load crispy_forms_tags i18n static ui %}
{% block title %}{% translate 'Log in' %}{% endblock %}
@@ -10,6 +10,11 @@
<div class="container">
<div class="row">
<div class="col col-12 col-md-6 offset-md-3">
<img
alt="HotPocket Icon"
class="d-block ms-auto me-auto ui-login-logo"
src="{% static 'ui/img/icon-mac-384.png' %}"
>
<div class="card">
<div class="card-header text-center">
<p class="fs-3 mb-0">{{ SITE_TITLE }}</p>
@@ -31,6 +36,12 @@
{% blocktranslate %}Log in with {{ HOTPOCKET_OIDC_DISPLAY_NAME }}{% endblocktranslate %}
</a>
{% endif %}
{% if OPERATOR_EMAIL %}
<p class="my-0 mt-2 text-center">
<small>{% blocktranslate %}For support with your account, contact the <a href="mailto:{{ OPERATOR_EMAIL }}">instance operator</a>.{% endblocktranslate %}</small>
</p>
{% endif %}
</div>
</div>
{% include "ui/ui/partials/uname.html" %}

View File

@@ -16,7 +16,7 @@
{% endif %}
</div>
<div class="card-footer d-flex align-items-center">
<a href="{{ association.target.url }}" target="_blank" rel="noopener noreferer"><small>{{ association.target.url|render_url_domain }}</small></a>
<a href="{{ association.target.url }}" target="_blank" rel="noopener noreferrer"><small>{{ association.target.url|render_url_domain }} <i class="bi bi-box-arrow-up-right"></i></small></a>
<div class="ms-auto flex-shrink-0 d-flex align-items-center">
{% if not association.archived_at %}
<div class="spinner-border spinner-border-sm ui-htmx-indicator" role="status">

View File

@@ -2,10 +2,24 @@
{% load i18n static ui %}
{% block title %}{{ association.title }}{% endblock %}
{% block title %}{% if association.title %}{{ association.title }}{% else %}{{ association.target.url }}{% endif %}{% endblock %}
{% block button_bar_class %}d-none{% endblock %}
{% block page_head %}
{{ block.super}}
<meta property="og:title" content="{% if association.title %}{{ association.title }}{% else %}{{ association.target.url }}{% endif %}">
{% if association.description %}
<meta property="og:description" content="{{ association.description|truncatechars:125 }}">
{% endif %}
<meta property="og:image" content="{{ og_card_url }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:url" content="{{ share_url }}">
<meta property="og:type" content="website">
<meta property="og:site_name" content="{{ SITE_TITLE }}">
{% endblock %}
{% block page_body %}
<div id="ViewAssociationView" class="container pb-3">
<p class="display-3 mt-3 mb-0 text-center">
@@ -20,7 +34,7 @@
{% blocktranslate with created_at=association.created_at %}Saved on {{ created_at }}{% endblocktranslate %}
</span>
<br>
<a href="{{ association.target.url }}" target="_blank" rel="noopener noreferer">
<a href="{{ association.target.url }}" target="_blank" rel="noopener noreferrer">
{% translate 'View original' %} <i class="bi bi-box-arrow-up-right"></i>
</a>
</p>
@@ -51,7 +65,7 @@
<a
class="btn btn-secondary btn-sm ui-noscript-show"
href="{{ share_url }}"
rel="noopener noreferer"
rel="noopener noreferrer"
target="_blank"
>
<i class="bi bi-link-45deg"></i> {% translate 'Share link' %}

View File

@@ -130,6 +130,56 @@
{% translate 'Log out' %}
</a>
</li>
<li class="nav-item px-3 d-flex justify-content-center">
{% spaceless %}
<a
class="btn btn-outline-info btn-sm"
href="https://apps.apple.com/pl/app/hotpocket-by-bthlabs/id6752321380"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Safari and Share extension for iOS and macOS' %}"
>
<i class="bi bi-apple"></i>
</a>
<a
class="btn btn-outline-info btn-sm ms-1"
href="https://chromewebstore.google.com/detail/save-to-hotpocket/mkmoejhhgnadmijpgkkioicjmikkkjbd"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Chrome extension' %}"
>
<i class="bi bi-browser-chrome"></i>
</a>
<a
class="btn btn-outline-info btn-sm ms-1"
href="https://addons.mozilla.org/en-GB/firefox/addon/save-to-hotpocket/"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Firefox extension' %}"
>
<i class="bi bi-browser-firefox"></i>
</a>
<a
class="btn btn-outline-info btn-sm ms-1"
data-bs-toggle="modal"
data-bs-target="#modalPWA"
href="#"
title="{% translate 'Android PWA' %}"
>
<i class="bi bi-android2"></i>
</a>
<div class="vr my-1 ms-1 opacity-75"></div>
<a
class="btn btn-outline-info btn-sm ms-1"
href="https://git.bthlabs.pl/tomekwojcik/hotpocket"
rel="noopener noreferrer"
target="_blank"
title="{% translate 'Source code repository' %}"
>
<i class="bi bi-git"></i>
</a>
{% endspaceless %}
</li>
{% else %}
<li class="nav-item">
<a class="nav-link" href="{% url 'ui.accounts.login' %}">
@@ -142,6 +192,38 @@
{% include "ui/ui/partials/uname.html" %}
</div>
</div>
{% if not request.user.is_anonymous %}
<div class="modal modal-fade" id="modalPWA" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="modalPWALabel">
{% translate 'HotPocket on Android and others' %}
</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<p class="mb-1">
{% blocktranslate %}
HotPocket doesn't natively support Android and other systems. However, it's a Progressive Web Application. You can install it from your browser and it'll register itself as share target.
{% endblocktranslate %}
</p>
<p class="mb-0">
{% blocktranslate %}
This is currently supported on Android and Windows 11 (when installed using Edge).
{% endblocktranslate %}
</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">
{% translate 'Cool' %}
</button>
</div>
</div>
</div>
</div>
{% endif %}
{% endblock %}
{% block scripts %}

View File

@@ -3,11 +3,13 @@
{% if save.is_youtube_video %}
<div class="mb-0 d-flex justify-content-center">
<iframe
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allow="accelerometer *; clipboard-write *; encrypted-media *; gyroscope *; picture-in-picture *; web-share *;"
allowfullscreen
class="ui-youtube-iframe"
frameborder="0"
height="200"
referrerpolicy="strict-origin"
scrolling="no"
src="{{ save|render_youtube_embed_url }}"
title="YouTube video player"
width="320"

View File

@@ -1,6 +1,6 @@
<p class="mb-0 mt-2 text-center text-muted ui-uname">
<span>
<a href="https://hotpocket.app/" target="_blank" rel="noopener noreferer">{{ SITE_TITLE }}</a> v{{ VERSION }}
<a href="https://hotpocket.app/" target="_blank" rel="noopener noreferrer">{{ SITE_TITLE }}</a> v{{ VERSION }}
(<code>{{ IMAGE_ID }}</code>)
</span>
<br>

View File

@@ -78,9 +78,13 @@ urlpatterns = [
name='ui.integrations.ios.shortcut',
),
path(
# Turns out PWAs can register a share target in Windows 11 when
# installed through Edge. Neat, too. I wish I knew this when I defined
# this URL path. Now it's gonna stay forever like this due to backwards
# compat ;).
'integrations/android/share-sheet/',
integrations.android.share_sheet,
name='ui.integrations.android.share_sheet',
integrations.pwa.share_sheet,
name='ui.integrations.pwa.share_sheet',
),
path(
'integrations/extension/authenticate/',

View File

@@ -5,9 +5,11 @@ import logging
import uuid
from django.contrib import messages
from django.contrib.auth.views import redirect_to_login
import django.db
from django.http import HttpRequest, HttpResponse, JsonResponse
from django.shortcuts import redirect, render
from django.templatetags.static import static
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic import FormView, View
@@ -25,7 +27,7 @@ from hotpocket_backend.apps.ui.forms.associations import (
RefreshForm,
)
from hotpocket_backend.apps.ui.services import UIAssociationsService
from hotpocket_common.constants import NULL_UUID, AssociationsSearchMode
from hotpocket_common.constants import AssociationsSearchMode
from hotpocket_soa.dto.associations import (
AssociationOut,
AssociationsQuery,
@@ -175,7 +177,9 @@ def view(request: HttpRequest, pk: uuid.UUID) -> HttpResponse:
if is_share is True:
account_uuid = None
else:
account_uuid = NULL_UUID
return redirect_to_login(
reverse('ui.associations.view', args=(pk,)),
)
else:
if is_share is False:
account_uuid = request.user.pk
@@ -198,12 +202,18 @@ def view(request: HttpRequest, pk: uuid.UUID) -> HttpResponse:
if is_share is True:
show_controls = show_controls and False
share_url = reverse(
share_url = request.build_absolute_uri(
reverse(
'ui.associations.view',
args=(association.pk,),
query=[
('share', 'true'),
],
),
)
og_card_url = request.build_absolute_uri(
static('ui/img/og-card.png'),
)
return render(
@@ -213,6 +223,12 @@ def view(request: HttpRequest, pk: uuid.UUID) -> HttpResponse:
'association': association,
'show_controls': show_controls,
'share_url': share_url,
'is_share': is_share,
'og_card_url': (
og_card_url
if is_share is True
else None
),
},
)

View File

@@ -27,9 +27,6 @@ def page_not_found(request: HttpRequest,
exception: Exception,
template_name: str = ERROR_404_TEMPLATE_NAME,
) -> HttpResponseNotFound:
if exception:
LOGGER.error('Exception: %s', exception, exc_info=exception)
return HttpResponseNotFound(render_to_string(
'ui/errors/page_not_found.html',
context={},

View File

@@ -1,3 +1,3 @@
from . import android # noqa: F401
from . import extension # noqa: F401
from . import ios # noqa: F401
from . import pwa # noqa: F401

View File

@@ -21,10 +21,14 @@ def share_sheet(request: HttpRequest) -> HttpResponse:
try:
assert request.user.is_anonymous is False, 'Login required'
assert 'text' in request.POST, 'Bad request: Missing `text`'
url: str = ''
if 'url' in request.POST:
url = request.POST['url'].strip()
elif 'text' in request.POST:
url = request.POST['text'].split('\n')[0].strip()
assert url != '', 'Bad request: Empty `text`'
assert url != '', 'Bad request: Empty `url`'
return CreateSaveWorkflow().run(
request=request,

View File

@@ -50,7 +50,7 @@ def manifest_json(request: HttpRequest) -> JsonResponse:
'scope': '/',
'share_target': {
'action': request.build_absolute_uri(
reverse('ui.integrations.android.share_sheet'),
reverse('ui.integrations.pwa.share_sheet'),
),
'method': 'POST',
'enctype': 'multipart/form-data',

View File

@@ -72,6 +72,7 @@ TEMPLATES = [
'hotpocket_backend.apps.ui.context_processors.debug',
'hotpocket_backend.apps.ui.context_processors.version',
'hotpocket_backend.apps.ui.context_processors.appearance_settings',
'hotpocket_backend.apps.ui.context_processors.operator_email',
],
},
},
@@ -295,3 +296,5 @@ SAVES_ASSOCIATION_ADAPTER = os.environ.get(
UPLOADS_PATH = None
SITE_SHORT_TITLE = 'HotPocket'
OPERATOR_EMAIL = os.environ.get('HOTPOCKET_BACKEND_OPERATOR_EMAIL', None)

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import os
from celery.schedules import crontab
from corsheaders.defaults import default_headers
from .base import * # noqa: F401,F403
@@ -46,6 +47,13 @@ SESSION_COOKIE_SECURE = True
CRISPY_ALLOWED_TEMPLATE_PACKS = 'bootstrap5'
CRISPY_TEMPLATE_PACK = 'bootstrap5'
CELERY_BEAT_SCHEDULE = {
'clean-expired-auth-keys': {
'task': 'hotpocket_backend.apps.accounts.tasks.clean_expired_auth_keys',
'schedule': crontab(minute='30', hour='3'),
},
}
HOTPOCKET_BOT_STRATEGY = 'hotpocket_backend.apps.bot.strategy.basic:BasicStrategy'
HOTPOCKET_BOT_BANNED_HOSTNAMES = [
# YT returns dummy data when I try to fetch the page and extract

View File

@@ -13,7 +13,7 @@ cat <<EOF
|_|
production
HotPocket v25.11.06 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
HotPocket v26.3.16 [${HOTPOCKET_BACKEND_IMAGE_ID}] (https://hotpocket.app/)
Copyright 2025-present by BTHLabs. All rights reserved. (https://bthlabs.pl/)
Licensed under Apache-2.0
EOF

View File

@@ -1,6 +1,6 @@
{
"name": "hotpocket-backend",
"version": "25.11.06",
"version": "26.3.16",
"description": "HotPocket Backend",
"main": "hotpocket_backend/apps/frontend/src/index.js",
"repository": "https://git.bthlabs.pl/tomekwojcik/hotpocket",

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "hotpocket-backend"
version = "25.11.06"
version = "26.3.16"
description = "HotPocket Backend"
authors = ["Tomek Wójcik <contact@bthlabs.pl>"]
license = "Apache-2.0"
@@ -12,24 +12,24 @@ url = "https://nexus.bthlabs.pl/repository/pypi/simple/"
priority = "supplemental"
[tool.poetry.dependencies]
python = "^3.12"
python = "^3.13"
bthlabs-jsonrpc-django = "1.2.0"
celery = "5.5.3"
crispy-bootstrap5 = "2025.6"
django = "5.2.7"
celery = "5.6.2"
crispy-bootstrap5 = "2026.3"
django = "5.2.12"
django-cors-headers = "4.9.0"
django-crispy-forms = "2.4"
django-htmx = "1.26.0"
django-crispy-forms = "2.6"
django-htmx = "1.27.0"
hotpocket-common = {path = "../packages/common", develop = true}
hotpocket-soa = {path = "../packages/soa", develop = true}
keep-it-secret = {version = "1.3.0", extras = ["aws", "vault"]}
psycopg = {version = "3.2.10", extras = ["binary"]}
pydantic = "2.12.2"
psycopg = {version = "3.3.3", extras = ["binary"]}
pydantic = "2.12.5"
pyquery = "2.0.1"
requests = "2.32.5"
social-auth-app-django = "5.6.0"
social-auth-core = "4.8.1"
sqlalchemy = "2.0.44"
social-auth-app-django = "5.7.0"
social-auth-core = "4.8.5"
sqlalchemy = "2.0.48"
uuid6 = "2025.0.1"
[tool.poetry.group.dev.dependencies]
@@ -41,12 +41,11 @@ freezegun = "1.5.5"
hotpocket-backend-testing = {path = "testing", develop = true}
hotpocket-testing = {path = "../packages/testing", develop = true}
hotpocket-workspace-tools = {path = "../packages/workspace_tools", develop = true}
invoke = "2.2.1"
ipdb = "0.13.13"
ipython = "9.6.0"
ipython = "9.7.0"
isort = "7.0.0"
mypy = "1.18.2"
pytest = "8.4.2"
pytest = "9.0.1"
pytest-django = "4.11.1"
pytest-env = "1.2.0"
pytest-mock = "3.15.1"

View File

@@ -103,7 +103,7 @@ def ci(ctx: Context):
@task
def setup(ctx: Context):
ctx.run('python manage.py migrate')
ctx.run('python manage.py create_initial_account hotpocket hotpocketm4st3r')
ctx.run('python manage.py create_initial_account -u hotpocket -p hotpocketm4st3r')
if WORKSPACE_MODE == WorkspaceMode.METAL:
ctx.run('mkdir -p run/uploads')

View File

@@ -17,3 +17,15 @@ class AuthKeysTestingService:
assert auth_key.created_at is not None
assert auth_key.updated_at is not None
def assert_deleted(self,
pk: uuid.UUID,
):
query_set = AuthKey.objects.filter(pk=pk)
assert query_set.exists() is False
def assert_exists(self,
pk: uuid.UUID,
):
query_set = AuthKey.objects.filter(pk=pk)
assert query_set.count() == 1

View File

@@ -7,8 +7,8 @@ license = "Apache-2.0"
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.12"
pydantic = "2.12.2"
python = "^3.13"
pydantic = "2.12.5"
[tool.poetry.plugins.pytest11]
hotpocket_backend = "hotpocket_backend_testing.plugin"

Some files were not shown because too many files have changed in this diff Show More