commit 08664b4309d4a8daddfe556329a0dde0ef11064a Author: Aubrey Taylor Date: Sun Jan 26 13:10:11 2025 -0600 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aec3a31 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/config.json +/node_modules + diff --git a/api.js b/api.js new file mode 100644 index 0000000..3e54463 --- /dev/null +++ b/api.js @@ -0,0 +1,9 @@ +export async function summary(token) { + let response = await fetch("https://api.wanikani.com/v2/summary", { + headers: { + "Authorization": `Bearer ${token}` + } + }); + + return await response.json(); +} \ No newline at end of file diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..db95ce3 --- /dev/null +++ b/default.nix @@ -0,0 +1,11 @@ +{ + lib, + buildNpmPackage, +}: +buildNpmPackage rec { + pname = "wanireminder"; + version = "1.0.0"; + src = ./.; + dontNpmBuild = true; + npmDepsHash = "sha256-ysvwp4eAe/nLEbS5VWTf/B5UY9gigSsVzGPwfIB0NGg="; +} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..65fac76 --- /dev/null +++ b/flake.lock @@ -0,0 +1,100 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1737873155, + "narHash": "sha256-6g9lSb6Vq/NnXdqKSSzQ09Wd2kK/mWIvv3ZrVhLYUOg=", + "owner": "nix-community", + "repo": "fenix", + "rev": "11391594600c119f02b3ee864decca1d6767f1b6", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1737746512, + "narHash": "sha256-nU6AezEX4EuahTO1YopzueAXfjFfmCHylYEFCagduHU=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "825479c345a7f806485b7f00dbe3abb50641b083", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1737831834, + "narHash": "sha256-Nd8Qd8kWrJSAaPD/VGVm98ntCIJ4d9ZkfDSySWM0bbU=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "90bf50c0112db1494233401b6c0f895fb3210ddd", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..fe8ae03 --- /dev/null +++ b/flake.nix @@ -0,0 +1,78 @@ +{ + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + outputs = { + self, + nixpkgs, + flake-utils, + fenix, + }: + flake-utils.lib.eachDefaultSystem + ( + system: let + overlays = [fenix.overlays.default]; + pkgs = import nixpkgs { + inherit system overlays; + }; + libPath = with pkgs; + lib.makeLibraryPath [ + libGL + libxkbcommon + wayland + ]; + in + with pkgs; { + formatter = alejandra; + defaultPackage = pkgs.callPackage ./. {}; + nixosModules.default = { + config, + lib, + pkgs, + ... + }: { + options.services.wanireminder = { + enable = lib.mkEnableOption "wanikani reminder bot"; + webhook = lib.mkOption { + type = lib.types.string; + description = "The webhook to post the messages to"; + }; + users = lib.mkOption { + type = lib.types.listOf lib.types.attrs; + } // { + name = lib.mkOption { + type = lib.types.string; + }; + wktoken = lib.mkOption { + type = lib.types.string; + }; + discord = lib.mkOption { + type = lib.types.string; + }; + }; + }; + config = lib.mkIf config.services.wanireminder.enable { + systemd.services.wanireminder = { + wantedBy = ["multi-user.target"]; + after = ["network.target"]; + description = "Run wanireminders"; + serviceConfig = { + WorkingDirectory = "${self.defaultPackage.${system}}"; + Type = "oneshot"; + OnCalendar = "hourly"; + Persistent = true; + ExecStart = let + configJson = builtins.toFile "config.json" (builtins.toJSON config.services.wanireminder); + in ''${self.defaultPackage.${system}}/bin/wanireminder ${configJson}''; + }; + }; + }; + }; + } + ); +} diff --git a/index.js b/index.js new file mode 100644 index 0000000..5b75fd2 --- /dev/null +++ b/index.js @@ -0,0 +1,25 @@ +import { WebhookClient } from "discord.js"; +import { summary } from "./api.js"; +import { readFileSync } from "fs"; + +const config = readFileSync(process.argv[1], "utf-8"); + +const webhook = new WebhookClient({ url: config.webhook }); + +for (let user of config.users) { + let result = await summary(user.wktoken); + + let current_reviews = result.data.reviews.filter(review => new Date(review.available_at) < new Date()).map(lesson => lesson.subject_ids).flat(); + let future_reviews = result.data.reviews.map(review => review.subject_ids).flat(); + let future_time = new Date(result.data.reviews.at(-1).available_at); + + if (current_reviews.length > 0) { + let message = `<@${user.discord}> You have reviews! If you do your wanikani now, there's only **${current_reviews.length}**! you'll have **${future_reviews.length}** reviews.`; + + await webhook.send({ + content: message, + username: "WaniKani Reminder", + avatarURL: "https://pbs.twimg.com/profile_images/1213575248911552512/WP11ESTy_400x400.jpg", + }) + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..5727793 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,314 @@ +{ + "name": "wanireminder", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "wanireminder", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "discord.js": "^14.17.3" + }, + "bin": { + "wanireminder": "index.js" + }, + "devDependencies": { + "@types/node": "^22.10.10" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.10.0.tgz", + "integrity": "sha512-ikVZsZP+3shmVJ5S1oM+7SveUCK3L9fTyfA8aJ7uD9cNQlTqF+3Irbk2Y22KXTb3C3RNUahRkSInClJMkHrINg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/formatters": "^0.6.0", + "@discordjs/util": "^1.1.1", + "@sapphire/shapeshift": "^4.0.0", + "discord-api-types": "^0.37.114", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.4", + "tslib": "^2.6.3" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.0.tgz", + "integrity": "sha512-YIruKw4UILt/ivO4uISmrGq2GdMY6EkoTtD0oS0GvkJFRZbTSdPhzYiUILbJ/QslsvC9H9nTgGgnarnIl4jMfw==", + "license": "Apache-2.0", + "dependencies": { + "discord-api-types": "^0.37.114" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.4.2.tgz", + "integrity": "sha512-9bOvXYLQd5IBg/kKGuEFq3cstVxAMJ6wMxO2U3wjrgO+lHv8oNCT+BBRpuzVQh7BoXKvk/gpajceGvQUiRoJ8g==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.1", + "@discordjs/util": "^1.1.1", + "@sapphire/async-queue": "^1.5.3", + "@sapphire/snowflake": "^3.5.3", + "@vladfrangu/async_event_emitter": "^2.4.6", + "discord-api-types": "^0.37.114", + "magic-bytes.js": "^1.10.0", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/util": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", + "integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.0.tgz", + "integrity": "sha512-QH5CAFe3wHDiedbO+EI3OOiyipwWd+Q6BdoFZUw/Wf2fw5Cv2fgU/9UEtJRmJa9RecI+TAhdGPadMaEIur5yJg==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/collection": "^2.1.0", + "@discordjs/rest": "^2.4.1", + "@discordjs/util": "^1.1.0", + "@sapphire/async-queue": "^1.5.2", + "@types/ws": "^8.5.10", + "@vladfrangu/async_event_emitter": "^2.2.4", + "discord-api-types": "^0.37.114", + "tslib": "^2.6.2", + "ws": "^8.17.0" + }, + "engines": { + "node": ">=16.11.0" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz", + "integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", + "integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz", + "integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v16" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.3.tgz", + "integrity": "sha512-jjmJywLAFoWeBi1W7994zZyiNWPIiqRRNAmSERxyg93xRGzNYvGjlZ0gR6x0F4gPRi2+0O6S71kOZYyr3cxaIQ==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@types/node": { + "version": "22.10.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.10.tgz", + "integrity": "sha512-X47y/mPNzxviAGY5TcYPtYL8JsY3kAq2n8fMmKoRCxq/c4v4pyGNCzM2R6+M5/umG4ZfHuT+sgqDYqWc9rJ6ww==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/ws": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.6.tgz", + "integrity": "sha512-RaI5qZo6D2CVS6sTHFKg1v5Ohq/+Bo2LZ5gzUEwZ/WkHhwtGTCB/sVLw8ijOkAUxasZ+WshN/Rzj4ywsABJ5ZA==", + "license": "MIT", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.117", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.117.tgz", + "integrity": "sha512-d+Z6RKd7v3q22lsil7yASucqMfVVV0s0XSqu3cw7kyHVXiDO/mAnqMzqma26IYnIm2mk3TlupYJDGrdL908ZKA==", + "license": "MIT" + }, + "node_modules/discord.js": { + "version": "14.17.3", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.17.3.tgz", + "integrity": "sha512-8/j8udc3CU7dz3Eqch64UaSHoJtUT6IXK4da5ixjbav4NAXJicloWswD/iwn1ImZEMoAV3LscsdO0zhBh6H+0Q==", + "license": "Apache-2.0", + "dependencies": { + "@discordjs/builders": "^1.10.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.6.0", + "@discordjs/rest": "^2.4.2", + "@discordjs/util": "^1.1.1", + "@discordjs/ws": "^1.2.0", + "@sapphire/snowflake": "3.5.3", + "discord-api-types": "^0.37.114", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "^2.6.3", + "undici": "6.19.8" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/discordjs/discord.js?sponsor" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "license": "MIT" + }, + "node_modules/magic-bytes.js": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.10.0.tgz", + "integrity": "sha512-/k20Lg2q8LE5xiaaSkMXk4sfvI+9EGEykFS4b0CHHGWqDYU0bGUFSwchNOMA56D7TCs9GwVTkqe9als1/ns8UQ==", + "license": "MIT" + }, + "node_modules/ts-mixer": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", + "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/undici": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.19.8.tgz", + "integrity": "sha512-U8uCCl2x9TK3WANvmBavymRzxbfFYG+tAu+fgx3zxQy3qdagQqBLwJVrdyO1TBfUXvfKveMKJZhpvUYoOjM+4g==", + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..5b00c8f --- /dev/null +++ b/package.json @@ -0,0 +1,21 @@ +{ + "name": "wanireminder", + "version": "1.0.0", + "main": "index.js", + "bin": { + "wanireminder": "index.js" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "type": "module", + "author": "", + "license": "ISC", + "description": "", + "dependencies": { + "discord.js": "^14.17.3" + }, + "devDependencies": { + "@types/node": "^22.10.10" + } +}