Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ This application requires several environment variables to function correctly. B
| `LOG_LEVEL` | The log level for the application. | `info` |

### `PLANKA_API_KEY`:
Currently, Planka does not official support API keys. This means that you will have to insert a long-lived session token in the database manually.
Planka v2 supports per-user API keys natively. Mint one with `POST /api/users/{id}/api-key` (the full key is returned only once, in `included.apiKey`) and store the result in this variable. The client sends it as the `X-Api-Key` header on every request.

### `PLANKA_URL`:
The Planka base URL. The `/api` segment is appended automatically if missing — both `https://planka.example.com` and `https://planka.example.com/api` work.

## Code Overview

Expand Down
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,7 @@
"prepare-husky": "husky"
},
"dependencies": {
"@gewis/planka-client": "^2.0.1",
"@hey-api/client-fetch": "^0.8.4",
"@gewis/planka-client": "3.0.1",
"imapflow": "^1.0.184",
"log4js": "^6.9.1"
},
Expand Down
61 changes: 34 additions & 27 deletions src/planka.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import {
client,
createCard,
CreateCardRequest,
UpdateCardRequest,
type CreateCardData,
type GetBoardData,
type GetBoardResponse,
getBoard,
type List,
type Options,
updateCard,
GetBoardRequest,
GetBoardResponse,
type UpdateCardData,
withApiKey,
} from '@gewis/planka-client';
import type { List } from '@gewis/planka-client';
import type { Client, Options } from '@hey-api/client-fetch';
import { getLogger } from 'log4js';
import type { CardEmail } from './mailer';

Expand All @@ -20,9 +21,14 @@ interface CacheEntry {
preferredList: List | null; // Preference is: header if present, list called 'mail' if present, otherwise first list
}

const ensureApiSuffix = (url: string): string => {
const trimmed = url.replace(/\/+$/, '');
return trimmed.endsWith('/api') ? trimmed : `${trimmed}/api`;
};

