diff options
Diffstat (limited to 'command.c')
-rw-r--r-- | command.c | 730 |
1 files changed, 730 insertions, 0 deletions
diff --git a/command.c b/command.c new file mode 100644 index 0000000..502ff17 --- /dev/null +++ b/command.c @@ -0,0 +1,730 @@ +/* Copyright (C) 2020 June McEnroe <june@causal.agency> + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + * + * Additional permission under GNU GPL version 3 section 7: + * + * If you modify this Program, or any covered work, by linking or + * combining it with OpenSSL (or a modified version of that library), + * containing parts covered by the terms of the OpenSSL License and the + * original SSLeay license, the licensors of this Program grant you + * additional permission to convey the resulting work. Corresponding + * Source for a non-source form of such a combination shall include the + * source code for the parts of OpenSSL used as well as that of the + * covered work. + */ + +#include <assert.h> +#include <ctype.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "chat.h" + +typedef void Command(uint id, char *params); + +static void commandDebug(uint id, char *params) { + (void)id; + (void)params; + self.debug ^= true; + uiFormat( + Debug, Warm, NULL, + "\3%dDebug is %s", Gray, (self.debug ? "on" : "off") + ); +} + +static void commandQuote(uint id, char *params) { + (void)id; + if (params) ircFormat("%s\r\n", params); +} + +static void echoMessage(char *cmd, uint id, char *params) { + if (!params) return; + ircFormat("%s %s :%s\r\n", cmd, idNames[id], params); + struct Message msg = { + .nick = self.nick, + .user = self.user, + .cmd = cmd, + .params[0] = idNames[id], + .params[1] = params, + }; + handle(&msg); +} + +static int splitChunk(const char *cmd, uint id) { + int overhead = snprintf( + NULL, 0, ":%s!%*s@%*s %s %s :\r\n", + self.nick, + (self.user ? 0 : network.userLen), (self.user ?: "*"), + (self.host ? 0 : network.hostLen), (self.host ?: "*"), + cmd, idNames[id] + ); + assert(overhead > 0 && overhead < 512); + return 512 - overhead; +} + +static int splitLen(int chunk, const char *params) { + int len = 0; + size_t cap = 1 + strlen(params); + for (int n = 0; params[len] != '\n' && len + n <= chunk; len += n) { + n = mblen(¶ms[len], cap - len); + if (n < 0) { + n = 1; + mblen(NULL, 0); + } + if (!n) break; + } + return len; +} + +static void splitMessage(char *cmd, uint id, char *params) { + if (!params) return; + int chunk = splitChunk(cmd, id); + if (strlen(params) <= (size_t)chunk && !strchr(params, '\n')) { + echoMessage(cmd, id, params); + return; + } + while (*params) { + int len = splitLen(chunk, params); + char ch = params[len]; + params[len] = '\0'; + echoMessage(cmd, id, params); + params[len] = ch; + params += len; + if (ch == '\n') params++; + } +} + +static void commandPrivmsg(uint id, char *params) { + splitMessage("PRIVMSG", id, params); +} + +static void commandNotice(uint id, char *params) { + splitMessage("NOTICE", id, params); +} + +static void commandMe(uint id, char *params) { + char buf[512]; + if (!params) params = ""; + int chunk = splitChunk("PRIVMSG \1ACTION\1", id); + if (strlen(params) <= (size_t)chunk && !strchr(params, '\n')) { + snprintf(buf, sizeof(buf), "\1ACTION %s\1", params); + echoMessage("PRIVMSG", id, buf); + return; + } + while (*params) { + int len = splitLen(chunk, params); + snprintf(buf, sizeof(buf), "\1ACTION %.*s\1", len, params); + echoMessage("PRIVMSG", id, buf); + params += len; + if (*params == '\n') params++; + } +} + +static void commandMsg(uint id, char *params) { + if (!params) return; + char *nick = strsep(¶ms, " "); + uint msg = idFor(nick); + if (idColors[msg] == Default) { + idColors[msg] = completeColor(id, nick); + } + if (params) { + splitMessage("PRIVMSG", msg, params); + } else { + windowShow(windowFor(msg)); + } +} + +static void commandJoin(uint id, char *params) { + if (!params && id == Network) params = self.invited; + if (!params) params = idNames[id]; + uint count = 1; + for (char *ch = params; *ch && *ch != ' '; ++ch) { + if (*ch == ',') count++; + } + ircFormat("JOIN %s\r\n", params); + replies[ReplyJoin] += count; + replies[ReplyTopic] += count; + replies[ReplyNames] += count; +} + +static void commandPart(uint id, char *params) { + if (params) { + ircFormat("PART %s :%s\r\n", idNames[id], params); + } else { + ircFormat("PART %s\r\n", idNames[id]); + } +} + +static void commandQuit(uint id, char *params) { + (void)id; + set(&self.quit, (params ?: "nyaa~")); +} + +static void commandNick(uint id, char *params) { + (void)id; + if (params) { + ircFormat("NICK :%s\r\n", params); + } else { + uiFormat( + Network, Warm, NULL, "You are \3%02d%s", + self.color, self.nick + ); + } +} + +static void commandAway(uint id, char *params) { + (void)id; + if (params) { + ircFormat("AWAY :%s\r\n", params); + } else { + ircFormat("AWAY\r\n"); + } + replies[ReplyAway]++; +} + +static void commandSetname(uint id, char *params) { + (void)id; + if (!params) return; + ircFormat("SETNAME :%s\r\n", params); +} + +static void commandTopic(uint id, char *params) { + if (params) { + ircFormat("TOPIC %s :%s\r\n", idNames[id], params); + } else { + ircFormat("TOPIC %s\r\n", idNames[id]); + replies[ReplyTopic]++; + } +} + +static void commandNames(uint id, char *params) { + (void)params; + ircFormat("NAMES %s\r\n", idNames[id]); + replies[ReplyNames]++; +} + +static void commandOps(uint id, char *params) { + (void)params; + char buf[1024]; + char *ptr = buf, *end = &buf[sizeof(buf)]; + ptr = seprintf( + ptr, end, "The council of \3%02d%s\3 are ", + idColors[id], idNames[id] + ); + bool first = true; + struct Cursor curs = {0}; + for (const char *nick; (nick = completeEach(&curs, id));) { + char prefix = bitPrefix(*completeBits(id, nick)); + if (!prefix || prefix == '+') continue; + ptr = seprintf( + ptr, end, "%s\3%02d%c%s\3", + (first ? "" : ", "), completeColor(id, nick), prefix, nick + ); + first = false; + } + if (first) { + uiFormat( + id, Warm, NULL, "\3%02d%s\3 is a lawless wasteland", + idColors[id], idNames[id] + ); + } else { + uiWrite(id, Warm, NULL, buf); + } +} + +static void commandInvite(uint id, char *params) { + if (!params) return; + char *nick = strsep(¶ms, " "); + ircFormat("INVITE %s %s\r\n", nick, idNames[id]); +} + +static void commandKick(uint id, char *params) { + if (!params) return; + char *nick = strsep(¶ms, " "); + if (params) { + ircFormat("KICK %s %s :%s\r\n", idNames[id], nick, params); + } else { + ircFormat("KICK %s %s\r\n", idNames[id], nick); + } +} + +static void commandMode(uint id, char *params) { + if (id == Network) { + if (params) { + ircFormat("MODE %s %s\r\n", self.nick, params); + } else { + ircFormat("MODE %s\r\n", self.nick); + replies[ReplyMode]++; + } + } else { + if (params) { + if (!params[1] || (params[0] == '+' && !params[2])) { + char m = (params[0] == '+' ? params[1] : params[0]); + if (m == 'b') replies[ReplyBan]++; + if (m == network.excepts) replies[ReplyExcepts]++; + if (m == network.invex) replies[ReplyInvex]++; + } + ircFormat("MODE %s %s\r\n", idNames[id], params); + } else { + ircFormat("MODE %s\r\n", idNames[id]); + replies[ReplyMode]++; + } + } +} + +static void channelListMode(uint id, char pm, char l, const char *params) { + int count = 1; + for (const char *ch = params; *ch; ++ch) { + if (*ch == ' ') count++; + } + char modes[13 + 1] = { l, l, l, l, l, l, l, l, l, l, l, l, l, '\0' }; + ircFormat("MODE %s %c%.*s %s\r\n", idNames[id], pm, count, modes, params); +} + +static void commandOp(uint id, char *params) { + if (params) { + channelListMode(id, '+', 'o', params); + } else { + ircFormat("CS OP %s\r\n", idNames[id]); + } +} + +static void commandDeop(uint id, char *params) { + channelListMode(id, '-', 'o', (params ?: self.nick)); +} + +static void commandVoice(uint id, char *params) { + if (params) { + channelListMode(id, '+', 'v', params); + } else { + ircFormat("CS VOICE %s\r\n", idNames[id]); + } +} + +static void commandDevoice(uint id, char *params) { + channelListMode(id, '-', 'v', (params ?: self.nick)); +} + +static void commandBan(uint id, char *params) { + if (params) { + channelListMode(id, '+', 'b', params); + } else { + ircFormat("MODE %s b\r\n", idNames[id]); + replies[ReplyBan]++; + } +} + +static void commandUnban(uint id, char *params) { + if (!params) return; + channelListMode(id, '-', 'b', params); +} + +static void commandExcept(uint id, char *params) { + if (params) { + channelListMode(id, '+', network.excepts, params); + } else { + ircFormat("MODE %s %c\r\n", idNames[id], network.excepts); + replies[ReplyExcepts]++; + } +} + +static void commandUnexcept(uint id, char *params) { + if (!params) return; + channelListMode(id, '-', network.excepts, params); +} + +static void commandInvex(uint id, char *params) { + if (params) { + channelListMode(id, '+', network.invex, params); + } else { + ircFormat("MODE %s %c\r\n", idNames[id], network.invex); + replies[ReplyInvex]++; + } +} + +static void commandUninvex(uint id, char *params) { + if (!params) return; + channelListMode(id, '-', network.invex, params); +} + +static void commandList(uint id, char *params) { + (void)id; + if (params) { + ircFormat("LIST :%s\r\n", params); + } else { + ircFormat("LIST\r\n"); + } + replies[ReplyList]++; +} + +static void commandWhois(uint id, char *params) { + (void)id; + if (!params) params = self.nick; + uint count = 1; + for (char *ch = params; *ch; ++ch) { + if (*ch == ',') count++; + } + ircFormat("WHOIS %s\r\n", params); + replies[ReplyWhois] += count; +} + +static void commandWhowas(uint id, char *params) { + (void)id; + if (!params) return; + ircFormat("WHOWAS %s\r\n", params); + replies[ReplyWhowas]++; +} + +static void commandNS(uint id, char *params) { + (void)id; + ircFormat("NS %s\r\n", (params ?: "HELP")); +} + +static void commandCS(uint id, char *params) { + (void)id; + ircFormat("CS %s\r\n", (params ?: "HELP")); +} + +static void commandQuery(uint id, char *params) { + if (!params) return; + uint query = idFor(params); + if (idColors[query] == Default) { + idColors[query] = completeColor(id, params); + } + windowShow(windowFor(query)); +} + +static void commandWindow(uint id, char *params) { + if (!params) { + windowList(); + } else if (isdigit(params[0])) { + windowShow(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) { + windowShow(windowFor(id)); + return; + } + struct Cursor curs = {0}; + for (const char *str; (str = completeSubstr(&curs, None, params));) { + id = idFind(str); + if (!id) continue; + completeAccept(&curs); + windowShow(windowFor(id)); + break; + } + } +} + +static void commandMove(uint id, char *params) { + if (!params) return; + char *name = strsep(¶ms, " "); + if (params) { + id = idFind(name); + if (id) windowMove(windowFor(id), strtoul(params, NULL, 10)); + } else { + windowMove(windowFor(id), strtoul(name, NULL, 10)); + } +} + +static void commandClose(uint id, char *params) { + if (!params) { + windowClose(windowFor(id)); + } else if (isdigit(params[0])) { + windowClose(strtoul(params, NULL, 10)); + } else { + id = idFind(params); + if (id) windowClose(windowFor(id)); + } +} + +static void commandOpen(uint id, char *params) { + if (!params) { + urlOpenCount(id, 1); + } else if (isdigit(params[0]) && !params[1]) { + urlOpenCount(id, params[0] - '0'); + } else { + urlOpenMatch(id, params); + } +} + +static void commandCopy(uint id, char *params) { + urlCopyMatch(id, params); +} + +static void commandFilter(enum Heat heat, uint id, char *params) { + if (params) { + struct Filter filter = filterAdd(heat, params); + uiFormat( + id, Cold, NULL, "%sing \3%02d%s %s %s %s", + (heat == Hot ? "Highlight" : "Ignor"), Brown, filter.mask, + (filter.cmd ?: ""), (filter.chan ?: ""), (filter.mesg ?: "") + ); + } else { + for (size_t i = 0; i < FilterCap && filters[i].mask; ++i) { + if (filters[i].heat != heat) continue; + uiFormat( + Network, Warm, NULL, "%sing \3%02d%s %s %s %s", + (heat == Hot ? "Highlight" : "Ignor"), Brown, filters[i].mask, + (filters[i].cmd ?: ""), (filters[i].chan ?: ""), + (filters[i].mesg ?: "") + ); + } + } +} + +static void commandUnfilter(enum Heat heat, uint id, char *params) { + if (!params) return; + struct Filter filter = filterParse(heat, params); + bool found = filterRemove(filter); + uiFormat( + id, Cold, NULL, "%s %sing \3%02d%s %s %s %s", + (found ? "No longer" : "Not"), (heat == Hot ? "highlight" : "ignor"), + Brown, filter.mask, (filter.cmd ?: ""), (filter.chan ?: ""), + (filter.mesg ?: "") + ); +} + +static void commandHighlight(uint id, char *params) { + commandFilter(Hot, id, params); +} +static void commandIgnore(uint id, char *params) { + commandFilter(Ice, id, params); +} +static void commandUnhighlight(uint id, char *params) { + commandUnfilter(Hot, id, params); +} +static void commandUnignore(uint id, char *params) { + commandUnfilter(Ice, id, params); +} + +static void commandExec(uint id, char *params) { + execID = id; + + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + setsid(); + close(STDIN_FILENO); + dup2(execPipe[1], STDOUT_FILENO); + dup2(utilPipe[1], STDERR_FILENO); + + const char *shell = getenv("SHELL") ?: "/bin/sh"; + execl(shell, shell, "-c", params, NULL); + warn("%s", shell); + _exit(EX_UNAVAILABLE); +} + +static void commandHelp(uint id, char *params) { + (void)id; + + if (params) { + ircFormat("HELP :%s\r\n", params); + replies[ReplyHelp]++; + return; + } + if (self.restricted) { + uiFormat(id, Warm, NULL, "See catgirl(1) or /help index"); + return; + } + + uiHide(); + pid_t pid = fork(); + if (pid < 0) err(EX_OSERR, "fork"); + if (pid) return; + + char buf[256]; + snprintf(buf, sizeof(buf), "%sp^COMMANDS$", (getenv("LESS") ?: "")); + setenv("LESS", buf, 1); + execlp("man", "man", "1", "catgirl", NULL); + dup2(utilPipe[1], STDERR_FILENO); + warn("man"); + _exit(EX_UNAVAILABLE); +} + +enum Flag { + BIT(Multiline), + BIT(Restrict), +}; + +static const struct Handler { + const char *cmd; + Command *fn; + enum Flag flags; + enum Cap caps; +} Commands[] = { + { "/away", commandAway, 0, 0 }, + { "/ban", commandBan, 0, 0 }, + { "/close", commandClose, 0, 0 }, + { "/copy", commandCopy, Restrict, 0 }, + { "/cs", commandCS, 0, 0 }, + { "/debug", commandDebug, 0, 0 }, + { "/deop", commandDeop, 0, 0 }, + { "/devoice", commandDevoice, 0, 0 }, + { "/except", commandExcept, 0, 0 }, + { "/exec", commandExec, Multiline | Restrict, 0 }, + { "/help", commandHelp, 0, 0 }, // Restrict special case. + { "/highlight", commandHighlight, 0, 0 }, + { "/ignore", commandIgnore, 0, 0 }, + { "/invex", commandInvex, 0, 0 }, + { "/invite", commandInvite, 0, 0 }, + { "/join", commandJoin, 0, 0 }, + { "/kick", commandKick, 0, 0 }, + { "/list", commandList, 0, 0 }, + { "/me", commandMe, Multiline, 0 }, + { "/mode", commandMode, 0, 0 }, + { "/move", commandMove, 0, 0 }, + { "/msg", commandMsg, Multiline, 0 }, + { "/names", commandNames, 0, 0 }, + { "/nick", commandNick, 0, 0 }, + { "/notice", commandNotice, Multiline, 0 }, + { "/ns", commandNS, 0, 0 }, + { "/o", commandOpen, Restrict, 0 }, + { "/op", commandOp, 0, 0 }, + { "/open", commandOpen, Restrict, 0 }, + { "/ops", commandOps, 0, 0 }, + { "/part", commandPart, 0, 0 }, + { "/query", commandQuery, 0, 0 }, + { "/quit", commandQuit, 0, 0 }, + { "/quote", commandQuote, Multiline, 0 }, + { "/say", commandPrivmsg, Multiline, 0 }, + { "/setname", commandSetname, 0, CapSetname }, + { "/topic", commandTopic, 0, 0 }, + { "/unban", commandUnban, 0, 0 }, + { "/unexcept", commandUnexcept, 0, 0 }, + { "/unhighlight", commandUnhighlight, 0, 0 }, + { "/unignore", commandUnignore, 0, 0 }, + { "/uninvex", commandUninvex, 0, 0 }, + { "/voice", commandVoice, 0, 0 }, + { "/whois", commandWhois, 0, 0 }, + { "/whowas", commandWhowas, 0, 0 }, + { "/window", commandWindow, 0, 0 }, +}; + +static int compar(const void *cmd, const void *_handler) { + const struct Handler *handler = _handler; + return strcmp(cmd, handler->cmd); +} + +const char *commandIsPrivmsg(uint id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (input[0] != '/') return input; + const char *space = strchr(&input[1], ' '); + const char *slash = strchr(&input[1], '/'); + if (slash && (!space || slash < space)) return input; + return NULL; +} + +const char *commandIsNotice(uint id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/notice ", 8)) return NULL; + return &input[8]; +} + +const char *commandIsAction(uint id, const char *input) { + if (id == Network || id == Debug) return NULL; + if (strncmp(input, "/me ", 4)) return NULL; + return &input[4]; +} + +size_t commandWillSplit(uint id, const char *input) { + int chunk; + const char *params; + if (NULL != (params = commandIsPrivmsg(id, input))) { + chunk = splitChunk("PRIVMSG", id); + } else if (NULL != (params = commandIsNotice(id, input))) { + chunk = splitChunk("NOTICE", id); + } else if (NULL != (params = commandIsAction(id, input))) { + chunk = splitChunk("PRIVMSG \1ACTION\1", id); + } else if (id != Network && id != Debug && !strncmp(input, "/say ", 5)) { + params = &input[5]; + chunk = splitChunk("PRIVMSG", id); + } else { + return 0; + } + if (strlen(params) <= (size_t)chunk) return 0; + for ( + int split; + params[(split = splitLen(chunk, params))]; + params = ¶ms[split + 1] + ) { + if (params[split] == '\n') continue; + return (params - input) + split; + } + return 0; +} + +static bool commandAvailable(const struct Handler *handler) { + if (handler->flags & Restrict && self.restricted) return false; + if (handler->caps && (handler->caps & self.caps) != handler->caps) { + return false; + } + return true; +} + +void command(uint id, char *input) { + if (id == Debug && input[0] != '/' && !self.restricted) { + commandQuote(id, input); + return; + } else if (!input[0]) { + return; + } else if (commandIsPrivmsg(id, input)) { + commandPrivmsg(id, input); + return; + } else if (input[0] == '/' && isdigit(input[1])) { + commandWindow(id, &input[1]); + return; + } + + struct Cursor curs = {0}; + const char *cmd = strsep(&input, " "); + const char *unique = completePrefix(&curs, None, cmd); + if (unique && !completePrefix(&curs, None, cmd)) { + cmd = unique; + } + + const struct Handler *handler = bsearch( + cmd, Commands, ARRAY_LEN(Commands), sizeof(*handler), compar + ); + if (!handler) { + uiFormat(id, Warm, NULL, "No such command %s", cmd); + return; + } + if (!commandAvailable(handler)) { + uiFormat(id, Warm, NULL, "Command %s is unavailable", cmd); + return; + } + + if (input) { + if (!(handler->flags & Multiline)) { + input[strcspn(input, "\n")] = '\0'; + } + input += strspn(input, " "); + size_t len = strlen(input); + while (input[len - 1] == ' ') input[--len] = '\0'; + if (!input[0]) input = NULL; + } + handler->fn(id, input); +} + +void commandCompletion(void) { + for (size_t i = 0; i < ARRAY_LEN(Commands); ++i) { + if (!commandAvailable(&Commands[i])) continue; + completePush(None, Commands[i].cmd, Default); + } +} |