General-purpose assistant for Discord, IRC and Telegram featuring more than 300 commands, including:
- 💬 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.4+
- 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 libssl1.0-dev libcairo2-dev poppler-utils maxima libsqlite3-dev graphviz libgraphviz-dev libtesseract-dev libleptonica-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 - Also make sure that you are installing Tesseract 4. If you are on an older version of Ubuntu, try adding the following repository:
sudo add-apt-repository ppa:alex-p/tesseract-ocr
- Note that you might need to use
- Install
maxima
brew tap vapor/tap
brew install ctls cairo poppler gd graphviz
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"
}
- Create a folder named
memeTemplates
inlocal
containing PNG images. Any fully transparent sections will be filled by a user-defined image, once the corresponding command is invoked.
- 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 Discord.(https://github.com/nuclearace/Discord.blob/master/README.md).
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
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 commandsD2NetAPIs
, client implementations of various web APIs
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 MessageHandler
s. One of these is CommandHandler
, which in turn parses the command and invokes the actual command.
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.