D2
General-purpose virtual assistant for Discord, IRC and Telegram.
It provides:
💬 Various useful chat utilities, e.g. polls or coin flips⚙️ A flexible command system that supports chaining, piping and permissions🎲 Multiplayer board and card games, such as chess or Uno📙 Integration with a wide range of web APIs, including WolframAlpha, MediaWiki, Reddit and OpenWeatherMap🖼 Image processing capabilities, including generation of animated GIFs📊 Tools for mathematics and linear algebra, e.g. a linear system solver🎵 Music theory utilities, including a chord finder🖥 Programming tools, including a Haskell API search and a Prolog interpreter🍬 Humorous commands, e.g. for jokes
Installation
using Docker (for production environments)
- Make sure to have recent versions of Docker and Docker Compose installed
- Create a volume named
d2localusingdocker volume create d2local
manually (for local development)
System Dependencies
- Linux or macOS 10.15+
- Swift 5.3
- Swift can be installed conveniently using a version manager such as
swiftenv - Current builds of Swift for Raspberry Pi can be found here
- Note that you might need to perform a custom installation if you use
swiftenvon Raspberry Pi
- Note that you might need to perform a custom installation if you use
- Swift can be installed conveniently using a version manager such as
- Haskell + Cabal Install or Stack (for Hoogle, Pointfree, ...)
- Node.js and npm (for LaTeX rendering)
timeoutandkill(forMaximaCommand)
Linux
sudo apt-get install libopus-dev libsodium-dev libssl1.0-dev libcairo2-dev poppler-utils maxima libsqlite3-dev graphviz- Note that you might need to use
libssl-devinstead oflibssl1.0-devon Ubuntu - If Swift cannot find the Freetype headers despite
libfreetype6-devbeing installed, you may need to add symlinks:mkdir /usr/include/freetype2/freetypeln -s /usr/include/freetype2/freetype.h /usr/include/freetype2/freetype/freetype.hln -s /usr/include/freetype2/tttables.h /usr/include/freetype2/freetype/tttables.h
- Note that you might need to
apt-get install clangseparately on a Raspberry Pi
- Note that you might need to use
macOS
- Install
maxima brew tap vapor/tapbrew install opus libsodium ctls cairo poppler gd
General
stack install happy show mueval pointfree pointful(orcabal-install ...)cd Node && ./install-all
Configuration
Required
- Create a folder named
localin the repository- If you use Docker, the
localfolder is represented by thed2localvolume - See here for instructions on how to copy files into it
- If you use Docker, the
- Create a file named
platformTokens.jsoninlocalcontaining the API tokens (at least one of them should be specified):
{
"discord": "YOUR_DISCORD_API_TOKEN",
"telegram": "YOUR_TELEGRAM_API_TOKEN",
"irc": [
{
"host": "YOUR_IRC_HOST",
"port": 6667,
"nickname": "YOUR_IRC_USERNAME",
"password": "YOUR_IRC_PASSWORD"
}
]
}For more information e.g. on how to connect to the Twitch IRC API, see this guide
Optional
- Create a file named
config.jsoninlocal(or thed2localvolume):
{
"prefix": "%"
}- Create a file named
adminWhitelist.jsoninlocal(or thed2localvolume) containing a list of Discord usernames that have full permissions:
{
"users": [
"YOUR_USERNAME#1234"
]
}- Create a file named
netApiKeys.jsoninlocal(or thed2localvolume) containing various API keys:
{
"mapQuest": "YOUR_MAP_QUEST_KEY",
"wolframAlpha": "YOUR_WOLFRAM_ALPHA_KEY",
"gitlab": "YOUR_GITLAB_PERSONAL_ACCESS_TOKEN"
}Building
- Using Docker:
docker-compose build - On Linux:
swift build - On macOS:
swift build -Xlinker -L/usr/local/lib -Xlinker -lopus -Xcc -I/usr/local/include
For Xcode support, see the README of SwiftDiscord.
Testing
swift test
Running
- Using Docker:
docker-compose up -d - On Linux:
swift run D2 - On macOS:
swift run -Xlinker -L/usr/local/lib -Xlinker -lopus -Xcc -I/usr/local/include D2
Additional Build Flags
To suppress warnings, you can use -Xswiftc -suppress-warnings after swift build or swift run.
Architecture
The program consists of a single executable:
D2, the main Discord frontend
This executable depends on several library targets:
D2Handlers, top-level message/event handlingD2Commands, the command framework and the implementationsD2MessageIO, the messaging framework (abstracting over the Discord library)D2DiscordIO, the Discord implementationD2TelegramIO, the Telegram implementationD2IRCIO, the IRC/Twitch implementation
D2Permissions, permission managementD2Script, an experimental DSL that can be used to script commandsD2Graphics, 2D graphics and drawingD2NetAPIs, client implementations of various web APIsD2Utils, a collection of useful utilities
D2
The 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 MessageHandlers. One of these is CommandHandler, which in turn parses the command and invokes the actual command.
D2Commands
At a basic level, the Command protocol consists of a single method named invoke that carries information about the user's request:
protocol Command: class {
...
func invoke(with input: RichValue, output: CommandOutput, context: CommandContext)
...
}The arguments each represent a part of the invocation context. Given a request such as %commandname arg1 arg2, the implementor would receive:
| Parameter | Value |
|---|---|
input |
.text("arg1 arg2") |
output |
DiscordOutput |
context |
CommandContext containing the message, the client and the command registry |
Since output: 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:
class PipeOutput: CommandOutput {
private let sink: Command
private let context: CommandContext
private let args: String
private let next: CommandOutput?
init(withSink sink: Command, context: CommandContext, args: String, next: CommandOutput? = nil) {
self.sink = sink
self.args = args
self.context = context
self.next = next
}
func append(_ value: RichValue) {
let nextInput = args.isEmpty ? value : (.text(args) + value)
sink.invoke(with: nextInput, output: next ?? PrintOutput(), context: context)
}
}Often 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:
protocol StringCommand: Command {
func invoke(with input: String, output: CommandOutput, context: CommandContext)
}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.
protocol ArgCommand: Command {
associatedtype Args: Arg
var argPattern: Args { get }
func invoke(withInputArgs inputArgs: [String], output: CommandOutput, context: CommandContext)
}ArgCommand should be adopted if the command expects a fixed structure of arguments.