{"id":15672390,"url":"https://github.com/fwcd/d2","last_synced_at":"2025-06-27T17:36:14.190Z","repository":{"id":37353189,"uuid":"175673827","full_name":"fwcd/d2","owner":"fwcd","description":"Command-based virtual assistant for Discord and other platforms","archived":false,"fork":false,"pushed_at":"2025-02-25T15:22:39.000Z","size":20668,"stargazers_count":16,"open_issues_count":75,"forks_count":5,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-04-11T14:41:27.381Z","etag":null,"topics":["discord","linux","virtual-assistant"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"gpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fwcd.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2019-03-14T17:59:53.000Z","updated_at":"2025-03-08T21:43:32.000Z","dependencies_parsed_at":"2023-09-25T00:29:33.195Z","dependency_job_id":"832109b3-e9a8-4991-a87d-624db04e0903","html_url":"https://github.com/fwcd/d2","commit_stats":{"total_commits":4090,"total_committers":7,"mean_commits":584.2857142857143,"dds":"0.20611246943765282","last_synced_commit":"f7fdb02fe6bc8d2523f5c22d9584b66ac498f4d9"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/fwcd/d2","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fwcd%2Fd2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fwcd%2Fd2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fwcd%2Fd2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fwcd%2Fd2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fwcd","download_url":"https://codeload.github.com/fwcd/d2/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fwcd%2Fd2/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":262302581,"owners_count":23290289,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["discord","linux","virtual-assistant"],"created_at":"2024-10-03T15:25:19.630Z","updated_at":"2025-06-27T17:36:14.144Z","avatar_url":"https://github.com/fwcd.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"# D2\n\n[![Build](https://github.com/fwcd/d2/actions/workflows/build.yml/badge.svg)](https://github.com/fwcd/d2/actions/workflows/build.yml)\n[![Docker](https://github.com/fwcd/d2/actions/workflows/docker.yml/badge.svg)](https://github.com/fwcd/d2/actions/workflows/docker.yml)\n\nGeneral-purpose assistant for Discord and IRC with more than 350 commands, including:\n\n- **Multiplayer games**, including chess, uno, hangman, codenames, wordle and others\n- **50+ Web APIs**, including dictionaries, news, weather, comics, recipes and more\n- **Image processors**, including various filters, animators and a QR code generator\n- **Mathematical utilities**, including equation solvers, plotters, matrix operations and a LaTeX renderer\n- **Musical utilities**, including chord finders, fretboard and piano visualizers\n- **Programming utilities**, including a Haskell API search and a Prolog interpreter\n- **Moderational utilities**, including automatic thread management, message previews, role reactions and bulk deletion\n- **Miscellaneous utilities**, including polls and coin flips\n- **Humorous and fun stuff**, including various party games and joke finders\n\n## Getting Started\n\n### Locally\n\nTo build and run D2 locally, make sure to have the following installed:\n\n- Linux or macOS\n- Swift 6+\n- Haskell + cabal-install or Stack (for Hoogle, Pointfree, ...)\n- Node.js and npm (for LaTeX rendering)\n\nOn Ubuntu, run\n\n```sh\nScripts/install-dependencies-apt\n```\n\nIf you use another distribution, use your native package manager to install the equivalent packages.\n\nOn macOS, run\n\n```sh\nScripts/install-dependencies-brew\n```\n\nCreate a folder named `local` under the repository and add configuration files as described in [the configuration section](#configuration).\n\nTo install the Haskell packages used by D2, please install Haskell Stack or cabal-install externally (e.g. via `ghcup`), then run\n\n```sh\nScripts/install-haskell-dependencies stack\n```\n\n\u003e If this fails due to version conflicts, check whether your global Stack resolver is too old in `~/.stack/global-project/stack.yaml` and/or append the `--allow-newer` flag to the command, which will be forwarded to Stack by the script.\n\n\u003e This is the command for Haskell Stack, for cabal-install substitute `cabal` for `stack` above.\n\nTo install the Node packages used by D2, run\n\n```sh\nScripts/install-node-dependencies\n```\n\nFinally, use `swift build` to build D2 and `swift run` to run it. With `swift test` you can run the test suite.\n\n\u003e To suppress warnings, you can append `-Xswiftc -suppress-warnings` after `swift build` or `swift run`.\n\n\u003e If your build fails with module redefinition errors regarding FFI includes, run `Scripts/remove-commandlinetools-ffi-includes` and try building again.\n\n### With Docker (Compose)\n\nMake sure to have recent versions of Docker and Docker Compose installed and create a volume named `d2local` using `docker volume create d2local`.\n\nIn this volume, add configuration files as described in [the configuration section](#configuration).\n\n\u003e [See here](https://stackoverflow.com/a/55683656) for instructions on how to copy files into a Docker volume\n\nYou can then use `docker-compose build` to build the image and `docker-compose up` to run it (add the `-d` flag to run it in daemonized mode).\n\n### With Kubernetes (Helm)\n\nMake sure to have `kubectl` + `helm` installed and connected to a Kubernetes cluster. The cluster should have a persistent volume available.\n\n\u003e In a local cluster (where persistent volumes generally aren't provisioned automatically), you may find the `d2-local-storage` chart useful, which registers a persistent volume. To use it, create a folder such as `./local/k8s` and run `helm upgrade --install --set storage.hostPath=$PWD/local/k8s d2-local-storage Helm/d2-local-storage`.\n\nCreate a `values.yaml` in some local location (e.g. in `local` or outside the repository) containing D2 configurations (see [the configuration section](#configuration) for details on the schema):\n\n```yaml\nd2:\n  adminWhitelist:\n    users:\n    - value: 'YOUR_DISCORD_USER_ID'\n      clientName: Discord\n  config:\n    commandPrefix: '%'\n    hostInfo:\n      instanceName: prod/k8s/yourDisplayName\n  platformTokens:\n    discord: 'YOUR_DISCORD_API_TOKEN'\n  netApiKeys: # optional\n    mapQuest: YOUR_MAP_QUEST_KEY\n    wolframAlpha: YOUR_WOLFRAM_ALPHA_KEY\n    gitlab: YOUR_GITLAB_KEY\n    openweathermap: YOUR_OPENWEATHERMAP_KEY\n    ...\n```\n\nYou can now upgrade/install D2 to the cluster using `helm upgrade --install -f path/to/local/values.yaml d2 Helm/d2`.\n\nTo uninstall it, just run `helm uninstall d2`.\n\n\u003e Note that the persistent volume storage claim by `d2` might still persist after uninstalling, in which case you could do [something like this](https://stackoverflow.com/questions/50667437/what-to-do-with-released-persistent-volume) (or reinstall `d2-local-storage`, if you installed it earlier).\n\n## Configuration\n\nNavigate to your `local` folder or volume (as described in the sections above).\n\n### Required\n\nCreate a file named `platformTokens.json` in `local` containing the API tokens (at least one of them should be specified):\n\n```json\n{\n  \"discord\": \"YOUR_DISCORD_API_TOKEN\",\n  \"irc\": [\n    {\n      \"host\": \"YOUR_IRC_HOST\",\n      \"port\": 6667,\n      \"nickname\": \"YOUR_IRC_USERNAME\",\n      \"password\": \"YOUR_IRC_PASSWORD\"\n    }\n  ]\n}\n```\n\n\u003e For more information e.g. on how to connect to the Twitch IRC API, see [this guide](https://dev.twitch.tv/docs/irc/guide/)\n\n### Optional\n\nCreate a file named `config.json` in `local` (or the `d2local` volume):\n\n```json\n{\n  \"prefix\": \"%\"\n}\n```\n\nCreate a file named `adminWhitelist.json` in `local` (or the `d2local` volume) containing a list of Discord usernames that have full permissions:\n\n```json\n{\n  \"users\": [\n    {\n      \"value\": \"YOUR_DISCORD_USER_ID\",\n      \"clientName\": \"Discord\"\n    }\n  ]\n}\n```\n\nCreate a file named `netApiKeys.json` in `local` (or the `d2local` volume) containing various API keys:\n\n```json\n{\n  \"mapQuest\": \"YOUR_MAP_QUEST_KEY\",\n  \"wolframAlpha\": \"YOUR_WOLFRAM_ALPHA_KEY\",\n  \"gitlab\": \"YOUR_GITLAB_PERSONAL_ACCESS_TOKEN\"\n}\n```\n\nCreate a folder named `memeTemplates` in `local` containing PNG images. Any fully transparent sections will be filled by a user-defined image, once the corresponding command is invoked.\n\n## Architecture\n\nThe program consists of a single executable:\n\n- `D2`, the main executable\n\nThis executable depends on several library targets:\n- `D2Handlers`, top-level message/event handling\n- `D2Commands`, the command framework and the implementations\n- `D2MessageIO`, the messaging framework (abstracting over the Discord library)\n  - `D2DiscordIO`, the Discord implementation\n  - `D2IRCIO`, the IRC/Twitch implementation\n- `D2Permissions`, permission management\n- `D2Script`, an experimental DSL that can be used to script commands\n- `D2NetAPIs`, client implementations of various web APIs\n\n### D2\n\nThe executable application. Sets up messaging backends (like Discord) and the top-level event handler (`D2Delegate`). Besides other events, the `D2Delegate` handles incoming messages and forwards them to multiple `MessageHandler`s. One of these is `CommandHandler`, which in turn parses the command and invokes the actual command.\n\n### D2Commands\n\nAt a basic level, the `Command` protocol consists of a single method named `invoke` that carries information about the user's request:\n\n```swift\nprotocol Command: AnyObject {\n    ...\n\n    func invoke(with input: RichValue, output: any CommandOutput, context: CommandContext) async\n\n    ...\n}\n```\n\nThe arguments each represent a part of the invocation context. Given a request such as `%commandname arg1 arg2`, the implementor would receive:\n\n| Parameter | Value |\n| --------- | ----- |\n| `input` | `.text(\"arg1 arg2\")` |\n| `output` | `DiscordOutput` |\n| `context` | `CommandContext` containing the message, the client and the command registry |\n\nSince `output: any CommandOutput` represents a polymorphic object, the output of an invocation does not necessarily get sent to the Discord channel where the request originated from. For example, if the user creates a piped request such as `%first | second | third`, only the third command would operate on a `DiscordOutput`. Both the first and the second command call a `PipeOutput` instead that passes any values to the next command:\n\n```swift\nclass PipeOutput: CommandOutput {\n    private let sink: any Command\n    private let context: CommandContext\n    private let args: String\n    private let next: (any CommandOutput)?\n\n    init(withSink sink: any Command, context: CommandContext, args: String, next: (any CommandOutput)? = nil) {\n        self.sink = sink\n        self.args = args\n        self.context = context\n        self.next = next\n    }\n\n    func append(_ value: RichValue) async {\n        let nextInput = args.isEmpty ? value : (.text(args) + value)\n        await sink.invoke(with: nextInput, output: next ?? PrintOutput(), context: context)\n    }\n}\n```\n\nOften the `Command` protocol is too low-level to be adopted directly, since the input can be of any form (including embeds or images). To address this, there are subprotocols that provide a simpler template interface for implementors:\n\n```swift\nprotocol StringCommand: Command {\n    func invoke(with input: String, output: any CommandOutput, context: CommandContext) async\n}\n```\n\n`StringCommand` is useful when the command accepts a single string as an argument or if a custom argument parser is used. Its default implementation of `Command.invoke` passes either `args`, if not empty, or otherwise `input.content` to `StringCommand.invoke`.\n\n```swift\nprotocol ArgCommand: Command {\n    associatedtype Args: Arg\n\n    var argPattern: Args { get }\n\n    func invoke(with input: [Args], output: any CommandOutput, context: CommandContext) async\n}\n```\n\n`ArgCommand` should be adopted if the command expects a fixed structure of arguments.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffwcd%2Fd2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffwcd%2Fd2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffwcd%2Fd2/lists"}