1 module hcspeech.hcspeech;
2 
3 import std.algorithm;
4 import std.path;
5 import std.range;
6 import std.string;
7 
8 import file = std.file;
9 import io = std.stdio;
10 
11 import hexchat.plugin;
12 import speech.synthesis;
13 
14 import hcspeech.base, hcspeech.commands;
15 
16 /*
17  * =========================================
18  * Command Setup
19  * =========================================
20  */
21 struct TTSAction
22 {
23 	alias void function(in char[][], in char[][]) Callback;
24 
25 	Callback callback;
26 	string usage;
27 	size_t minArgs;
28 }
29 
30 __gshared TTSAction[string] actions;
31 __gshared string[] actionList; // For ordered help output.
32 
33 void addAction(string name, TTSAction.Callback callback, string usage, size_t minArgs = 0)
34 {
35 	actions[name] = TTSAction(callback, usage, minArgs);
36 	actionList ~= usage;
37 }
38 
39 void addHelpSeparator()
40 {
41 	actionList ~= "";
42 }
43 
44 shared static this()
45 {
46 	addAction("help", &helpCommand, helpUsage);
47 	addHelpSeparator();
48 
49 	addAction("toggle", &toggleCommand, toggleUsage);
50 	addAction("add", &addCommand, addUsage, 1);
51 	addAction("remove", &removeCommand, removeUsage, 1);
52 	addAction("list", &listCommand, listUsage);
53 	addHelpSeparator();
54 
55 	addAction("voicelist", &voiceListCommand, voiceListUsage);
56 	addAction("assign", &assignCommand, assignUsage, 2);
57 	addAction("unassign", &unassignCommand, unassignUsage, 1);
58 	addAction("assignedvoices", &assignedVoicesCommand, assignedVoicesUsage);
59 	addHelpSeparator();
60 
61 	addAction("volume", &volumeCommand, volumeUsage);
62 	addAction("rate", &rateCommand, rateUsage);
63 }
64 
65 immutable ttsUsage = "Usage: TTS [action [args]], main TTS command. " ~
66 "Toggles TTS for the current channel when given no arguments. " ~
67 `Use "/TTS help" to see available actions.`;
68 
69 EatMode ttsCommand(in char[][] words, in char[][] words_eol)
70 {
71 	if(words.length == 1)
72 	{
73 		toggleCommand(words[1 .. $], words_eol[1 .. $]);
74 	}
75 	else
76 	{
77 		auto actionName = words[1];
78 
79 		if(auto action = toLower(actionName) in actions)
80 		{
81 			auto args = words[1 .. $];
82 			auto args_eol = words_eol[1 .. $];
83 
84 			if(args.length - 1 >= action.minArgs)
85 				action.callback(args, args_eol);
86 			else
87 			{
88 				writefln("Usage: %s", action.usage);
89 				writefln(`Action "%s" requires at least %s argument(s).`, actionName, action.minArgs);
90 			}
91 		}
92 		else
93 		{
94 			writefln(`Unknown TTS action "%s". Use "/TTS HELP" to see usage.`, actionName);
95 		}
96 	}
97 	return EatMode.all;
98 }
99 
100 immutable helpUsage = "HELP [action], display help information.";
101 
102 void helpCommand(in char[][] words, in char[][] words_eol)
103 {
104 	if(words.length > 1)
105 	{
106 		auto actionName = words[1];
107 		if(auto action = toLower(actionName) in actions)
108 		{
109 			writefln("Usage: %s", action.usage);
110 		}
111 		else
112 		{
113 			writefln(`Unknown TTS action "%s". Use "/TTS HELP" to see usage.`, actionName);
114 		}
115 	}
116 	else
117 	{
118 		writefln("Usage: TTS [action [arguments]], perform the specified Text To Speech related action.");
119 		writefln("");
120 		foreach(usage; actionList)
121 			writefln("    %s", usage);
122 		writefln("");
123 		writefln("When no action is specified, the TOGGLE action is performed.");
124 	}
125 }
126 
127 /*
128 * =========================================
129 * TTS Hooks
130 * =========================================
131 */
132 EatMode onMessage(in char[][] words, in char[][] words_eol)
133 {
134 	auto channelName = words[2];
135 
136 	auto position = ttsChannels.find!((channel, name) => channel.name == name)(channelName);
137 	if(position.empty)
138 		return EatMode.none;
139 
140 	auto channel = position.front;
141 
142 	auto user = parseUser(words[0][1 .. $]);
143 	const(char)[] message = words_eol[3];
144 
145 	if(message.length > 0 && message[0] == ':')
146 		message = message[1 .. $];
147 
148 	tts.voice = getUserVoice(channel, user.nick);
149 	tts.queue(message);
150 
151 	return EatMode.none;
152 }
153 
154 /*
155 * =========================================
156 * Persistent Settings
157 * =========================================
158 */
159 string configFilePath()
160 {
161 	return buildPath(getInfo("xchatdir"), "hcspeech.conf");
162 }
163 
164 PluginInfo pluginInfo;
165 
166 void loadSettings()
167 {
168 	auto path = configFilePath();
169 	if(!file.exists(path))
170 		return;
171 
172 	auto file = io.File(path, "r");
173 
174 	string nick;
175 
176 	foreach(line; file.byLine())
177 	{
178 		auto stripped = line.strip();
179 		if(stripped.empty)
180 			continue;
181 
182 		auto key = stripped.munch("^=").strip();
183 
184 		stripped.popFront();
185 		auto value = stripped.strip();
186 
187 		switch(key)
188 		{
189 			case "nick":
190 				nick = value.idup;
191 				break;
192 			case "voice":
193 				assert(nick !is null);
194 
195 				if(auto voice = voiceByName(value))
196 					specifiedUserVoices[nick] = *voice;
197 				else
198 					writefln(`No voice found for "%s", removing entry for user %s.`, value, nick);
199 				break;
200 			default:
201 		}
202 	}
203 }
204 
205 void saveSettings()
206 {
207 	auto file = io.File(configFilePath(), "w");
208 
209 	void writeKey(in char[] key, in char[] value)
210 	{
211 		file.writefln("%s = %s", key, value);
212 	}
213 
214 	writeKey("plugin_version", pluginInfo.version_);
215 	file.writeln();
216 
217 	foreach(nick, voice; specifiedUserVoices)
218 	{
219 		writeKey("nick", nick);
220 		writeKey("voice", voice.name);
221 		file.writeln();
222 	}
223 }
224 
225 /*
226 * =========================================
227 * Initialization
228 * =========================================
229 */
230 void init(ref PluginInfo info)
231 {
232 	info.name = "hcspeech";
233 	info.description = "Text To Speech";
234 	info.version_ = (cast(string)import("version.txt")).chomp.stripLeft('v');
235 	pluginInfo = info;
236 
237 	tts = Synthesizer.create();
238 	allVoices = voiceList().array();
239 
240 	loadSettings();
241 
242 	hookCommand("tts", &ttsCommand, ttsUsage);
243 
244 	hookServer("PRIVMSG", &onMessage);
245 
246 	writefln("Text To Speech plugin (hcspeech %s) successfully loaded", info.version_);
247 }
248 
249 void shutdown()
250 {
251 	saveSettings();
252 }
253 
254 version(Windows)
255 {
256 	import core.sys.windows.dll;
257 	mixin SimpleDllMain;
258 }
259 
260 mixin Plugin!(init, shutdown);
261