1// Copyright (c) 2016-2017 Daniel Oaks <daniel@danieloaks.net> 2// released under the MIT license 3 4package irc 5 6import ( 7 "fmt" 8 "sort" 9 "strings" 10 "sync" 11 12 "github.com/ergochat/ergo/irc/languages" 13) 14 15// HelpEntryType represents the different sorts of help entries that can exist. 16type HelpEntryType int 17 18const ( 19 // CommandHelpEntry is a help entry explaining a client command. 20 CommandHelpEntry HelpEntryType = iota 21 // InformationHelpEntry is a help entry explaining general server info. 22 InformationHelpEntry 23 // ISupportHelpEntry is a help entry explaining a specific RPL_ISUPPORT token. 24 ISupportHelpEntry 25) 26 27// HelpEntry represents an entry in the Help map. 28type HelpEntry struct { 29 oper bool 30 text string 31 textGenerator func(*Client) string 32 helpType HelpEntryType 33 duplicate bool 34} 35 36// used for duplicates 37var ( 38 cmodeHelpText = `== Channel Modes == 39 40Ergo supports the following channel modes: 41 42 +b | Client masks that are banned from the channel (e.g. *!*@127.0.0.1) 43 +e | Client masks that are exempted from bans. 44 +I | Client masks that are exempted from the invite-only flag. 45 +i | Invite-only mode, only invited clients can join the channel. 46 +k | Key required when joining the channel. 47 +l | Client join limit for the channel. 48 +f | Users who are unable to join this channel (due to another mode) are forwarded 49 to the provided channel instead. 50 +m | Moderated mode, only privileged clients can talk on the channel. 51 +n | No-outside-messages mode, only users that are on the channel can send 52 | messages to it. 53 +R | Only registered users can join the channel. 54 +M | Only registered or voiced users can speak in the channel. 55 +s | Secret mode, channel won't show up in /LIST or whois replies. 56 +t | Only channel opers can modify the topic. 57 +E | Roleplaying commands are enabled in the channel. 58 +C | Clients are blocked from sending CTCP messages in the channel. 59 +u | Auditorium mode: JOIN, PART, QUIT, NAMES, and WHO are hidden 60 from unvoiced clients. 61 +U | Op-moderated mode: messages from unprivileged clients are sent 62 only to channel operators. 63 64= Prefixes = 65 66 +q (~) | Founder channel mode. 67 +a (&) | Admin channel mode. 68 +o (@) | Operator channel mode. 69 +h (%) | Halfop channel mode. 70 +v (+) | Voice channel mode.` 71 umodeHelpText = `== User Modes == 72 73Ergo supports the following user modes: 74 75 +a | User is marked as being away. This mode is set with the /AWAY command. 76 +i | User is marked as invisible (their channels are hidden from whois replies). 77 +o | User is an IRC operator. 78 +R | User only accepts messages from other registered users. 79 +s | Server Notice Masks (see help with /HELPOP snomasks). 80 +Z | User is connected via TLS. 81 +B | User is a bot. 82 +E | User can receive roleplaying commands. 83 +T | CTCP messages to the user are blocked.` 84 snomaskHelpText = `== Server Notice Masks == 85 86Ergo supports the following server notice masks for operators: 87 88 a | Local announcements. 89 c | Local client connections. 90 d | Local client disconnects. 91 j | Local channel actions. 92 k | Local kills. 93 n | Local nick changes. 94 o | Local oper actions. 95 q | Local quits. 96 t | Local /STATS usage. 97 u | Local client account actions. 98 x | Local X-lines (DLINE/KLINE/etc). 99 v | Local vhost changes. 100 101To set a snomask, do this with your nickname: 102 103 /MODE <nick> +s <chars> 104 105For instance, this would set the kill, oper, account and xline snomasks on dan: 106 107 /MODE dan +s koux` 108) 109 110// Help contains the help strings distributed with the IRCd. 111var Help = map[string]HelpEntry{ 112 // Commands 113 "ambiance": { 114 text: `AMBIANCE <target> <text to be sent> 115 116The AMBIANCE command is used to send a scene notification to the given target.`, 117 }, 118 "authenticate": { 119 text: `AUTHENTICATE 120 121Used during SASL authentication. See the IRCv3 specs for more info: 122http://ircv3.net/specs/extensions/sasl-3.1.html`, 123 }, 124 "away": { 125 text: `AWAY [message] 126 127If [message] is sent, marks you away. If [message] is not sent, marks you no 128longer away.`, 129 }, 130 "batch": { 131 text: `BATCH {+,-}reference-tag type [params...] 132 133BATCH initiates an IRCv3 client-to-server batch. You should never need to 134issue this command manually.`, 135 }, 136 "cap": { 137 text: `CAP <subcommand> [:<capabilities>] 138 139Used in capability negotiation. See the IRCv3 specs for more info: 140http://ircv3.net/specs/core/capability-negotiation-3.1.html 141http://ircv3.net/specs/core/capability-negotiation-3.2.html`, 142 }, 143 "chathistory": { 144 text: `CHATHISTORY [params] 145 146CHATHISTORY is a history replay command associated with the IRCv3 147specification draft/chathistory. See this document: 148https://github.com/ircv3/ircv3-specifications/pull/393`, 149 }, 150 "debug": { 151 oper: true, 152 text: `DEBUG <option> 153 154Provides various debugging commands for the IRCd. <option> can be one of: 155 156* GCSTATS: Garbage control statistics. 157* NUMGOROUTINE: Number of goroutines in use. 158* STARTCPUPROFILE: Starts the CPU profiler. 159* STOPCPUPROFILE: Stops the CPU profiler. 160* PROFILEHEAP: Writes a memory profile. 161* CRASHSERVER: Crashes the server (for use in failover testing)`, 162 }, 163 "defcon": { 164 oper: true, 165 text: `DEFCON [level] 166 167The DEFCON system can disable server features at runtime, to mitigate 168spam or other hostile activity. It has five levels, which are cumulative 169(i.e., level 3 includes all restrictions from level 4 and so on): 170 1715: Normal operation 1724: No new account or channel registrations; if Tor is enabled, no new 173 unauthenticated connections from Tor 1743: All users are +R; no changes to vhosts 1752: No new unauthenticated connections; all channels are +R 1761: No new connections except from localhost or other trusted IPs`, 177 }, 178 "deoper": { 179 oper: true, 180 text: `DEOPER 181 182DEOPER removes the IRCop privileges granted to you by a successful /OPER.`, 183 }, 184 "dline": { 185 oper: true, 186 text: `DLINE [ANDKILL] [MYSELF] [duration] <ip>/<net> [ON <server>] [reason [| oper reason]] 187DLINE LIST 188 189Bans an IP address or network from connecting to the server. If the duration is 190given then only for that long. The reason is shown to the user themselves, but 191everyone else will see a standard message. The oper reason is shown to 192operators getting info about the DLINEs that exist. 193 194Bans are saved across subsequent launches of the server. 195 196"ANDKILL" means that all matching clients are also removed from the server. 197 198"MYSELF" is required when the DLINE matches the address the person applying it is connected 199from. If "MYSELF" is not given, trying to DLINE yourself will result in an error. 200 201[duration] can be of the following forms: 202 1y 12mo 31d 10h 8m 13s 203 204<net> is specified in typical CIDR notation. For example: 205 127.0.0.1/8 206 8.8.8.8/24 207 208ON <server> specifies that the ban is to be set on that specific server. 209 210[reason] and [oper reason], if they exist, are separated by a vertical bar (|). 211 212If "DLINE LIST" is sent, the server sends back a list of our current DLINEs. 213 214To remove a DLINE, use the "UNDLINE" command.`, 215 }, 216 "extjwt": { 217 text: `EXTJWT <target> [service_name] 218 219Get a JSON Web Token for target (either * or a channel name).`, 220 }, 221 "help": { 222 text: `HELP <argument> 223 224Get an explanation of <argument>, or "index" for a list of help topics.`, 225 }, 226 "helpop": { 227 text: `HELPOP <argument> 228 229Get an explanation of <argument>, or "index" for a list of help topics.`, 230 }, 231 "history": { 232 text: `HISTORY <target> [limit] 233 234Replay message history. <target> can be a channel name, "me" to replay direct 235message history, or a nickname to replay another client's direct message 236history (they must be logged into the same account as you). [limit] can be 237either an integer (the maximum number of messages to replay), or a time 238duration like 10m or 1h (the time window within which to replay messages).`, 239 }, 240 "info": { 241 text: `INFO 242 243Sends information about the server, developers, etc.`, 244 }, 245 "invite": { 246 text: `INVITE <nickname> <channel> 247 248Invites the given user to the given channel, so long as you have the 249appropriate channel privs.`, 250 }, 251 "ison": { 252 text: `ISON <nickname>{ <nickname>} 253 254Returns whether the given nicks exist on the network.`, 255 }, 256 "join": { 257 text: `JOIN <channel>{,<channel>} [<key>{,<key>}] 258 259Joins the given channels with the matching keys.`, 260 }, 261 "kick": { 262 text: `KICK <channel> <user> [reason] 263 264Removes the user from the given channel, so long as you have the appropriate 265channel privs.`, 266 }, 267 "kill": { 268 oper: true, 269 text: `KILL <nickname> [reason] 270 271Removes the given user from the network, showing them the reason if it is 272supplied.`, 273 }, 274 "kline": { 275 oper: true, 276 text: `KLINE [ANDKILL] [MYSELF] [duration] <mask> [ON <server>] [reason [| oper reason]] 277KLINE LIST 278 279Bans a mask from connecting to the server. If the duration is given then only for that 280long. The reason is shown to the user themselves, but everyone else will see a standard 281message. The oper reason is shown to operators getting info about the KLINEs that exist. 282 283Bans are saved across subsequent launches of the server. 284 285"ANDKILL" means that all matching clients are also removed from the server. 286 287"MYSELF" is required when the KLINE matches the address the person applying it is connected 288from. If "MYSELF" is not given, trying to KLINE yourself will result in an error. 289 290[duration] can be of the following forms: 291 1y 12mo 31d 10h 8m 13s 292 293<mask> is specified in typical IRC format. For example: 294 dan 295 dan!5*@127.* 296 297ON <server> specifies that the ban is to be set on that specific server. 298 299[reason] and [oper reason], if they exist, are separated by a vertical bar (|). 300 301If "KLINE LIST" is sent, the server sends back a list of our current KLINEs. 302 303To remove a KLINE, use the "UNKLINE" command.`, 304 }, 305 "language": { 306 text: `LANGUAGE <code>{ <code>} 307 308Sets your preferred languages to the given ones.`, 309 }, 310 "list": { 311 text: `LIST [<channel>{,<channel>}] [<elistcond>{,<elistcond>}] 312 313Shows information on the given channels (or if none are given, then on all 314channels). <elistcond>s modify how the channels are selected.`, 315 //TODO(dan): Explain <elistcond>s in more specific detail 316 }, 317 "lusers": { 318 text: `LUSERS [<mask> [<server>]] 319 320Shows statistics about the size of the network. If <mask> is given, only 321returns stats for servers matching the given mask. If <server> is given, the 322command is processed by that server.`, 323 }, 324 "mode": { 325 text: `MODE <target> [<modestring> [<mode arguments>...]] 326 327Sets and removes modes from the given target. For more specific information on 328mode characters, see the help for "modes".`, 329 }, 330 "monitor": { 331 text: `MONITOR <subcmd> 332 333Allows the monitoring of nicknames, for alerts when they are online and 334offline. The subcommands are: 335 336 MONITOR + target{,target} 337Adds the given names to your list of monitored nicknames. 338 339 MONITOR - target{,target} 340Removes the given names from your list of monitored nicknames. 341 342 MONITOR C 343Clears your list of monitored nicknames. 344 345 MONITOR L 346Lists all the nicknames you are currently monitoring. 347 348 MONITOR S 349Lists whether each nick in your MONITOR list is online or offline.`, 350 }, 351 "motd": { 352 text: `MOTD [server] 353 354Returns the message of the day for this, or the given, server.`, 355 }, 356 "names": { 357 text: `NAMES [<channel>{,<channel>}] 358 359Views the clients joined to a channel and their channel membership prefixes. To 360view the channel membership prefixes supported by this server, see the help for 361"PREFIX".`, 362 }, 363 "nick": { 364 text: `NICK <newnick> 365 366Sets your nickname to the new given one.`, 367 }, 368 "notice": { 369 text: `NOTICE <target>{,<target>} <text to be sent> 370 371Sends the text to the given targets as a NOTICE.`, 372 }, 373 "npc": { 374 text: `NPC <target> <sourcenick> <text to be sent> 375 376The NPC command is used to send a message to the target as the source. 377 378Requires the roleplay mode (+E) to be set on the target.`, 379 }, 380 "npca": { 381 text: `NPCA <target> <sourcenick> <text to be sent> 382 383The NPC command is used to send an action to the target as the source. 384 385Requires the roleplay mode (+E) to be set on the target.`, 386 }, 387 "oper": { 388 text: `OPER <name> [password] 389 390If the correct details are given, gives you IRCop privs.`, 391 }, 392 "part": { 393 text: `PART <channel>{,<channel>} [reason] 394 395Leaves the given channels and shows people the given reason.`, 396 }, 397 "pass": { 398 text: `PASS <password> 399 400When the server requires a connection password to join, used to send us the 401password.`, 402 }, 403 "ping": { 404 text: `PING <args>... 405 406Requests a PONG. Used to check link connectivity.`, 407 }, 408 "pong": { 409 text: `PONG <args>... 410 411Replies to a PING. Used to check link connectivity.`, 412 }, 413 "privmsg": { 414 text: `PRIVMSG <target>{,<target>} <text to be sent> 415 416Sends the text to the given targets as a PRIVMSG.`, 417 }, 418 "relaymsg": { 419 text: `RELAYMSG <channel> <spoofed nick> :<message> 420 421This command lets channel operators relay messages to their 422channel from other messaging systems using relay bots. The 423spoofed nickname MUST contain a forwardslash. 424 425For example: 426 RELAYMSG #ircv3 Mallory/D :Welp, we linked Discord...`, 427 }, 428 "rename": { 429 text: `RENAME <channel> <newname> [<reason>] 430 431Renames the given channel with the given reason, if possible. 432 433For example: 434 RENAME #ircv2 #ircv3 :Protocol upgrades!`, 435 }, 436 "sajoin": { 437 oper: true, 438 text: `SAJOIN [nick] #channel{,#channel} 439 440Forcibly joins a user to a channel, ignoring restrictions like bans, user limits 441and channel keys. If [nick] is omitted, it defaults to the operator.`, 442 }, 443 "sanick": { 444 oper: true, 445 text: `SANICK <currentnick> <newnick> 446 447Gives the given user a new nickname.`, 448 }, 449 "samode": { 450 oper: true, 451 text: `SAMODE <target> [<modestring> [<mode arguments>...]] 452 453Forcibly sets and removes modes from the given target -- only available to 454opers. For more specific information on mode characters, see the help for 455"cmode" and "umode".`, 456 }, 457 "scene": { 458 text: `SCENE <target> <text to be sent> 459 460The SCENE command is used to send a scene notification to the given target.`, 461 }, 462 "setname": { 463 text: `SETNAME <realname> 464 465The SETNAME command updates the realname to be the newly-given one.`, 466 }, 467 "summon": { 468 text: `SUMMON [parameters] 469 470The SUMMON command is not implemented.`, 471 }, 472 "tagmsg": { 473 text: `@+client-only-tags TAGMSG <target>{,<target>} 474 475Sends the given client-only tags to the given targets as a TAGMSG. See the IRCv3 476specs for more info: http://ircv3.net/specs/core/message-tags-3.3.html`, 477 }, 478 "quit": { 479 text: `QUIT [reason] 480 481Indicates that you're leaving the server, and shows everyone the given reason.`, 482 }, 483 "register": { 484 text: `REGISTER <email | *> <password> 485 486Registers an account in accordance with the draft/register capability.`, 487 }, 488 "rehash": { 489 oper: true, 490 text: `REHASH 491 492Reloads the config file and updates TLS certificates on listeners`, 493 }, 494 "time": { 495 text: `TIME [server] 496 497Shows the time of the current, or the given, server.`, 498 }, 499 "topic": { 500 text: `TOPIC <channel> [topic] 501 502If [topic] is given, sets the topic in the channel to that. If [topic] is not 503given, views the current topic on the channel.`, 504 }, 505 "uban": { 506 text: `UBAN <subcommand> [arguments] 507 508Ergo's "unified ban" system. Accepts the following subcommands: 509 5101. UBAN ADD <target> [REQUIRE-SASL] [DURATION <duration>] [REASON...] 5112. UBAN DEL <target> 5123. UBAN LIST 5134. UBAN INFO <target> 514 515<target> may be an IP, a CIDR, a nickmask with wildcards, or the name of an 516account to suspend. Note that REQUIRE-SASL is only valid for IP and CIDR bans.`, 517 }, 518 "undline": { 519 oper: true, 520 text: `UNDLINE <ip>/<net> 521 522Removes an existing ban on an IP address or a network. 523 524<net> is specified in typical CIDR notation. For example: 525 127.0.0.1/8 526 8.8.8.8/24`, 527 }, 528 "unkline": { 529 oper: true, 530 text: `UNKLINE <mask> 531 532Removes an existing ban on a mask. 533 534For example: 535 dan 536 dan!5*@127.*`, 537 }, 538 "user": { 539 text: `USER <username> 0 * <realname> 540 541Used in connection registration, sets your username and realname to the given 542values (though your username may also be looked up with Ident).`, 543 }, 544 "uninvite": { 545 text: `UNINVITE <nickname> <channel> 546 547UNINVITE rescinds a channel invitation sent for an invite-only channel.`, 548 }, 549 "users": { 550 text: `USERS [parameters] 551 552The USERS command is not implemented.`, 553 }, 554 "userhost": { 555 text: `USERHOST <nickname>{ <nickname>} 556 557Shows information about the given users. Takes up to 10 nicknames.`, 558 }, 559 "verify": { 560 text: `VERIFY <account> <password> 561 562Verifies an account in accordance with the draft/register capability.`, 563 }, 564 "version": { 565 text: `VERSION [server] 566 567Views the version of software and the RPL_ISUPPORT tokens for the given server.`, 568 }, 569 "webirc": { 570 oper: true, // not really, but it's restricted anyways 571 text: `WEBIRC <password> <gateway> <hostname> <ip> [:<flags>] 572 573Used by web<->IRC gateways and bouncers, the WEBIRC command allows gateways to 574pass-through the real IP addresses of clients: 575ircv3.net/specs/extensions/webirc.html 576 577<flags> is a list of space-separated strings indicating various details about 578the connection from the client to the gateway, such as: 579 580- tls: this flag indicates that the client->gateway connection is secure`, 581 }, 582 "who": { 583 text: `WHO <name> [o] 584 585Returns information for the given user.`, 586 }, 587 "whois": { 588 text: `WHOIS <client>{,<client>} 589 590Returns information for the given user(s).`, 591 }, 592 "whowas": { 593 text: `WHOWAS <nickname> 594 595Returns historical information on the last user with the given nickname.`, 596 }, 597 "znc": { 598 text: `ZNC <module> [params] 599 600Used to emulate features of the ZNC bouncer. This command is not intended 601for direct use by end users.`, 602 duplicate: true, 603 }, 604 605 // Informational 606 "modes": { 607 textGenerator: modesTextGenerator, 608 helpType: InformationHelpEntry, 609 }, 610 "cmode": { 611 text: cmodeHelpText, 612 helpType: InformationHelpEntry, 613 }, 614 "cmodes": { 615 text: cmodeHelpText, 616 helpType: InformationHelpEntry, 617 duplicate: true, 618 }, 619 "umode": { 620 text: umodeHelpText, 621 helpType: InformationHelpEntry, 622 }, 623 "umodes": { 624 text: umodeHelpText, 625 helpType: InformationHelpEntry, 626 duplicate: true, 627 }, 628 "snomask": { 629 text: snomaskHelpText, 630 helpType: InformationHelpEntry, 631 oper: true, 632 duplicate: true, 633 }, 634 "snomasks": { 635 text: snomaskHelpText, 636 helpType: InformationHelpEntry, 637 oper: true, 638 }, 639 640 // RPL_ISUPPORT 641 "casemapping": { 642 text: `RPL_ISUPPORT CASEMAPPING 643 644Ergo supports an experimental unicode casemapping designed for extended 645Unicode support. This casemapping is based off RFC 7613 and the draft rfc7613 646casemapping spec here: https://ergo.chat/specs.html`, 647 helpType: ISupportHelpEntry, 648 }, 649 "prefix": { 650 text: `RPL_ISUPPORT PREFIX 651 652Ergo supports the following channel membership prefixes: 653 654 +q (~) | Founder channel mode. 655 +a (&) | Admin channel mode. 656 +o (@) | Operator channel mode. 657 +h (%) | Halfop channel mode. 658 +v (+) | Voice channel mode.`, 659 helpType: ISupportHelpEntry, 660 }, 661} 662 663// modesTextGenerator generates the text for the 'modes' help entry. 664// it exists only so we can translate this entry appropriately. 665func modesTextGenerator(client *Client) string { 666 return client.t(cmodeHelpText) + "\n\n" + client.t(umodeHelpText) 667} 668 669type HelpIndexManager struct { 670 sync.RWMutex // tier 1 671 672 langToIndex map[string]string 673 langToOperIndex map[string]string 674} 675 676// GenerateHelpIndex is used to generate HelpIndex. 677// Returns: a map from language code to the help index in that language. 678func GenerateHelpIndex(lm *languages.Manager, forOpers bool) map[string]string { 679 // generate the help entry lists 680 var commands, isupport, information []string 681 682 var line string 683 for name, info := range Help { 684 if info.duplicate { 685 continue 686 } 687 if info.oper && !forOpers { 688 continue 689 } 690 691 line = fmt.Sprintf(" %s", name) 692 693 if info.helpType == CommandHelpEntry { 694 commands = append(commands, line) 695 } else if info.helpType == ISupportHelpEntry { 696 isupport = append(isupport, line) 697 } else if info.helpType == InformationHelpEntry { 698 information = append(information, line) 699 } 700 } 701 702 // create the strings 703 sort.Strings(commands) 704 commandsString := strings.Join(commands, "\n") 705 sort.Strings(isupport) 706 isupportString := strings.Join(isupport, "\n") 707 sort.Strings(information) 708 informationString := strings.Join(information, "\n") 709 710 // sub them in 711 defaultHelpIndex := `= Help Topics = 712 713Commands: 714%[1]s 715 716RPL_ISUPPORT Tokens: 717%[2]s 718 719Information: 720%[3]s` 721 722 newHelpIndex := make(map[string]string) 723 724 newHelpIndex["en"] = fmt.Sprintf(defaultHelpIndex, commandsString, isupportString, informationString) 725 726 for langCode := range lm.Languages { 727 translatedHelpIndex := lm.Translate([]string{langCode}, defaultHelpIndex) 728 if translatedHelpIndex != defaultHelpIndex { 729 newHelpIndex[langCode] = fmt.Sprintf(translatedHelpIndex, commandsString, isupportString, informationString) 730 } 731 } 732 733 return newHelpIndex 734} 735 736// GenerateIndices regenerates our help indexes for each currently enabled language. 737func (hm *HelpIndexManager) GenerateIndices(lm *languages.Manager) { 738 // generate help indexes 739 langToIndex := GenerateHelpIndex(lm, false) 740 langToOperIndex := GenerateHelpIndex(lm, true) 741 742 hm.Lock() 743 defer hm.Unlock() 744 hm.langToIndex = langToIndex 745 hm.langToOperIndex = langToOperIndex 746} 747 748// sendHelp sends the client help of the given string. 749func (client *Client) sendHelp(helpEntry string, text string, rb *ResponseBuffer) { 750 helpEntry = strings.ToUpper(helpEntry) 751 nick := client.Nick() 752 textLines := strings.Split(text, "\n") 753 754 for i, line := range textLines { 755 if i == 0 { 756 rb.Add(nil, client.server.name, RPL_HELPSTART, nick, helpEntry, line) 757 } else { 758 rb.Add(nil, client.server.name, RPL_HELPTXT, nick, helpEntry, line) 759 } 760 } 761 rb.Add(nil, client.server.name, RPL_ENDOFHELP, nick, helpEntry, client.t("End of /HELPOP")) 762} 763 764// GetHelpIndex returns the help index for the given language. 765func (hm *HelpIndexManager) GetIndex(languages []string, oper bool) string { 766 hm.RLock() 767 langToIndex := hm.langToIndex 768 if oper { 769 langToIndex = hm.langToOperIndex 770 } 771 hm.RUnlock() 772 773 for _, lang := range languages { 774 index, exists := langToIndex[lang] 775 if exists { 776 return index 777 } 778 } 779 // 'en' always exists 780 return langToIndex["en"] 781} 782 783func init() { 784 // startup check that we have HELP entries for every command 785 for name := range Commands { 786 _, exists := Help[strings.ToLower(name)] 787 if !exists { 788 panic(fmt.Sprintf("Help entry does not exist for command %s", name)) 789 } 790 } 791} 792