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
- Make sure to have recent versions of Docker and Docker Compose installed
- Create a volume named
d2local
usingdocker volume create d2local
- Linux or macOS 10.15+
- Swift 5.2
- 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
swiftenv
on 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)
timeout
andkill
(forMaximaCommand
)
sudo apt-get install libopus-dev libsodium-dev libssl1.0-dev libcairo2-dev poppler-utils maxima libsqlite3-dev
- Note that you might need to use
libssl-dev
instead oflibssl1.0-dev
on Ubuntu - If Swift cannot find the Freetype headers despite
libfreetype6-dev
being installed, you may need to add symlinks:mkdir /usr/include/freetype2/freetype
ln -s /usr/include/freetype2/freetype.h /usr/include/freetype2/freetype/freetype.h
ln -s /usr/include/freetype2/tttables.h /usr/include/freetype2/freetype/tttables.h
- Note that you might need to
apt-get install clang
separately on a Raspberry Pi
- Note that you might need to use
- Install
maxima
brew tap vapor/tap
brew install opus libsodium ctls cairo poppler gd
stack install happy show mueval pointfree pointful
(orcabal-install ...
)cd Node && ./install-all
- Create a folder named
local
in the repository- If you use Docker, the
local
folder is represented by thed2local
volume - See here for instructions on how to copy files into it
- If you use Docker, the
- Create a file named
platformTokens.json
inlocal
containing 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
- Create a file named
config.json
inlocal
(or thed2local
volume):
{
"prefix": "%"
}
- Create a file named
adminWhitelist.json
inlocal
(or thed2local
volume) containing a list of Discord usernames that have full permissions:
{
"users": [
"YOUR_USERNAME#1234"
]
}
- Create a file named
netApiKeys.json
inlocal
(or thed2local
volume) containing various API keys:
{
"mapQuest": "YOUR_MAP_QUEST_KEY",
"wolframAlpha": "YOUR_WOLFRAM_ALPHA_KEY",
"gitlab": "YOUR_GITLAB_PERSONAL_ACCESS_TOKEN"
}
- 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.
swift test
- 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
To suppress warnings, you can use -Xswiftc -suppress-warnings
after swift build
or swift run
.
The program consists of a single executable:
D2
, the main Discord frontend
Each executable uses its own implementation of message IO, e.g. D2
uses SwiftDiscord
to conform to D2MessageIO
's API.
These depend on several library targets:
D2Commands
, the command framework and the implementationsD2Graphics
, 2D graphics and drawingD2MessageIO
, the messaging framework used by D2 (abstracts over the Discord library)D2Permissions
, the permission managerD2Script
, 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
The executable application. The base functionality is provided by D2ClientHandler
, which is a DiscordClientDelegate
that handles raw, incoming messages and dispatches them to custom handlers that conform to the Command
protocol.
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(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(input: 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(withStringInput 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.