export default class Planka {
private static instance: Planka | null = null;
private static client: Client | null = null;
private static initialized = false;
private settings: { plankaUrl?: string; plankaApiKey?: string };

private static boardCache: Map<bigint, CacheEntry | null> = new Map();
Expand All @@ -40,18 +46,19 @@ export default class Planka {
* Sets up API configuration like base URL and authentication.
*/
private initializeClient() {
if (!Planka.client) {
const plankaUrl = this.settings.plankaUrl || DEFAULT_PLANKA_URL;
const plankaApiKey = this.settings.plankaApiKey || process.env['PLANKA_API_KEY'];

client.setConfig({
baseUrl: plankaUrl,
headers: {
Authorization: `Bearer ${plankaApiKey}`,
},
});
Planka.client = client;
if (Planka.initialized) return;

const plankaUrl = this.settings.plankaUrl || DEFAULT_PLANKA_URL;
const plankaApiKey = this.settings.plankaApiKey || process.env['PLANKA_API_KEY'];
if (!plankaApiKey) {
throw new Error('PLANKA_API_KEY is required (mint one via createUserApiKey).');
}

client.setConfig({
baseUrl: ensureApiSuffix(plankaUrl),
...withApiKey(plankaApiKey),
});
Planka.initialized = true;
}

/**
Expand All @@ -71,7 +78,7 @@ export default class Planka {
* Throws an error if the client isn't initialized.
*/
private static pre() {
if (!Planka.client) {
if (!Planka.initialized) {
throw new Error('Client has not been initialized. Please call initialize() first.');
}
}
Expand Down Expand Up @@ -104,8 +111,8 @@ export default class Planka {
continue;
}

const board = await getBoard({ path: { id: id.toString() } } as Options<GetBoardRequest, false>);
const status = board.response.status;
const board = await getBoard({ path: { id: id.toString() } } as Options<GetBoardData, false>);
const status = board.response?.status ?? 0;
Planka.logger.trace('caching board', id, 'status', status);

if (status === 200 && board.data) {
Expand All @@ -115,7 +122,7 @@ export default class Planka {

if (lists.length > 0) {
// Find the list named 'mail', or fall back to the first list if not found
preferredList = lists.find((list: List) => list.name.toLowerCase() === 'mail') || lists[0];
preferredList = lists.find((list: List) => list.name?.toLowerCase() === 'mail') || lists[0];
}

Planka.boardCache.set(id, { board: board.data, preferredList });
Expand Down Expand Up @@ -168,10 +175,10 @@ export default class Planka {
name: card.title,
position: 0,
},
} as Options<CreateCardRequest, false>).then(async (result) => {
Planka.logger.trace('created card', card.uid, 'status', result.response.status);
} as Options<CreateCardData, false>).then(async (result) => {
const status = result.response?.status ?? 0;
Planka.logger.trace('created card', card.uid, 'status', status);
const cardResult = result.data;
const status = result.response.status;

if (status !== 200 || !cardResult) return;

Expand All @@ -187,9 +194,9 @@ export default class Planka {
description: card.body,
dueDate: card.date ? card.date : null,
},
} as Options<UpdateCardRequest>)
} as Options<UpdateCardData>)
.then((result) => {
Planka.logger.trace('updated card', card.uid, 'status', result.response.status);
Planka.logger.trace('updated card', card.uid, 'status', result.response?.status ?? 0);
})
.catch((e) => {
Planka.logger.error('error updating card', card.uid, e);
Expand Down
18 changes: 9 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -96,12 +96,12 @@
eslint-plugin-import "^2.31.0"
typescript-eslint "^8.25.0"

"@gewis/planka-client@^2.0.1":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@gewis/planka-client/-/planka-client-2.0.1.tgz#aa290262752f825bc3d4eaf283e2462419500f2b"
integrity sha512-1XDahfHOIXuAeMpzjlrQbwhRORbrwN8gjsuwy4W9gPrEZ1hP9BfwBlchjRC7I4Dm82pkvvHu20UWbspL3UONbA==
"@gewis/planka-client@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@gewis/planka-client/-/planka-client-3.0.1.tgz#02bb65adb5fca4cb4d825022f61c973f85e74088"
integrity sha512-2BHT4bonHWsodenhzAulW8o/CSGkO7MyfX2JtOOLmZ+VRIVNHrIFplKWwX/p6i3XBiS7sBaTtl03fiNqlMyBgQ==
dependencies:
"@hey-api/client-fetch" "^0.8.1"
"@hey-api/client-fetch" "0.13.1"

"@gewis/prettier-config@^2.2.2":
version "2.2.2"
Expand All @@ -111,10 +111,10 @@
"@vue/eslint-config-prettier" "^10.2.0"
eslint-config-prettier "^10.0.1"

"@hey-api/client-fetch@^0.8.1", "@hey-api/client-fetch@^0.8.4":
version "0.8.4"
resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.8.4.tgz#2dc2d368b4dd2137789f46adcae68ccd04ea2adb"
integrity sha512-SWtUjVEFIUdiJGR2NiuF0njsSrSdTe7WHWkp3BLH3DEl2bRhiflOnBo29NSDdrY90hjtTQiTQkBxUgGOF29Xzg==
"@hey-api/client-fetch@0.13.1":
version "0.13.1"
resolved "https://registry.yarnpkg.com/@hey-api/client-fetch/-/client-fetch-0.13.1.tgz#d01cdeed5f5b5b5706d52ffe29387741d2a533e0"
integrity sha512-29jBRYNdxVGlx5oewFgOrkulZckpIpBIRHth3uHFn1PrL2ucMy52FvWOY3U3dVx2go1Z3kUmMi6lr07iOpUqqA==

"@humanfs/core@^0.19.1":
version "0.19.1"
Expand Down
Loading