├── .github ├── codeowners ├── renovate.json └── workflows │ ├── lint.yml │ ├── format-check.yml │ └── codeql-analysis.yml ├── .gitignore ├── .vscode └── settings.json ├── deno.jsonc ├── license.md ├── deno.lock ├── main.ts └── README.md /.github/codeowners: -------------------------------------------------------------------------------- 1 | license.md @igorkowalczyk 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["github>igorkowalczyk/shared-configs//packages/renovate-config/index.json"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Deno 2 | /.idea/ 3 | /.vscode/ 4 | 5 | /node_modules 6 | 7 | .env 8 | *.orig 9 | *.pyc 10 | *.swp 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "markdown.validate.fragmentLinks.enabled": "ignore", 3 | "markdown.validate.referenceLinks.enabled": "ignore", 4 | "eslint.validate": ["javascript", "javascriptreact", "typescript", "typescriptreact"], 5 | "editor.codeActionsOnSave": { 6 | "source.fixAll.eslint": true 7 | }, 8 | "deno.enable": true 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Format check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check: 7 | name: Format check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: 🧱 Clone repository 11 | uses: actions/checkout@v4 12 | - name: 🔩 Install Deno 13 | uses: denoland/setup-deno@v1 14 | with: 15 | deno-version: v1.x 16 | - name: 🚀 Check lint 17 | run: deno task lint -------------------------------------------------------------------------------- /.github/workflows/format-check.yml: -------------------------------------------------------------------------------- 1 | name: Format check 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | check: 7 | name: Format check 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: 🧱 Clone repository 11 | uses: actions/checkout@v4 12 | - name: 🔩 Install Deno 13 | uses: denoland/setup-deno@v1 14 | with: 15 | deno-version: v1.x 16 | - name: 🚀 Check formatting 17 | run: deno task fmt:check -------------------------------------------------------------------------------- /deno.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://deno.land/x/deno/cli/schemas/config-file.v1.json", 3 | "tasks": { 4 | "dev": "deno run --watch --allow-net --allow-env --allow-read main.ts", 5 | "start": "deno run --allow-net --allow-env --allow-read main.ts", 6 | "fmt": "deno fmt", 7 | "fmt:check": "deno fmt --check", 8 | "lint": "deno lint" 9 | }, 10 | "compilerOptions": { 11 | "checkJs": false 12 | }, 13 | "fmt": { 14 | "include": ["."], 15 | "useTabs": false, 16 | "lineWidth": 2000, 17 | "indentWidth": 1, 18 | "singleQuote": false, 19 | "proseWrap": "preserve" 20 | }, 21 | "lint": { 22 | "include": ["."], 23 | "rules": { 24 | "tags": ["recommended"] 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL Checks" 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | analyze: 7 | name: Analyze 8 | runs-on: ubuntu-latest 9 | permissions: 10 | actions: read 11 | contents: read 12 | security-events: write 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | language: ["javascript-typescript"] 17 | steps: 18 | - name: 🧱 Checkout repository 19 | uses: actions/checkout@v4 20 | - name: 🚀 Initialize CodeQL 21 | uses: github/codeql-action/init@v3 22 | with: 23 | languages: ${{ matrix.language }} 24 | - name: 🚀 Perform CodeQL Analysis 25 | uses: github/codeql-action/analyze@v3 26 | with: 27 | category: "/language:${{matrix.language}}" 28 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Igor Kowalczyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /deno.lock: -------------------------------------------------------------------------------- 1 | { 2 | "version": "3", 3 | "remote": { 4 | "https://deno.land/std@0.194.0/collections/filter_values.ts": "5b9feaf17b9a6e5ffccdd36cf6f38fa4ffa94cff2602d381c2ad0c2a97929652", 5 | "https://deno.land/std@0.194.0/collections/without_all.ts": "a89f5da0b5830defed4f59666e188df411d8fece35a5f6ca69be6ca71a95c185", 6 | "https://deno.land/std@0.194.0/dotenv/mod.ts": "39e5d19e077e55d7e01ea600eb1c6d1e18a8dfdfc65d68826257a576788da3a4", 7 | "https://deno.land/std@0.208.0/dotenv/mod.ts": "039468f5c87d39b69d7ca6c3d68ebca82f206ec0ff5e011d48205eea292ea5a6", 8 | "https://esm.sh/badge-maker@3.3.1": "dab6044080fbfe9119bb5f887bb26bb3dba26c1c483174935c1c3dc4a6c9d58d", 9 | "https://esm.sh/buffer@6.0.3": "8f5a9e4e338659848df874d8cdbfb65611af358e30940a46e44fe335c6d358ab", 10 | "https://esm.sh/lru-cache@10.0.0": "e4fa58517b79bbbb3d44cdc994c712f7e8dabcc07013916599e94833996712c1", 11 | "https://esm.sh/lru-cache@10.1.0": "5a5e773a0c61d5bb74c153d9ea96ab7791b4b031363a013bb1d727ceb40d01cd", 12 | "https://esm.sh/v127/anafanafo@2.0.0/denonext/anafanafo.mjs": "7858882a820a2ee6c27e52c22f39cdb9024883ef3c92ba1416cd7e3b1d8447c5", 13 | "https://esm.sh/v127/badge-maker@3.3.1/denonext/badge-maker.mjs": "d0f3d81bed4a6792830bb90405ffeed1cb9b220539854d7b66256249b90dab53", 14 | "https://esm.sh/v127/binary-search@1.3.6/denonext/binary-search.mjs": "e53e6e937aa01c06a13753beb7b630bb8e62a5f956119e84ad37f42fcfcdcc6e", 15 | "https://esm.sh/v127/char-width-table-consumer@1.0.0/denonext/char-width-table-consumer.mjs": "be43268048cbd636ff55838e4f77acad7112b82ff02aac5085b9b940ec49568c", 16 | "https://esm.sh/v127/color-name@1.1.4/denonext/color-name.mjs": "88abd3b0b19ef92d342221b38dc51760a4bab4540308c2dcb9db3c2cc68e5986", 17 | "https://esm.sh/v127/css-color-converter@2.0.0/denonext/css-color-converter.mjs": "f6dd72fcdffbcf664ad9c5a7040410b08e5767baf2e9826da1ace3c7c50692f8", 18 | "https://esm.sh/v127/css-unit-converter@1.1.2/denonext/css-unit-converter.mjs": "293def11db92837966a9c9efc5c32772f60e900cb22e512f1776bcffeea812b1", 19 | "https://esm.sh/v128/base64-js@1.5.1/denonext/base64-js.mjs": "fc961905a7e7a9a1c753f006903a610306d299569a0b3af9c48318a28ac48b84", 20 | "https://esm.sh/v128/buffer@6.0.3/denonext/buffer.mjs": "2532b5597e5f3bdd63967089de1f509bda7379ca85d07f1609d3c4e35907c4ac", 21 | "https://esm.sh/v128/ieee754@1.2.1/denonext/ieee754.mjs": "9ec2806065f50afcd4cf3f3f2f38d93e777a92a5954dda00d219f57d4b24832f", 22 | "https://esm.sh/v128/lru-cache@10.0.0/denonext/lru-cache.mjs": "07c4092705d4cd15c57cd7c04de101373d67c2b2440ff668644e73fa46e443a1", 23 | "https://esm.sh/v135/lru-cache@10.1.0/denonext/lru-cache.mjs": "8eccca15513d8d1e660969697e34dd104c57dfbe3f20f0e88e04650a3e1bd93a" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /main.ts: -------------------------------------------------------------------------------- 1 | import { makeBadge } from "https://esm.sh/badge-maker@3.3.1"; 2 | import type { Format } from "https://esm.sh/badge-maker@3.3.1"; 3 | import { Buffer } from "https://esm.sh/buffer@6.0.3"; 4 | import { load } from "https://deno.land/std@0.208.0/dotenv/mod.ts"; 5 | import { LRUCache } from "https://esm.sh/lru-cache@10.1.0"; 6 | 7 | type BadgeProps = { 8 | label?: string; 9 | labelColor?: string; 10 | color?: string; 11 | style?: string; 12 | }; 13 | 14 | const env = await load(); 15 | const port = parseInt(Deno.env.get("PORT") as string) || parseInt(env["PORT"]) || 8080; 16 | const server = Deno.listen({ port: port }); 17 | 18 | const cache = new LRUCache({ 19 | max: 1, 20 | ttl: 1000 * 60 * 60, // 1 hour 21 | }); 22 | 23 | console.log(`HTTP webserver running. Access it at: http://localhost:${port}/`); 24 | 25 | const api = Deno.env.get("WAKATIME_API_KEY") || env["WAKATIME_API_KEY"]; 26 | if (!api) throw new Error("WAKATIME_API_KEY is not defined in .env file."); 27 | const token = Buffer.from(api).toString("base64"); 28 | 29 | for await (const conn of server) { 30 | serveHttp(conn).catch(console.error) as Promise; 31 | } 32 | 33 | async function serveHttp(conn: Deno.Conn) { 34 | const httpConn = Deno.serveHttp(conn) as Deno.HttpConn; 35 | 36 | for await (const requestEvent of httpConn) { 37 | if (requestEvent.request.method !== "GET") { 38 | requestEvent.respondWith(new Response("Invaild method! Use GET instead.", { status: 405 })); 39 | continue; 40 | } 41 | 42 | const url = new URL(requestEvent.request.url) as URL; 43 | const path = url.pathname as string; 44 | 45 | if (path !== "/api/badge") { 46 | requestEvent.respondWith( 47 | new Response("Redirecting...", { 48 | status: 302, 49 | headers: { 50 | Location: "/api/badge", 51 | }, 52 | }), 53 | ); 54 | continue; 55 | } 56 | 57 | const { label, labelColor, color, style } = Object.fromEntries(new URLSearchParams(url.search)) as BadgeProps; 58 | const start = Date.now(); 59 | 60 | if (cache.has("data")) { 61 | const badge = makeBadge({ 62 | label: label || "Wakatime", 63 | message: cache.get("data") as string, 64 | color: color || "blue", 65 | labelColor: labelColor || "grey", 66 | style: style || "flat", 67 | } as Format); 68 | 69 | requestEvent.respondWith( 70 | new Response(badge, { 71 | status: 200, 72 | headers: { 73 | "Content-Type": "image/svg+xml", 74 | "Cache-Control": "public, max-age=3600, s-maxage=3600, stale-while-revalidate=600", 75 | Vary: "Accept-Encoding", 76 | "x-server-cache": "HIT", 77 | ...(Deno.env.get("NODE_ENV") !== "development" && { 78 | "Server-Timing": `response;dur=${Date.now() - start}ms`, 79 | }), 80 | }, 81 | }), 82 | ); 83 | continue; 84 | } 85 | 86 | const response = await fetch("https://wakatime.com/api/v1/users/current/all_time_since_today", { 87 | method: "GET", 88 | headers: { 89 | Authorization: `Basic ${token}`, 90 | }, 91 | }); 92 | 93 | if (!response.ok) { 94 | requestEvent.respondWith( 95 | new Response(makeBadge({ label: "Error", message: "Internal server error!", color: "red", style: "flat" }), { 96 | status: 500, 97 | headers: { 98 | "Content-Type": "image/svg+xml", 99 | "Cache-Control": "public, max-age=3600, s-maxage=3600, stale-while-revalidate=600", 100 | Vary: "Accept-Encoding", 101 | "x-server-cache": "MISS", 102 | ...(Deno.env.get("NODE_ENV") !== "development" && { 103 | "Server-Timing": `response;dur=${Date.now() - start}ms`, 104 | }), 105 | }, 106 | }), 107 | ); 108 | continue; 109 | } 110 | 111 | const data = await response.json(); 112 | 113 | cache.set("data", data.data?.text || "Getting data..."); 114 | 115 | const badge = makeBadge({ 116 | label: label || "Wakatime", 117 | message: data.data?.text || "Getting data...", 118 | color: color || "blue", 119 | labelColor: labelColor || "grey", 120 | style: style || "flat", 121 | } as Format); 122 | 123 | requestEvent.respondWith( 124 | new Response(badge, { 125 | status: 200, 126 | headers: { 127 | "Content-Type": "image/svg+xml", 128 | "Cache-Control": "public, max-age=3600, s-maxage=3600, stale-while-revalidate=600", 129 | Vary: "Accept-Encoding", 130 | "x-server-cache": "MISS", 131 | ...(Deno.env.get("NODE_ENV") !== "development" && { 132 | "Server-Timing": `response;dur=${Date.now() - start}ms`, 133 | }), 134 | }, 135 | }), 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Wakatime Hours](https://github.com/IgorKowalczyk/wakatime-hours/assets/49127376/d47625a9-5232-444f-9279-ce30aa69b5ca) 2 | 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | --- 19 | 20 | > [!IMPORTANT] 21 | > In order to display your statistics you need to host this API yourself, for this I recommend using [Deno Deploy](https://deno.com/deploy). 22 | 23 | > [!NOTE] 24 | > Don't forget to replace example `YOUR-DEPLOY` parameter with real value. 25 | 26 | ## 🖥️ Hosting with Deno Deploy 27 | 28 | 1. Fork [this repository](https://github.com/IgorKowalczyk/wakatime-hours) 29 | 2. Go to [Deno Deploy](https://deno.com/deploy) and connect your GitHub account 30 | 3. Click `New Project` and select your forked repository 31 | 4. Select `main` branch 32 | 5. Add `WAKATIME_API_KEY` environment variable with your Wakatime API Key 33 | 6. Click `Link` 34 | 7. Visit `https://YOUR-DEPLOY/api/badge` in your browser 35 | 36 | ## 🔩 Self Hosting 37 | 38 | 1. Clone [this repository](https://github.com/igorkowalczyk/wakatime-hours) `git clone https://github.com/IgorKowalczyk/wakatime-hours` 39 | 2. Create new file named `.env` Remember - the file is super secret, better to not share it. 40 | 3. In `.env` file set this values: 41 | - `WAKATIME_API_KEY` - Your Wakatime API Key 42 | - `PORT` - Port on which the API will be available (optional, default: `8080`) 43 | 4. Run `deno task dev` to start the project in development mode or `deno task start` to run the project in production mode. 44 | 5. Visit `http://localhost:8080` in your browser _(or `http://localhost:${PORT}` if you set custom port)_ 45 | 46 | ## ▲ Hosting with Vercel 47 | 48 | 49 | 50 | > [!WARNING] 51 | > **This API no longer supports Vercel hosting.** But if you want to host this API on Vercel, **you can use old version of this API (`>= 2.x.x`)** which is available [here](https://github.com/IgorKowalczyk/wakatime-hours/releases/tag/v2.1.0). 52 | 53 | 54 | 55 | > **The old version of this API is no longer supported and will not receive any updates!** 56 | 57 | ## 🗜️ API Usage 58 | 59 | ```http 60 | GET https://YOUR-DEPLOY/api/badge?style=${style}&color=${color}&label=${label} 61 | ``` 62 | 63 | | Parameter | Type | Description | Available values | Default value | 64 | | :-------- | :------- | :----------------------------------- | :---------------------------------------------- | :------------ | 65 | | `style` | `string` | **Optional**. The style of the badge | [Available styles](#%EF%B8%8F-available-styles) | `flat` | 66 | | `color` | `string` | **Optional**. The color of the badge | [Available colors](#-custom-colors) | `blue` | 67 | | `label` | `string` | **Optional**. The label of the badge | Any string | `Wakatime` | 68 | 69 | ## 🖼️ Available styles 70 | 71 | > [!NOTE] 72 | > The default style is `flat` 73 | 74 | | Style | Example | Usage | 75 | | --------------- | ------------------------------------------------------------------ | --------------------- | 76 | | `flat` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat) | `style=flat` | 77 | | `flat-square` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat-square) | `style=flat-square` | 78 | | `for-the-badge` | ![](https://wakatime-hours.deno.dev/api/badge?style=for-the-badge) | `style=for-the-badge` | 79 | | `plastic` | ![](https://wakatime-hours.deno.dev/api/badge?style=plastic) | `style=plastic` | 80 | | `social` | ![](https://wakatime-hours.deno.dev/api/badge?style=social) | `style=social` | 81 | 82 | > [!NOTE] 83 | > To apply the style, add to the URL `?style=YOUR-STYLE`, if you use other parameters you can use `&style=YOUR-STYLE` 84 | 85 | ## 🎨 Custom colors 86 | 87 | > [!NOTE] 88 | > The default color is `blue` 89 | 90 | | Color | Example | Usage | Label Color | Label usage | 91 | | ------------- | --------------------------------------------------------------------------- | ------------------- | -------------------------------------------------------------------------------- | ------------------------ | 92 | | `brightgreen` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=brightgreen) | `color=brightgreen` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=brightgreen) | `labelColor=brightgreen` | 93 | | `green` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=green) | `color=green` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=green) | `labelColor=green` | 94 | | `yellow` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=yellow) | `color=yellow` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=yellow) | `labelColor=yellow` | 95 | | `yellowgreen` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=yellowgreen) | `color=yellowgreen` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=yellowgreen) | `labelColor=yellowgreen` | 96 | | `orange` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=orange) | `color=orange` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=orange) | `labelColor=orange` | 97 | | `red` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=red) | `color=red` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=red) | `labelColor=red` | 98 | | `blue` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=blue) | `color=blue` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=blue) | `labelColor=blue` | 99 | | `grey` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=grey) | `color=grey` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=grey) | `labelColor=grey` | 100 | | `lightgrey` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=lightgrey) | `color=lightgrey` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=lightgrey) | `labelColor=lightgrey` | 101 | | `blueviolet` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=blueviolet) | `color=blueviolet` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=blueviolet) | `labelColor=blueviolet` | 102 | | `ff69b4` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&color=ff69b4) | `color=ff69b4` | ![](https://wakatime-hours.deno.dev/api/badge?style=flat&labelColor=ff69b4) | `labelColor=ff69b4` | 103 | 104 | > [!NOTE] 105 | > To apply the style, add to the URL `?color=YOUR-COLOR`, if you use other parameters you can use `&color=YOUR-COLOR` 106 | 107 | > [!WARNING] 108 | > HEX colors should be used without `#` symbol prefix. 109 | 110 | ## 📝 Custom text 111 | 112 | You can overwrite default `Wakatime` text with your own label. 113 | 114 | ![](https://wakatime-hours.deno.dev/api/badge?label=Your+own+label&color=blue) 115 | 116 | > [!NOTE] 117 | > Replace whitespace with `+` character in multi-word labels. 118 | 119 | ```markdown 120 | ![Wakatime Hours](https://YOUR-DEPLOY/api/badge?label=Your+own+label) 121 | ``` 122 | 123 | ## 📊 Getting Wakatime API Key 124 | 125 | 1. Go to [Wakatime](https://wakatime.com) and login to your account 126 | 2. Go to [API Keys](https://wakatime.com/settings/api-key) page and copy your API Key 127 | 3. Paste your API Key to `.env` file or add it as environment variable on your hosting 128 | 4. Restart your API if needed 129 | 130 | > [!IMPORTANT] 131 | > Wakatime API Key is super secret, better to not share it. If you share it, anyone can use your API Key to get or modify your statistics. 132 | 133 | ## ⁉️ Issues 134 | 135 | If you come across any errors or have suggestions for improvements, please create a [new issue here](https://github.com/igorkowalczyk/wakatime-hours/issues) and describe it clearly. 136 | 137 | ## 📥 Pull Requests 138 | 139 | When submitting a pull request, please follow these steps: 140 | 141 | - Clone [this repository](https://github.com/igorkowalczyk/wakatime-hours) `https://github.com/IgorKowalczyk/wakatime-hours.git` 142 | - Create a branch from `main` and give it a meaningful name (e.g. `my-awesome-new-feature`). 143 | - Open a [pull request](https://github.com/igorkowalczyk/wakatime-hours/pulls) on [GitHub](https://github.com/) and clearly describe the feature or fix you are proposing. 144 | 145 | ## 📋 License 146 | 147 | This project is licensed under the MIT. See the [LICENSE](https://github.com/igorkowalczyk/wakatime-hours/blob/main/license.md) file for details 148 | 149 | --------------------------------------------------------------------------------