[
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\n.idea\n*.iml\n\n*.swp\n\nexample/everything/everything\nexample/minimal/minimal\nexample/repl/repl\nplugins/*/console/console\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"{}\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright {yyyy} {name of copyright owner}\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "NOTICE",
    "content": "The font in hal/text2image.go is derived from the IBM VGA 8x16 font\nfrom http://int10h.org/oldschool-pc-fonts/ which is under Creative\nCommons Attribution-ShareAlike 4.0 International.\n"
  },
  {
    "path": "OSSMETADATA",
    "content": "osslifecycle=active\n"
  },
  {
    "path": "README.md",
    "content": "# Hal-9001\n\nHal-9001 is a Go library that offers a number of facilities for creating a bot\nand its plugins.\n\n# Goals\n\n* make easy things easy and hard things accessible\n* 15 minutes from getting started to a working bot\n* optimize for long-term maintenance\n\n# Requirements\n\n* Go >= 1.5\n\nIt should build with older versions of Go but it has not been tested.\n\n# Creating your own bot\n\nThe easiest place to start is with the examples in the examples directory. Take\na look at what's there and copy the main.go of your favorite into a new repo\nand start editing it to your taste.\n\nexamples/everything/main.go has the most coverage of Hal's features and has\ncommentary throughout the file that should help you get going.\n\nTODO: add more of a tutorial here / on the wiki\n\n# Building\n\nA few dependencies are required by Hal's core library and plugins. For\nbuilding the examples/everything demo, you will need the following. Hal\ncore requires at least the mysql driver to build. Everything else is\na dependency of a plugin or broker and can be omitted if you don't import\nthose.\n\n```\ngo get github.com/nlopes/slack\ngo get github.com/mattn/go-xmpp\ngo get github.com/codegangsta/cli\ngo get github.com/go-sql-driver/mysql\n\n# optional - currently only used in examples/repl\ngo get gopkg.in/DATA-DOG/go-sqlmock.v1\n```\n\n# Using Hal in chat\n\nMost bots built with hal start the pluginmgr plugin first. The pluginmgr\nallows users to enable and configure plugins from inside the chat system.\n\ne.g.\n\n```\n!plugin attach uptime\n!plugin detach uptime\n!plugin attach uptime --regex ^[[:space:]]*!up\n!plugin list\n```\n\n# Terminology\n\n### Event\n\nHal's events (hal.Evt) are an abstraction of the messages/events that\nbrokers produce/consume. An event has a Body, User, Room, and timestamp.\n\nThe handle offers some convenience methods for replying to events and\nother tedious bits around processing them.\n\n### Broker\n\nA broker is a 2-way producer/consumer of events. The code that hooks\nhal up to Slack, Hipchat, and others are brokers. There is a hal.Broker\ninterface that defines the required behavior of brokers.\n\n### Plugin\n\nA plugin is a function that processes events with metadata. Plugins do\nnothing until they are attached to a room in the plugin manager.\n\n### Instance\n\nAn instance is a plugin that has been attached to a room.\n\n### Room\n\nHal calls all channels/rooms/related concepts rooms. Mostly \"room\" was picked\nbecause calling things channels in Go code gets confusing when you're also\nusing channels extensively.\n\n# Authoring Plugins\n\nHal plugins should be in a package. You can have more than one plugin\nper package. Some ship with Hal, others are in their own repos and\ncan be added with go get/import.\n\nBecause plugins are not activated automatically and can be bound to channels\nwith separate configs, they have to be registered and then instantiated.\n\n```go\npackage uptime\n\n// uptime: the simplest useful plugin possible\n\nimport (\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar booted time.Time\n\nfunc init() {\n\tbooted = time.Now()\n}\n\n// The plugin's Register() should be called from main() in the bot to\n// make the plugin available for use at runtime. It can be called anything\n// you like, but most of the plugins call it Register().\n//\n// Plugins are not tied to a specific broker so if it is going to use\n// the evt.Original field, be careful about double-checking the type\n// of message or evt.Broker to make sure it's safe to use.\nfunc Register() {\n\tp := hal.Plugin{\n\t\tName:   \"uptime\",\n\t\tFunc:   uptime,\n\t\tRegex:  \"^!uptime\",\n\t}\n\n\tp.Register()\n}\n\n// uptime implements the plugin itself\nfunc uptime(evt hal.Evt) {\n\tut := time.Since(booted)\n\tevt.Replyf(\"uptime: %s\", ut.String())\n}\n```\n\n# Rationale\n\nSome constructs in Hal are the result of a few decisions that deserve explanation.\n\n## MySQL as the only supported database driver\n\nRight now, only mysql-compatible database backends are supported. This is\nunlikely to change. Coding directly against a specific database allows Hal\nto use database-specific features and avoid unncessarry abstractions or loss\nof power required to support other databases.\n\nNetflix runs its bot in AWS using Aurora with local testing against MariaDB.\n\n## missing tests & ubiquitous assertions\n\nThis is not a permanent situation. The API changed a lot as the bot was being\nbuilt and tests were frequently invalidated. Now that the API is more stable,\ntests are being added back over time.\n\nIn order to speed up development and reduce the frequence of error checking\nin plugin/bot code, many parts of hal simply crash the program when errors\noccur. This makes assumptions about errors obvious and immediately visible\nwithout having to bubble errors up into consumer code at the cost of having\nto run your hal bot under a supervisor. When reasons are found to convert\nfatal errors into error returns, code should be refactored to do so.\n\n# TODO\n\n- [ ] implement sensible REST patterns for HTTP endpoints\n- [ ] work on the TODOs sprinkled throughout the code\n- [ ] provide more examples, e.g. slack-only, hipchat-only, console + slack\n- [ ] logging hooks to redirect logs to a channel\n- [ ] revive/update the Docker plugin\n- [ ] update constants to match the Go standards\n\n# Future Ideas\n\n* [in progress] a Docker plugin that runs code in Docker over stdio\n    * exists, but is not ready to be released yet\n* integrate sshchat as a broker or an maybe an ssh server for admin stuff\n* build in a simple arg parser something like evt.Getopts()\n  along the lines of evt.BodyAsArgv()\n\n# Community\n\nThe hangops slack seems like as good a place as any to start out.\nBot presence coming soon.\n\nhttps://hangops.slack.com/messages/hal-9001/\n\n# Author\n\nAl Tobey <atobey@netflix.com>\n\n# License\n\nApache 2\n"
  },
  {
    "path": "brokers/console/broker.go",
    "content": "package console\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/chzyer/readline\"\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\ntype Config struct{}\n\ntype Broker struct {\n\tUser   string\n\tRoom   string\n\tTopic  string\n\tStdin  chan string\n\tStdout chan string\n}\n\ntype SlashReaction string\n\n// REPL starts a readline-like bot REPL on the console.\n// All plugins that are present and registered are automatically enabled.\n// name should be a non-empty string. It is reported as the room name/id and\n// will be the string in the REPL prompt, e.g. \"foo\" -> \"foo> \".\n// If prefix is set, it is prepended to every line so you can do, e.g.\n// REPL(\"foo\", \"!foo\") and every line in the REPL will show up in the hal\n// Evt.Body as \"!foo <whatever>\". To avoid this, set it to empty string.\n// This will start 2 goroutines.\nfunc REPL(name, prefix string) {\n\tconf := Config{}\n\tbroker := conf.NewBroker(name)\n\trouter := hal.Router()\n\trouter.AddBroker(broker)\n\tgo router.Route()\n\n\t// automatically wire up all loaded & registered plugins\n\tpr := hal.PluginRegistry()\n\tfor _, p := range pr.PluginList() {\n\t\ti := p.Instance(broker.Room, broker)\n\t\ti.Register()\n\t}\n\n\tlines := make(chan string, 100)\n\tquit := make(chan struct{}, 1)\n\n\t// a simple forwarder - lines from the REPL are forwarded to the router\n\t// lines from the router are printed to stdout\n\t// when the user quits, the goroutine is shut down gracefully\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-quit:\n\t\t\t\tclose(quit)\n\t\t\t\tclose(lines)\n\t\t\t\treturn\n\t\t\tcase line := <-broker.Stdout:\n\t\t\t\tprintln(line)\n\t\t\tcase line := <-lines:\n\t\t\t\tbroker.Stdin <- line\n\t\t\t}\n\t\t}\n\t}()\n\n\trl, err := readline.New(name + \"> \")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer rl.Close()\n\n\t// block forever reading lines from stdin\n\tfor {\n\t\tline, err := rl.Readline()\n\t\tif err == io.EOF {\n\t\t\ttime.Sleep(time.Millisecond * 100)\n\t\t\tcontinue\n\t\t} else if err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\t// quit or exit immediately exit the REPL\n\t\ttrimmed := strings.Trim(line, \"\\r\\n\t\t\")\n\t\tif trimmed == \"quit\" || trimmed == \"exit\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// no input, user likely hit enter on an empty line\n\t\tif trimmed == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// e.g. prefix=\"!prefs\" translates \"list\" to \"!prefs list\"\n\t\t// so the plugin system automatically takes care of things\n\t\t// without hacks in the core code\n\t\tif prefix != \"\" {\n\t\t\tlines <- prefix + \" \" + strings.Trim(line, \" \")\n\t\t} else {\n\t\t\tlines <- line\n\t\t}\n\t}\n\n\tquit <- struct{}{}\n}\n\n// NewBroker returns a new console.Broker.\nfunc (c Config) NewBroker(name string) Broker {\n\tuser := os.Getenv(\"USER\")\n\tif user == \"\" {\n\t\tuser = \"testuser\"\n\t}\n\n\tout := Broker{\n\t\tUser:   user,\n\t\tRoom:   name,\n\t\tStdin:  make(chan string, 1000),\n\t\tStdout: make(chan string, 1000),\n\t}\n\n\treturn out\n}\n\nfunc (cb Broker) Name() string {\n\treturn cb.Room\n}\n\nfunc (cb Broker) Send(e hal.Evt) {\n\tcb.Stdout <- e.Body\n}\n\nfunc (cb Broker) SendDM(e hal.Evt) {\n\tcb.Stdout <- e.Body\n}\n\nfunc (cb Broker) Leave(roomId string) error {\n\tlog.Println(\"Leave(roomId string) not implemented.\")\n\treturn nil\n}\n\nfunc (cb Broker) GetTopic(roomId string) (string, error) {\n\treturn cb.Topic, nil\n}\n\nfunc (cb Broker) SetTopic(roomId, topic string) error {\n\tcb.Topic = topic\n\tcb.Stdout <- fmt.Sprintf(\"topic set to: %q\", topic)\n\treturn nil\n}\n\nfunc (cb Broker) SendTable(e hal.Evt, hdr []string, rows [][]string) {\n\tcb.Stdout <- hal.Utf8Table(hdr, rows)\n}\n\nfunc (cb Broker) LooksLikeRoomId(room string) bool {\n\treturn true\n}\n\nfunc (cb Broker) LooksLikeUserId(user string) bool {\n\treturn true\n}\n\n// SimpleStdin will loop forever reading stdin and publish each line\n// as an event in the console broker.\n// e.g. go cbroker.SimpleStdin()\nfunc (cb Broker) SimpleStdin() {\n\tscanner := bufio.NewScanner(os.Stdin)\n\tfor scanner.Scan() {\n\t\tline := scanner.Text()\n\n\t\tif err := scanner.Err(); err != nil {\n\t\t\tlog.Fatalf(\"Failed while reading from stdin: %s\\n\", err)\n\t\t}\n\n\t\t// ignore empty lines\n\t\tif len(line) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tcb.Stdin <- line\n\t}\n}\n\n// SimpleStdout prints all replies, etc to the broker on os.Stdout.\n// e.g. go cbroker.SimpleStdout()\nfunc (cb Broker) SimpleStdout() {\n\tfor {\n\t\tselect {\n\t\tcase txt := <-cb.Stdout:\n\t\t\t// events from the Reply() method go through a go channel\n\t\t\t_, err := os.Stdout.WriteString(txt)\n\t\t\tif err != nil {\n\t\t\t\tlog.Fatalf(\"Could not write to stdout: %s\\n\", err)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (cb Broker) Stream(out chan *hal.Evt) {\n\tfor {\n\t\tinput := <-cb.Stdin\n\t\tnow := time.Now()\n\n\t\te := hal.Evt{\n\t\t\tID:       fmt.Sprintf(\"%d.%06d\", now.Unix(), now.UnixNano()),\n\t\t\tUser:     cb.User,\n\t\t\tUserId:   cb.User,\n\t\t\tRoom:     cb.Room,\n\t\t\tRoomId:   cb.Room,\n\t\t\tBody:     input,\n\t\t\tTime:     now,\n\t\t\tBroker:   cb,\n\t\t\tIsChat:   true,\n\t\t\tOriginal: &input,\n\t\t}\n\n\t\tif strings.HasPrefix(e.Body, \"/\") {\n\t\t\targs := e.BodyAsArgv()\n\n\t\t\t// detect slash commands for creating specialized event types\n\t\t\tswitch args[0] {\n\t\t\tcase \"/reaction\":\n\t\t\t\tif len(args) == 2 {\n\t\t\t\t\te.Body = args[1]\n\t\t\t\t\t// re-cast the reaction as a type that can be introspected by plugins\n\t\t\t\t\torig := SlashReaction(args[1])\n\t\t\t\t\te.Original = &orig\n\t\t\t\t} else {\n\t\t\t\t\te.IsChat = true\n\t\t\t\t\te.Reply(\"/reaction requires exactly one argument!\")\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\t// everything else is just a plain chat event\n\t\t\tout <- &e\n\t\t}\n\t}\n}\n\n// required by interface\nfunc (b Broker) RoomIdToName(in string) string { return in }\nfunc (b Broker) RoomNameToId(in string) string { return in }\nfunc (b Broker) UserIdToName(in string) string { return in }\nfunc (b Broker) UserNameToId(in string) string { return in }\n"
  },
  {
    "path": "brokers/hipchat/broker.go",
    "content": "package hipchat\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/mattn/go-xmpp\"\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\n// Broker contains the Hipchat API handles required for interacting\n// with the hipchat service.\ntype Broker struct {\n\tClient *xmpp.Client\n\tConfig Config\n\tinst   string\n}\n\ntype Config struct {\n\tHost     string\n\tJid      string\n\tPassword string\n\tRooms    map[string]string\n}\n\n// HIPCHAT_HOST is the only supported hipchat host.\nconst HIPCHAT_HOST = `chat.hipchat.com:5223`\n\n// Hipchat is a singleton that returns an initialized and connected\n// Broker. It can be called anywhere in the bot at any time.\n// Host must be \"chat.hipchat.com:5223\". This requirement can go away\n// once someone takes the time to integrate and test against an on-prem\n// Hipchat server.\nfunc (c Config) NewBroker(name string) Broker {\n\t// TODO: remove this once the TLS/SSL requirements are sorted\n\tif c.Host != HIPCHAT_HOST {\n\t\tlog.Println(\"TODO: Only SSL and hosted Hipchat are supported at the moment.\")\n\t\tlog.Printf(\"Hipchat host must be %q.\", HIPCHAT_HOST)\n\t}\n\n\t// for some reason Go's STARTTLS seems to be incompatible with\n\t// Hipchat's or maybe Hipchat TLS is broken, so don't bother and use SSL.\n\toptions := xmpp.Options{\n\t\tHost:          c.Host,\n\t\tUser:          c.Jid,\n\t\tDebug:         false,\n\t\tPassword:      c.Password,\n\t\tResource:      \"bot\",\n\t\tSession:       true,\n\t\tStatus:        \"Available\",\n\t\tStatusMessage: \"Hal-9001 online.\",\n\t}\n\n\tclient, err := options.NewClient()\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not connect to Hipchat over XMPP: %s\\n\", err)\n\t}\n\n\tfor jid, name := range c.Rooms {\n\t\t_, err = client.JoinMUCNoHistory(jid, name)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not join room %q/%q: %s\", name, jid, err)\n\t\t}\n\t}\n\n\thb := Broker{\n\t\tClient: client,\n\t\tConfig: c,\n\t\tinst:   name,\n\t}\n\n\treturn hb\n}\n\nfunc (hb Broker) Name() string {\n\treturn hb.inst\n}\n\nfunc (hb Broker) Send(evt hal.Evt) {\n\tremote := fmt.Sprintf(\"%s/%s\", evt.RoomId, hb.RoomIdToName(evt.RoomId))\n\n\tmsg := xmpp.Chat{\n\t\tText:   evt.Body,\n\t\tStamp:  evt.Time,\n\t\tType:   \"groupchat\",\n\t\tRemote: remote,\n\t}\n\n\t_, err := hb.Client.Send(msg)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to send message to Hipchat server: %s\\n\", err)\n\t}\n}\n\n// TODO: implement this - if Atlassian ever re-publishes the API docs.\nfunc (hb Broker) SendDM(e hal.Evt) {\n\tpanic(\"SendDM not implemented in Hipchat yet.\")\n}\n\n// TODO: this is untested and may not be entirely correct\nfunc (hb Broker) Leave(roomId string) error {\n\tfor jid, name := range c.Rooms {\n\t\tif roomId == name {\n\t\t\t_, err := hb.Client.LeaveMUC(jid)\n\t\t\treturn err\n\t\t}\n\t}\n\treturn fmt.Errorf(\"Unable to determine JID of room %q.\", roomId)\n}\n\n// TODO: implement\nfunc (hb Broker) GetTopic(roomId string) (string, error) {\n\tpanic(\"SetTopic not implemented in Hipchat yet. Pull requests welcome.\")\n}\n\n// TODO: implement\nfunc (hb Broker) SetTopic(roomId, topic string) error {\n\tpanic(\"SetTopic not implemented in Hipchat yet. Pull requests welcome.\")\n}\n\nfunc (hb Broker) SendTable(evt hal.Evt, hdr []string, rows [][]string) {\n\tout := evt.Clone()\n\t// TODO: verify if this works for bots - works fine in the client\n\t// will probably need to post with the API\n\tout.Body = fmt.Sprintf(\"/code %s\", hal.Utf8Table(hdr, rows))\n\thb.Send(out)\n}\n\nfunc (hb Broker) LooksLikeRoomId(room string) bool {\n\tlog.Println(\"brokers/hipchat/LooksLikeRoomId() is a stub that always returns true!\")\n\treturn true\n}\n\nfunc (hb Broker) LooksLikeUserId(user string) bool {\n\tlog.Println(\"brokers/hipchat/LooksLikeUserId() is a stub that always returns true!\")\n\treturn true\n}\n\n// Subscribe joins a room with the given alias.\n// These names are specific to how Hipchat does things.\nfunc (hb *Broker) Subscribe(room, alias string) {\n\t// TODO: take a room name and somehow look up the goofy MUC name\n\t// e.g. client.JoinMUC(\"99999_roomName@conf.hipchat.com\", \"Bot Name\")\n\thb.Client.JoinMUCNoHistory(room, alias)\n\thb.Config.Rooms[room] = alias\n}\n\n// Keepalive is a timer loop that can be fired up to periodically\n// send keepalive messages to the Hipchat server in order to prevent\n// Hipchat from shutting the connection down due to inactivity.\nfunc (hb *Broker) heartbeat(t time.Time) {\n\t// this seems to work but returns an error you'll see in the logs\n\tmsg := xmpp.Chat{\n\t\tText:  \"heartbeat\",\n\t\tStamp: t,\n\t}\n\tmsg.Stamp = t\n\n\tn, err := hb.Client.Send(msg)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to send keepalive (%d): %s\\n\", n, err)\n\t}\n}\n\n// Stream is an event loop for Hipchat events.\nfunc (hb Broker) Stream(out chan *hal.Evt) {\n\tclient := hb.Client\n\tincoming := make(chan *xmpp.Chat)\n\ttimer := time.Tick(time.Minute * 1) // once a minute\n\n\t// grab chat messages using the blocking Recv() and forward them\n\t// on a channel so the select loop can also handle sending heartbeats\n\tgo func() {\n\t\tfor {\n\t\t\tmsg, err := client.Recv()\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error receiving from Hipchat: %s\\n\", err)\n\t\t\t}\n\n\t\t\tswitch t := msg.(type) {\n\t\t\tcase xmpp.Chat:\n\t\t\t\tm := msg.(xmpp.Chat)\n\t\t\t\tincoming <- &m\n\t\t\tcase xmpp.Presence:\n\t\t\t\tcontinue // ignored\n\t\t\tdefault:\n\t\t\t\tlog.Printf(\"Unhandled message of type '%T': %s \", t, t)\n\t\t\t}\n\t\t}\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase t := <-timer:\n\t\t\thb.heartbeat(t)\n\t\tcase chat := <-incoming:\n\t\t\t// Remote should look like \"99999_roomName@conf.hipchat.com/User Name\"\n\t\t\tparts := strings.SplitN(chat.Remote, \"/\", 2)\n\t\t\tnow := time.Now()\n\n\t\t\tif len(parts) == 2 {\n\t\t\t\t// XMPP doesn't have IDs, use time like Slack\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       fmt.Sprintf(\"%d.%06d\", now.Unix(), now.UnixNano()),\n\t\t\t\t\tBody:     chat.Text,\n\t\t\t\t\tRoom:     hb.RoomIdToName(parts[0]),\n\t\t\t\t\tRoomId:   parts[0],\n\t\t\t\t\tUser:     parts[1],\n\t\t\t\t\tUserId:   chat.Remote,\n\t\t\t\t\tTime:     now, // m.Stamp seems to be zeroed\n\t\t\t\t\tBroker:   hb,\n\t\t\t\t\tIsChat:   true,\n\t\t\t\t\tOriginal: &chat,\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\t\t\t} else {\n\t\t\t\tlog.Printf(\"hipchat broker received an unsupported message: %+v\", chat)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// only considers rooms that have been configured in the bot\n// and does not hit the Hipchat APIs at all\n// TODO: hit the API and get the room/name lists and cache them\nfunc (b Broker) RoomIdToName(in string) string {\n\tif name, exists := b.Config.Rooms[in]; exists {\n\t\treturn name\n\t}\n\n\treturn \"\"\n}\n\nfunc (b Broker) RoomNameToId(in string) string {\n\tfor id, name := range b.Config.Rooms {\n\t\tif name == in {\n\t\t\treturn id\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc (b Broker) UserIdToName(in string) string { return in }\nfunc (b Broker) UserNameToId(in string) string { return in }\n"
  },
  {
    "path": "brokers/slack/broker.go",
    "content": "package slack\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"image/png\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n\t\"github.com/nlopes/slack\"\n)\n\nvar log hal.Logger\n\n// Broker interacts with the slack service.\n// TODO: add a miss cache to avoid hammering the room/user info apis\ntype Broker struct {\n\tClient  *slack.Client     // slack API object\n\tRTM     *slack.RTM        // slack RTM object\n\tUserId  string            // slack Bot user ID (for preventing loops)\n\tinst    string            // broker instance name\n\ti2u     map[string]string // id->name cache\n\ti2c     map[string]string // id->name cache\n\tu2i     map[string]string // name->id cache\n\tc2i     map[string]string // name->id cache\n\timcs    map[string]string // userId -> channelId im channels\n\tlufill  time.Time         // timestamp of the last user cache fill\n\tlrfill  time.Time         // timestamp of the last room cache fill\n\tidRegex *regexp.Regexp    // compiled RE to match user/room ids\n\tmut     sync.Mutex        // protect access to the lookup maps\n}\n\ntype Config struct {\n\tToken string\n}\n\nvar LooksLikeIdRE *regexp.Regexp\n\nfunc init() {\n\tLooksLikeIdRE = regexp.MustCompile(`^[UCD]\\w{8}$`)\n\n\tlog.SetPrefix(\"brokers/slack\")\n}\n\nfunc (c Config) NewBroker(name string) Broker {\n\tclient := slack.New(c.Token)\n\t// TODO: check for failures and log.Fatalf()\n\trtm := client.NewRTM()\n\n\tsb := Broker{\n\t\tClient: client,\n\t\tRTM:    rtm,\n\t\tinst:   name,\n\t\ti2u:    make(map[string]string),\n\t\ti2c:    make(map[string]string),\n\t\tu2i:    make(map[string]string),\n\t\tc2i:    make(map[string]string),\n\t\timcs:   make(map[string]string),\n\t}\n\n\t// fill the caches at startup to cut down on API requests\n\tsb.FillUserCache()\n\tsb.FillRoomCache()\n\n\tgo rtm.ManageConnection()\n\n\treturn sb\n}\n\n// Name returns the name of the broker as set in NewBroker.\nfunc (sb Broker) Name() string {\n\treturn sb.inst\n}\n\nfunc (sb Broker) Send(evt hal.Evt) {\n\t// Slack refuses messages over 4000 characters. Most of the time that's\n\t// probably data so post it as a file. Using len instead of rune count since\n\t// slack is probably looking at bytes.\n\tif len(evt.Body) > 3999 {\n\t\tsb.SendAsSnippet(evt)\n\t} else {\n\t\tsb.SendAsIs(evt)\n\t}\n}\n\nfunc (sb Broker) SendAsSnippet(evt hal.Evt) {\n\tf, err := ioutil.TempFile(os.TempDir(), \"hal\")\n\tif err != nil {\n\t\tevt.Replyf(\"Could not create tempfile for large text upload: %s\", err)\n\t\treturn\n\t}\n\tdefer os.Remove(f.Name())\n\n\tf.WriteString(evt.Body)\n\tf.Close()\n\n\t// upload the file\n\tparams := slack.FileUploadParameters{\n\t\tFile:     f.Name(),\n\t\tFilename: \"reply.txt\",\n\t\tChannels: []string{evt.RoomId},\n\t}\n\t_, err = sb.Client.UploadFile(params)\n\tif err != nil {\n\t\tevt.Replyf(\"Could not upload snippet file: %s\", err)\n\t}\n}\n\n// SendAsIs directly sends a message without considering it for posting as a snippet.\nfunc (sb Broker) SendAsIs(evt hal.Evt) {\n\t// if evt.Original is a slack.PostMessageParameters, assume that means that there is\n\t// a rich message in the body with params that need to be posted to the web API\n\t// rather than going through RTM.\n\t// See: https://api.slack.com/bot-users\n\tswitch evt.Original.(type) {\n\tcase *slack.PostMessageParameters:\n\t\tparams := evt.Original.(*slack.PostMessageParameters)\n\t\tparams.AsUser = true // if we've gotten here, we always want this\n\t\tsb.Client.PostMessage(evt.RoomId, evt.Body, *params)\n\tdefault:\n\t\tom := sb.RTM.NewOutgoingMessage(evt.Body, evt.RoomId)\n\t\tsb.RTM.SendMessage(om)\n\t}\n}\n\nfunc (sb Broker) SendDM(evt hal.Evt) {\n\tevt.Room = \"\"\n\tevt.RoomId = \"\"\n\n\tif roomId, exists := sb.imcs[evt.UserId]; exists {\n\t\t// cache hit\n\t\t// TODO: verify what happens if the destination user has closed the DM\n\t\tevt.RoomId = roomId\n\t} else {\n\t\t// try to open the channel, cache it if it works\n\t\t_, _, roomId, err := sb.RTM.OpenIMChannel(evt.UserId)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error from RTM.OpenIMChannel(%q): %s\", evt.UserId, err)\n\t\t} else {\n\t\t\tsb.imcs[evt.UserId] = roomId\n\t\t\tsb.i2c[roomId] = evt.UserId // TODO: verify this isn't a stupid idea\n\t\t\tevt.RoomId = roomId\n\t\t}\n\t}\n\n\tif evt.RoomId != \"\" {\n\t\tsb.Send(evt)\n\t} else {\n\t\tlog.Printf(\"SendDM() failed because it couldn't identify a DM RoomID!\")\n\t\tlog.Printf(\"Failed message: %q\", evt.String())\n\t}\n}\n\nfunc (sb Broker) Leave(roomId string) error {\n\t_, err := sb.Client.LeaveChannel(roomId)\n\treturn err\n}\n\nfunc (sb Broker) GetTopic(roomId string) (string, error) {\n\tch, err := sb.Client.GetChannelInfo(roomId)\n\treturn ch.Topic.Value, err\n}\n\nfunc (sb Broker) SetTopic(roomId, topic string) error {\n\tr, err := sb.Client.SetChannelTopic(roomId, topic)\n\tlog.Debugf(\"SetTopic(%q, %q) = %q\", roomId, topic, r)\n\treturn err\n}\n\nfunc (sb Broker) SendTable(evt hal.Evt, hdr []string, rows [][]string) {\n\tout := evt.Clone()\n\tout.Body = hal.Utf8Table(hdr, rows)\n\n\ttblFmt := hal.FindPrefs(\"\", \"\", \"\", \"\", \"table.format\").One()\n\n\tif tblFmt.Value == \"image\" {\n\t\tsb.SendAsImage(out)\n\t} else if tblFmt.Value == \"snippet\" {\n\t\tsb.SendAsSnippet(out)\n\t} else {\n\t\tsb.SendAsIs(out)\n\t}\n}\n\n// SendAsImage sends the body of the event as a png file. The png is rendered\n// using hal's FixedFont facility.\n// This is useful for making sure pre-formatted text stays legible in\n// Slack while we wait for them to figure out a way to render things like\n// tables of data consistently.\nfunc (sb Broker) SendAsImage(evt hal.Evt) {\n\tfd := hal.FixedFont()\n\n\t// create a tempfile\n\tf, err := ioutil.TempFile(os.TempDir(), \"hal\")\n\tif err != nil {\n\t\tevt.Replyf(\"Could not create tempfile for image upload: %s\", err)\n\t\treturn\n\t}\n\tdefer os.Remove(f.Name())\n\n\t// check for a color preference\n\t// need to figure out a way to have a helper around this\n\tvar fg color.Color\n\tfg = color.Black\n\t// TODO: prefs --set isn't setting the room, etc. remove the filter for now\n\tfgprefs := hal.FindPrefs(\"\", \"\", \"\", \"\", \"image.fg\")\n\tufgprefs := fgprefs.User(evt.UserId)\n\tif len(ufgprefs) > 0 {\n\t\tfg = fd.ParseColor(ufgprefs[0].Value, fg)\n\t} else if len(fgprefs) > 0 {\n\t\tfg = fd.ParseColor(fgprefs[0].Value, fg)\n\t}\n\n\tvar bg color.Color\n\tbg = color.Transparent\n\t// TODO: ditto from ft\n\t//bgprefs := hal.FindPrefs(\"\", sb.Name(), evt.RoomId, \"\", \"image.bg\")\n\tbgprefs := hal.FindPrefs(\"\", \"\", \"\", \"\", \"image.bg\")\n\tubgprefs := bgprefs.User(evt.UserId)\n\tif len(ubgprefs) > 0 {\n\t\tbg = fd.ParseColor(ubgprefs[0].Value, fg)\n\t} else if len(bgprefs) > 0 {\n\t\tbg = fd.ParseColor(bgprefs[0].Value, fg)\n\t}\n\n\t// generate the image\n\tlines := strings.Split(strings.TrimSpace(evt.Body), \"\\n\")\n\ttextimg := fd.StringsToImage(lines, fg)\n\n\t// img has a background color, copy textimg onto it\n\timg := image.NewRGBA(textimg.Bounds())\n\tdraw.Draw(img, img.Bounds(), &image.Uniform{bg}, image.ZP, draw.Src)\n\tdraw.Draw(img, img.Bounds(), textimg, image.ZP, draw.Src)\n\n\t// TODO: apply background color\n\n\t// write the png data to the temp file\n\tpng.Encode(f, img)\n\tf.Close()\n\n\t// upload the file\n\tparams := slack.FileUploadParameters{\n\t\tFile:     f.Name(),\n\t\tFilename: \"text.png\",\n\t\tChannels: []string{evt.RoomId},\n\t}\n\t_, err = sb.Client.UploadFile(params)\n\tif err != nil {\n\t\tevt.Replyf(\"Could not upload image: %s\", err)\n\t}\n}\n\nfunc (sb Broker) LooksLikeRoomId(room string) bool {\n\tsb.mut.Lock()\n\tdefer sb.mut.Unlock()\n\n\tif _, exists := sb.i2c[room]; exists {\n\t\treturn true\n\t}\n\n\treturn LooksLikeIdRE.MatchString(room)\n}\n\nfunc (sb Broker) LooksLikeUserId(user string) bool {\n\tsb.mut.Lock()\n\tdefer sb.mut.Unlock()\n\n\tif _, exists := sb.i2u[user]; exists {\n\t\treturn true\n\t}\n\n\treturn LooksLikeIdRE.MatchString(user)\n}\n\n// checks the cache to see if the room is known to this broker\nfunc (sb Broker) HasRoom(room string) bool {\n\tsb.mut.Lock()\n\tdefer sb.mut.Unlock()\n\n\tif LooksLikeIdRE.MatchString(room) {\n\t\t_, exists := sb.i2c[room]\n\t\treturn exists\n\t} else {\n\t\t_, exists := sb.c2i[room]\n\t\treturn exists\n\t}\n}\n\n// Stream is an event loop for Slack events & messages from the RTM API.\n// Events are copied to a hal.Evt and forwarded to the exchange where they\n// can be processed by registered handlers.\nfunc (sb Broker) Stream(out chan *hal.Evt) {\n\tfor {\n\t\tselect {\n\t\tcase msg := <-sb.RTM.IncomingEvents:\n\t\t\tswitch ev := msg.Data.(type) {\n\t\t\tcase *slack.UserTypingEvent:\n\t\t\t\t// frequent and mostly useless in a bot: ignore\n\n\t\t\tcase *slack.HelloEvent:\n\t\t\t\tlog.Debugf(\"HelloEvent\")\n\n\t\t\tcase *slack.ConnectedEvent:\n\t\t\t\tinfo := sb.RTM.GetInfo()\n\t\t\t\tsb.UserId = info.User.ID\n\n\t\t\t\tlog.Debugf(\"ConnectedEvent - retreived bot ID %q\", sb.UserId)\n\n\t\t\tcase *slack.MessageEvent:\n\t\t\t\t// https://api.slack.com/events/message\n\t\t\t\tm := msg.Data.(*slack.MessageEvent)\n\n\t\t\t\t// mark messages generated by the bot user to prevent loops, etc.\n\t\t\t\t// but pass them through so stuff like the archive module can get them\n\t\t\t\tisBot := m.User == sb.UserId\n\n\t\t\t\t// A few other kinds of events are bundled as messages with a subtype.\n\t\t\t\t// Only allow isChat to remain true if it's an actual chat message.\n\t\t\t\tisChat := m.SubType == \"\"\n\n\t\t\t\t// slack channels = hal rooms, see hal-9001/hal/event.go\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       m.Timestamp,\n\t\t\t\t\tBody:     m.Text,\n\t\t\t\t\tRoom:     sb.RoomIdToName(m.Channel),\n\t\t\t\t\tRoomId:   m.Channel,\n\t\t\t\t\tUser:     sb.UserIdToName(m.User),\n\t\t\t\t\tUserId:   m.User,\n\t\t\t\t\tBroker:   sb,\n\t\t\t\t\tTime:     SlackTime(m.Timestamp),\n\t\t\t\t\tIsChat:   isChat,\n\t\t\t\t\tIsBot:    isBot,\n\t\t\t\t\tOriginal: m,\n\t\t\t\t}\n\n\t\t\t\t// let everyone know the bot is working if it appears to be a command\n\t\t\t\tif !isBot && strings.HasPrefix(strings.TrimSpace(m.Text), \"!\") {\n\t\t\t\t\ttm := sb.RTM.NewTypingMessage(m.Channel)\n\t\t\t\t\tsb.RTM.SendMessage(tm)\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\n\t\t\tcase *slack.StarAddedEvent:\n\t\t\t\tsae := msg.Data.(*slack.StarAddedEvent)\n\n\t\t\t\tif sae.User == sb.UserId {\n\t\t\t\t\tlog.Debugf(\"ignoring event from bot with id %s\", sb.UserId)\n\t\t\t\t\tcontinue // ignore bot-created events\n\t\t\t\t}\n\n\t\t\t\tuser := sb.UserIdToName(sae.User)\n\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       sae.EventTimestamp,\n\t\t\t\t\tBody:     fmt.Sprintf(\"%q added a star\", user),\n\t\t\t\t\tRoom:     sb.RoomIdToName(sae.Item.Channel),\n\t\t\t\t\tRoomId:   sae.Item.Channel,\n\t\t\t\t\tUser:     user,\n\t\t\t\t\tUserId:   sae.User,\n\t\t\t\t\tBroker:   sb,\n\t\t\t\t\tTime:     SlackTime(sae.EventTimestamp),\n\t\t\t\t\tOriginal: sae,\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\n\t\t\tcase *slack.StarRemovedEvent:\n\t\t\t\tsre := msg.Data.(*slack.StarRemovedEvent)\n\n\t\t\t\tif sre.User == sb.UserId {\n\t\t\t\t\tlog.Debugf(\"ignoring event from bot with id %s\", sb.UserId)\n\t\t\t\t\tcontinue // ignore bot-created events\n\t\t\t\t}\n\n\t\t\t\tuser := sb.UserIdToName(sre.User)\n\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       sre.EventTimestamp,\n\t\t\t\t\tBody:     fmt.Sprintf(\"%q removed a star\", user),\n\t\t\t\t\tRoom:     sb.RoomIdToName(sre.Item.Channel),\n\t\t\t\t\tRoomId:   sre.Item.Channel,\n\t\t\t\t\tUser:     user,\n\t\t\t\t\tUserId:   sre.User,\n\t\t\t\t\tBroker:   sb,\n\t\t\t\t\tTime:     SlackTime(sre.EventTimestamp),\n\t\t\t\t\tOriginal: sre,\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\n\t\t\tcase *slack.ReactionAddedEvent:\n\t\t\t\trae := msg.Data.(*slack.ReactionAddedEvent)\n\n\t\t\t\tif rae.User == sb.UserId {\n\t\t\t\t\tlog.Debugf(\"ignoring event from bot with id %s\", sb.UserId)\n\t\t\t\t\tcontinue // ignore bot-created events\n\t\t\t\t}\n\n\t\t\t\tuser := sb.UserIdToName(rae.User)\n\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       rae.EventTimestamp,\n\t\t\t\t\tBody:     fmt.Sprintf(\"%q added reaction %q\", user, rae.Reaction),\n\t\t\t\t\tRoom:     sb.RoomIdToName(rae.Item.Channel),\n\t\t\t\t\tRoomId:   rae.Item.Channel,\n\t\t\t\t\tUser:     user,\n\t\t\t\t\tUserId:   rae.User,\n\t\t\t\t\tBroker:   sb,\n\t\t\t\t\tTime:     SlackTime(rae.EventTimestamp),\n\t\t\t\t\tOriginal: rae,\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\n\t\t\tcase *slack.ReactionRemovedEvent:\n\t\t\t\trre := msg.Data.(*slack.ReactionRemovedEvent)\n\n\t\t\t\tif rre.User == sb.UserId {\n\t\t\t\t\tlog.Debugf(\"ignoring event from bot with id %s\", sb.UserId)\n\t\t\t\t\tcontinue // ignore bot-created events\n\t\t\t\t}\n\n\t\t\t\tuser := sb.UserIdToName(rre.User)\n\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       rre.EventTimestamp,\n\t\t\t\t\tBody:     fmt.Sprintf(\"%q removed reaction %q\", user, rre.Reaction),\n\t\t\t\t\tRoom:     sb.RoomIdToName(rre.Item.Channel),\n\t\t\t\t\tRoomId:   rre.Item.Channel,\n\t\t\t\t\tUser:     user,\n\t\t\t\t\tUserId:   rre.User,\n\t\t\t\t\tBroker:   sb,\n\t\t\t\t\tTime:     SlackTime(rre.EventTimestamp),\n\t\t\t\t\tOriginal: rre,\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\n\t\t\tcase *slack.ChannelJoinedEvent:\n\t\t\t\tje := msg.Data.(*slack.ChannelJoinedEvent)\n\t\t\t\tnow := time.Now()\n\n\t\t\t\tsb.injectRoomId(je.Channel.ID, je.Channel.Name) // cache the id:name\n\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       now.String(), // fake an id\n\t\t\t\t\tBody:     je.Channel.Name,\n\t\t\t\t\tRoom:     je.Channel.Name,\n\t\t\t\t\tRoomId:   je.Channel.ID,\n\t\t\t\t\tUser:     sb.UserId,\n\t\t\t\t\tUserId:   sb.UserId,\n\t\t\t\t\tBroker:   sb,\n\t\t\t\t\tTime:     now,\n\t\t\t\t\tOriginal: je,\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\n\t\t\tcase *slack.GroupJoinedEvent:\n\t\t\t\t// exactly the same as ChannelJoinedEvent ^^ in a separate type\n\t\t\t\tje := msg.Data.(*slack.GroupJoinedEvent)\n\t\t\t\tnow := time.Now()\n\n\t\t\t\tsb.injectRoomId(je.Channel.ID, je.Channel.Name) // cache the id:name\n\n\t\t\t\te := hal.Evt{\n\t\t\t\t\tID:       now.String(), // fake an id\n\t\t\t\t\tBody:     je.Channel.Name,\n\t\t\t\t\tRoom:     je.Channel.Name,\n\t\t\t\t\tRoomId:   je.Channel.ID,\n\t\t\t\t\tUser:     sb.UserId,\n\t\t\t\t\tUserId:   sb.UserId,\n\t\t\t\t\tBroker:   sb,\n\t\t\t\t\tTime:     now,\n\t\t\t\t\tOriginal: je,\n\t\t\t\t}\n\n\t\t\t\tout <- &e\n\n\t\t\tcase *slack.PresenceChangeEvent:\n\t\t\t\t// ignored\n\n\t\t\tcase *slack.LatencyReport:\n\t\t\t\t// ignored\n\n\t\t\tcase *slack.FileCreatedEvent, *slack.FilePublicEvent, *slack.FileSharedEvent:\n\t\t\t\t// ignored\n\n\t\t\tcase *slack.PrefChangeEvent:\n\t\t\t\t// ignored\n\n\t\t\tcase *slack.RTMError:\n\t\t\t\tlog.Printf(\"ignoring RTMError: %s\\n\", ev.Error())\n\n\t\t\tcase *slack.InvalidAuthEvent:\n\t\t\t\tlog.Debugf(\"InvalidAuthEvent\")\n\t\t\t\tbreak\n\n\t\t\tdefault:\n\t\t\t\tlog.Debugf(\"unexpected message: %+v\\n\", msg)\n\t\t\t}\n\t\t}\n\t}\n}\n\n// SlackTime converts the timestamp string to time.Time\nfunc SlackTime(t string) time.Time {\n\tif t == \"\" {\n\t\treturn time.Now()\n\t}\n\n\t// Slack advises not to parse the timestamp as a float.\n\t// I tried it. Turns out that string mangling is more accurate than\n\t// float conversions.\n\tparts := strings.SplitN(t, \".\", 2)\n\n\ts, _ := strconv.ParseInt(parts[0], 10, 64)\n\tns, _ := strconv.ParseInt(parts[1], 10, 64)\n\n\treturn time.Unix(s, ns)\n}\n\nfunc (sb *Broker) FillUserCache() {\n\t// don't let this fire more than once every half hour\n\tnow := time.Now()\n\tif now.Sub(sb.lufill) < time.Minute*30 {\n\t\tlog.Debugf(\"refusing to fill cache because it has been less than 30 minutes since the last fill @ %s\", sb.lufill.String())\n\t\treturn\n\t}\n\tsb.lufill = now\n\n\tusers, err := sb.Client.GetUsers()\n\tif err != nil {\n\t\tlog.Printf(\"failed to fetch user list: %s\", err)\n\t\treturn\n\t}\n\n\t// push the users into the directory async so it doesn't hold up bot\n\t// startup (FillUserCache is called preemptively at startup)\n\tgo func() {\n\t\tfor _, user := range users {\n\t\t\tattrs := map[string]string{\n\t\t\t\t\"username\": user.Name,\n\t\t\t\t\"name\":     user.RealName,\n\t\t\t\t\"email\":    user.Profile.Email,\n\t\t\t}\n\t\t\thal.Directory().Put(user.ID, \"slack-user\", attrs, []string{\"email\"})\n\t\t}\n\t}()\n\n\tsb.mut.Lock()\n\tdefer sb.mut.Unlock()\n\n\tfor _, user := range users {\n\t\tsb.u2i[user.Name] = user.ID\n\t\tsb.i2u[user.ID] = user.Name\n\t}\n}\n\nfunc (sb *Broker) FillRoomCache() {\n\t// don't let this fire more than once every half hour\n\tnow := time.Now()\n\tif now.Sub(sb.lrfill) < time.Minute*30 {\n\t\tlog.Printf(\"refusing to fill cache because it has been less than 30 minutes since the last fill @ %s\", sb.lrfill.String())\n\t\treturn\n\t}\n\tsb.lrfill = now\n\n\trooms, err := sb.Client.GetChannels(true)\n\tif err != nil {\n\t\tlog.Printf(\"failed to fetch room list: %s\", err)\n\t\treturn\n\t}\n\n\t// now get private channels a.k.a. groups\n\tgroups, err := sb.Client.GetGroups(true)\n\tif err != nil {\n\t\tlog.Printf(\"failed to fetch private channel list: %s\", err)\n\t\treturn\n\t}\n\n\tsb.mut.Lock()\n\tdefer sb.mut.Unlock()\n\n\tfor _, room := range rooms {\n\t\tsb.c2i[room.Name] = room.ID\n\t\tsb.i2c[room.ID] = room.Name\n\t}\n\n\tfor _, group := range groups {\n\t\tsb.c2i[group.Name] = group.ID\n\t\tsb.i2c[group.ID] = group.Name\n\t}\n}\n\n// UserIdToName gets the human-readable username for a user ID using an\n// in-memory cache that falls through to the Slack API\nfunc (sb Broker) UserIdToName(id string) string {\n\tif id == \"\" {\n\t\tlog.Debugf(\"UserIdToName(): Cannot look up empty string!\")\n\t\treturn \"\"\n\t}\n\n\tsb.mut.Lock()\n\tname, exists := sb.i2u[id]\n\tsb.mut.Unlock()\n\n\tif exists {\n\t\treturn name\n\t} else {\n\t\tuser, err := sb.Client.GetUserInfo(id)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"could not retrieve user info for '%s' via API: %s\\n\", id, err)\n\t\t\treturn \"\"\n\t\t}\n\n\t\t// don't wait around for this - it can block\n\t\tgo func() {\n\t\t\tattrs := map[string]string{\n\t\t\t\t\"username\": user.Name,\n\t\t\t\t\"name\":     user.RealName,\n\t\t\t\t\"email\":    user.Profile.Email,\n\t\t\t}\n\n\t\t\thal.Directory().Put(user.ID, \"slack-user\", attrs, []string{\"email\"})\n\t\t}()\n\n\t\tsb.mut.Lock()\n\t\tdefer sb.mut.Unlock()\n\n\t\tsb.i2u[user.ID] = user.Name\n\t\tsb.i2u[user.Name] = user.ID\n\n\t\treturn user.Name\n\t}\n}\n\n// RoomIdToName gets the human-readable room name for a user ID using an\n// in-memory cache that falls through to the Slack API\nfunc (sb Broker) RoomIdToName(id string) string {\n\tsb.mut.Lock()\n\tdefer sb.mut.Unlock()\n\n\tif id == \"\" {\n\t\tlog.Debugf(\"RoomIdToName(): Cannot look up empty string!\")\n\t\treturn \"\"\n\t}\n\n\tif name, exists := sb.i2c[id]; exists {\n\t\treturn name\n\t} else {\n\t\tvar name string\n\n\t\t// private channels are on a different endpoint\n\t\tif strings.HasPrefix(id, \"G\") {\n\t\t\tgrp, err := sb.Client.GetGroupInfo(id)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"could not retrieve room info for '%s' via API: %s\\n\", id, err)\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\tname = grp.Name\n\t\t} else if strings.HasPrefix(id, \"D\") {\n\t\t\tlog.Println(\"DM CHANNELS ARE A WORK IN PROGRESS\")\n\t\t\t//log.Printf(\"could not retrieve room info for '%s' via API: %s\\n\", id, err)\n\t\t} else {\n\t\t\troom, err := sb.Client.GetChannelInfo(id)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"could not retrieve room info for '%s' via API: %s\\n\", id, err)\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\tname = room.Name\n\t\t}\n\n\t\tsb.i2c[id] = name\n\t\tsb.c2i[name] = id\n\n\t\treturn name\n\t}\n}\n\n// UserNameToId gets the human-readable username for a user ID using an\n// in-memory cache that falls through to the Slack API\nfunc (sb Broker) UserNameToId(name string) string {\n\tif name == \"\" {\n\t\tlog.Debugf(\"UserNameToId(): Cannot look up empty string!\")\n\t\treturn \"\"\n\t}\n\n\tsb.mut.Lock()\n\tid, exists := sb.u2i[name]\n\tsb.mut.Unlock()\n\n\tif exists {\n\t\treturn id\n\t} else {\n\t\t// there doesn't seem to be a name->id lookup so refresh the cache\n\t\t// and try again if we get here\n\t\tsb.FillUserCache()\n\n\t\tsb.mut.Lock()\n\t\tdefer sb.mut.Unlock()\n\n\t\tif id, exists := sb.u2i[name]; exists {\n\t\t\treturn id\n\t\t}\n\n\t\tlog.Printf(\"service does not seem to have knowledge of username %q\", name)\n\t\treturn \"\"\n\t}\n}\n\n// RoomNameToId gets the id for a room name using an\n// in-memory cache that falls through to the Slack API\nfunc (sb Broker) RoomNameToId(name string) string {\n\tif name == \"\" {\n\t\tlog.Println(\"RoomNameToId(): Cannot look up empty string!\")\n\t\treturn \"\"\n\t}\n\n\tsb.mut.Lock()\n\tid, exists := sb.c2i[name]\n\tsb.mut.Unlock()\n\n\tif exists {\n\t\treturn id\n\t} else {\n\t\tsb.FillRoomCache()\n\n\t\tsb.mut.Lock()\n\t\tdefer sb.mut.Unlock()\n\n\t\tif id, exists = sb.c2i[name]; exists {\n\t\t\treturn id\n\t\t}\n\n\t\tlog.Printf(\"service does not seem to have knowledge of room name %q\", name)\n\t\treturn \"\"\n\t}\n}\n\n// injectRoomId adds an id:name mapping to the forward and reverse lookup maps\n// for internal use only, used to inject groups (private channels) on join\nfunc (sb Broker) injectRoomId(id, name string) {\n\tsb.mut.Lock()\n\tdefer sb.mut.Unlock()\n\n\tsb.c2i[name] = id\n\tsb.i2c[id] = name\n}\n"
  },
  {
    "path": "example/demos/colorparser.go",
    "content": "package main\n\n// go run utf8table.go\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"github.com/netflix/hal-9001/hal\"\n\t\"image/color\"\n)\n\nfunc main() {\n\tsamples := []string{\n\t\t\"ffffff\",\n\t\t\"ffffffff\",\n\t\t\"000000ff\",\n\t\t\"000000aa\",\n\t\t\"888888ff\",\n\t\t\"888888\",\n\t\t\"f79e10\",   // amber\n\t\t\"f79e10ff\", // amber with alpha\n\t}\n\n\tfd := hal.FixedFont()\n\n\tfor _, sample := range samples {\n\t\tresult := fd.ParseColor(sample, color.Black)\n\t\tfmt.Printf(\"%q => %q\\n\", sample, result)\n\t}\n}\n"
  },
  {
    "path": "example/demos/imgtable.go",
    "content": "package main\n\n// go run utf8table.go\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"github.com/netflix/hal-9001/hal\"\n\t\"image/color\"\n\t\"image/png\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc main() {\n\tsamples := [][][]string{\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t},\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"left\", \"right\"},\n\t\t\t{\"one\", \"three\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"HEADER 1\", \"HDR 2\", \"LOL WUT\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t},\n\t\t{\n\t\t\t{\"Col 1\", \"Col 2\", \"3rd Column\", \"4th\", \"FIFTH\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t\t{\"hi\"},\n\t\t\t{\"\", \"\", \"\", \"-\", \"+\"},\n\t\t},\n\t}\n\n\tfd := hal.FixedFont()\n\n\tfor i, sample := range samples {\n\t\tout := hal.Utf8Table(sample[0], sample[1:])\n\n\t\tlines := strings.Split(strings.TrimSpace(out), \"\\n\")\n\n\t\timg := fd.StringsToImage(lines, color.Black)\n\n\t\tfilename := fmt.Sprintf(\"%d.png\", i)\n\t\tf, err := os.Create(filename)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tdefer f.Close()\n\n\t\tpng.Encode(f, img)\n\n\t\tfmt.Printf(\"Created file: %q\\n\", filename)\n\t}\n}\n"
  },
  {
    "path": "example/demos/utf8table.go",
    "content": "package main\n\n// go run utf8table.go\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nfunc main() {\n\tsamples := [][][]string{\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t},\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"left\", \"right\"},\n\t\t\t{\"one\", \"three\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"HEADER 1\", \"HDR 2\", \"LOL WUT\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t},\n\t\t{\n\t\t\t{\"Col 1\", \"Col 2\", \"3rd Column\", \"4th\", \"FIFTH\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t\t{\"hi\"},\n\t\t\t{\"\", \"\", \"\", \"-\", \"+\"},\n\t\t},\n\t}\n\n\tfor _, sample := range samples {\n\t\t// first row is the header, the rest is data rows\n\t\tout := hal.Utf8Table(sample[0], sample[1:])\n\t\tfmt.Println(out)\n\t}\n}\n"
  },
  {
    "path": "example/docker-repl/main.go",
    "content": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"github.com/chzyer/readline\"\n\n\t\"github.com/netflix/hal-9001/brokers/console\"\n\t\"github.com/netflix/hal-9001/hal\"\n\t\"github.com/netflix/hal-9001/plugins/docker\"\n\t\"github.com/netflix/hal-9001/plugins/pluginmgr\"\n\t\"github.com/netflix/hal-9001/plugins/prefmgr\"\n)\n\n// a simple bot that only implements generic plugins on a repl\n// possibly a basis for a command-line client for Slack, etc....\n\nfunc main() {\n\trl, err := readline.New(\"hal> \")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer rl.Close()\n\n\tbconf := console.Config{}\n\tbroker := bconf.NewBroker(\"cli\")\n\n\tdocker.Register()\n\tpluginmgr.Register()\n\tprefmgr.Register()\n\n\tpr := hal.PluginRegistry()\n\tpmp, _ := pr.GetPlugin(\"pluginmgr\")\n\tpmp.Instance(broker.Room, broker).Register()\n\n\tprmp, _ := pr.GetPlugin(\"prefmgr\")\n\tprmp.Instance(broker.Room, broker).Register()\n\n\tdp, _ := pr.GetPlugin(\"docker\")\n\tdp.Instance(broker.Room, broker).Register()\n\n\trouter := hal.Router()\n\trouter.AddBroker(broker)\n\tgo router.Route()\n\n\tfor {\n\t\tline, err := rl.Readline()\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tbroker.Line(line)\n\t}\n}\n"
  },
  {
    "path": "example/everything/main.go",
    "content": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"io/ioutil\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n\n\t\"github.com/netflix/hal-9001/brokers/hipchat\"\n\t\"github.com/netflix/hal-9001/brokers/slack\"\n\n\t\"github.com/netflix/hal-9001/plugins/archive\"\n\t\"github.com/netflix/hal-9001/plugins/google_calendar\"\n\t\"github.com/netflix/hal-9001/plugins/mark\"\n\t\"github.com/netflix/hal-9001/plugins/pagerduty\"\n\t\"github.com/netflix/hal-9001/plugins/pluginmgr\"\n\t\"github.com/netflix/hal-9001/plugins/prefmgr\"\n\t\"github.com/netflix/hal-9001/plugins/roster\"\n\t\"github.com/netflix/hal-9001/plugins/seppuku\"\n\t\"github.com/netflix/hal-9001/plugins/uptime\"\n)\n\nfunc main() {\n\t// configuration is in environment variables\n\t// if you prefer configuration files or flags, that's cool, just replace\n\t// this part with your thing\n\tdsn := requireEnv(\"HAL_DSN\")\n\tkeyfile := requireEnv(\"HAL_SECRETS_KEY_FILE\")\n\tcontrolRoom := requireEnv(\"HAL_CONTROL_ROOM\")\n\thipchatRoomJid := requireEnv(\"HAL_HIPCHAT_ROOM_JID\")\n\thipchatRoomName := requireEnv(\"HAL_HIPCHAT_ROOM_NAME\")\n\twebAddr := defaultEnv(\"HAL_HTTP_LISTEN_ADDR\", \":9001\")\n\n\t// hal provides a k/v API for managing secrets that the DB code uses to get\n\t// its DSN (which contains a password). Put the DSN there so the DB can find\n\t// it.\n\tsecrets := hal.Secrets()\n\tsecrets.Set(hal.SECRETS_KEY_DSN, dsn)\n\n\t// parts of hal rely on the database (prefs, secrets, etc.)\n\t// so make sure the DSN is valid and hal can connect before\n\t// doing anything else\n\t// hal can't do much without the database, so you probably want this\n\tdb := hal.SqlDB()\n\tif err := db.Ping(); err != nil {\n\t\tlog.Fatalf(\"Could not ping the database: %s\", err)\n\t}\n\n\t// get the secrets encryption key from the file specified\n\t// this should be protected like any other private key\n\t// if you don't use the secrets persistence, this can be removed/ignored\n\tskey, err := ioutil.ReadFile(keyfile)\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not read key file '%s': %s\", keyfile, err)\n\t}\n\n\t// Set the encryption key for persisted secrets.\n\t// Secrets can persist to the database, encrypting the key and value\n\t// with AES-GCM before writing so that database backups, etc only contain\n\t// ciphertext and no cleartext secrets.\n\tsecrets.SetEncryptionKey(skey)\n\n\t// load secrets from the database\n\tsecrets.LoadFromDB()\n\n\t// update the DSN again since the database might have a stale copy\n\tsecrets.Set(hal.SECRETS_KEY_DSN, dsn)\n\n\t// configure the Hipchat broker\n\thconf := hipchat.Config{\n\t\tHost:     hipchat.HIPCHAT_HOST, // TODO: not really configurable yet\n\t\tJid:      secrets.Get(\"hipchat.jid\"),\n\t\tPassword: secrets.Get(\"hipchat.password\"),\n\n\t\t// TODO: make this configurable via prefs (or maybe secrets?)\n\t\tRooms: map[string]string{\n\t\t\thipchatRoomJid: hipchatRoomName,\n\t\t},\n\t}\n\thc := hconf.NewBroker(\"hipchat\")\n\n\t// configure the Slack broker\n\tsconf := slack.Config{\n\t\tToken: secrets.Get(\"slack.token\"),\n\t}\n\tslk := sconf.NewBroker(\"slack\")\n\n\t// bind the slack and hipchat plugins to the router\n\trouter := hal.Router()\n\trouter.AddBroker(hc)\n\trouter.AddBroker(slk)\n\n\t// Plugin registration makes them available to the bot but does not\n\t// activate them. That happens at runtime using e.g. pluginmgr or\n\t// the plugin registry's LoadInstances() (used below)\n\tarchive.Register()\n\tgoogle_calendar.Register()\n\tmark.Register()\n\tpagerduty.Register()\n\tpluginmgr.Register()\n\tprefmgr.Register()\n\troster.Register()\n\tseppuku.Register()\n\tuptime.Register()\n\n\t// start up the router goroutine\n\tgo router.Route()\n\n\t// load any previously configured plugin instances from the database\n\tpr := hal.PluginRegistry()\n\tpr.LoadInstances()\n\n\t// pluginmgr is needed to set up all the other plugins\n\t// so if it's not present, initialize it manually just this once\n\t// alternatively, you could poke config straight into the DB\n\t// TODO: remove the hard-coded room name or make it configurable\n\tfor _, broker := range router.Brokers() {\n\t\tif len(pr.FindInstances(controlRoom, broker.Name(), \"pluginmgr\")) == 0 {\n\t\t\tmgr, _ := pr.GetPlugin(\"pluginmgr\")\n\t\t\tmgrInst := mgr.Instance(controlRoom, broker)\n\t\t\tmgrInst.Register()\n\t\t}\n\t}\n\n\t// temporary ... (2016-03-02)\n\t// TODO: remove this or make it permanent by using the same method as\n\t// the pluginmgr bootstrap above to set the room name, etc.\n\tfor _, broker := range router.Brokers() {\n\t\tbroker.Send(hal.Evt{\n\t\t\tBody: \"Ohai! HAL-9001 up and running.\",\n\t\t\tRoom: controlRoom,\n\t\t\tUser: \"HAL-9001\",\n\t\t})\n\t}\n\n\t// start the webserver - some plugins register handlers to the default\n\t// net/http router. This makes them available. Remove this if you don't\n\t// want the webserver and the handlers will be silently ignored.\n\tgo func() {\n\t\terr := http.ListenAndServe(webAddr, nil)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not listen on '%s': %s\\n\", webAddr, err)\n\t\t}\n\t}()\n\n\t// block forever\n\tselect {}\n}\n\nfunc requireEnv(key string) string {\n\tval := os.Getenv(key)\n\tif val == \"\" {\n\t\tlog.Fatalf(\"The %q environment variable is required!\", key)\n\t}\n\n\treturn val\n}\n\nfunc defaultEnv(key, def string) string {\n\tval := os.Getenv(key)\n\n\tif val == \"\" {\n\t\treturn def\n\t}\n\n\treturn val\n}\n"
  },
  {
    "path": "example/minimal/main.go",
    "content": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport \"github.com/netflix/hal-9001/hal\"\n\n// This bot doesn't do anything except start the router and wait forever\n// for messages that will never come.\n//\n// Most of hal's functionality is optional. It's still built along with the\n// rest of hal but is not active unless it's used in main or a plugin.\n\nfunc main() {\n\trouter := hal.Router()\n\trouter.Route()\n}\n"
  },
  {
    "path": "example/repl/main.go",
    "content": "package main\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"gopkg.in/DATA-DOG/go-sqlmock.v1\"\n\n\t\"github.com/netflix/hal-9001/brokers/console\"\n\t\"github.com/netflix/hal-9001/hal\"\n\t\"github.com/netflix/hal-9001/plugins/pluginmgr\"\n\t\"github.com/netflix/hal-9001/plugins/prefmgr\"\n\t\"github.com/netflix/hal-9001/plugins/uptime\"\n)\n\n// a simple bot that only implements generic plugins on a repl\n\nfunc main() {\n\t// SqlInit calls will still throw errors at startup but\n\t// it seems the program will continue so this will do for now\n\tdb, _, err := sqlmock.New()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\thal.ForceSqlDBHandle(db)\n\tdefer db.Close()\n\n\tpluginmgr.Register()\n\tprefmgr.Register()\n\tuptime.Register()\n\n\tconsole.REPL(\"repl\", \"\")\n}\n"
  },
  {
    "path": "hal/asciitable.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Table takes a 2-dimensional array of strings and returns a single string\n// formatted in a table appropriate for rendering in a fixed-width font.\n// Should be suitable for Markdown table rendering.\n// cheesy 2-pass technique, assuming straight fixed-width ASCII for now\nfunc AsciiTable(hdr []string, rows [][]string) string {\n\tif len(rows) == 0 {\n\t\treturn \"NO DATA TO DISPLAY\"\n\t} else if len(rows[0]) == 0 {\n\t\tpanic(\"BUG: the first row seems to be empty!\")\n\t}\n\n\t// find the needed width of each column\n\tcolwidths := make([]int, len(hdr))\n\t// start with the headers' widths\n\tfor j, col := range hdr {\n\t\tcolwidths[j] = len(col)\n\t}\n\n\t// bump to the size of any larger cells\n\tfor i, row := range rows {\n\t\t// handle empty/short rows gracefully by reallocating which\n\t\t// results in a default value of \"\"\n\t\tif len(row) < len(hdr) {\n\t\t\tnewrow := make([]string, len(hdr))\n\t\t\tcopy(newrow[0:len(row)], row)\n\t\t\trows[i] = newrow\n\t\t\trow = newrow\n\t\t}\n\n\t\tfor j, col := range row {\n\t\t\tif colwidths[j] < len(col) {\n\t\t\t\tcolwidths[j] = len(col)\n\t\t\t}\n\t\t}\n\t}\n\n\t// generate format strings for the columns\n\tfmts := make([]string, len(colwidths))\n\thrcs := make([]string, len(colwidths))\n\tif len(colwidths) > 1 {\n\t\tfor i, width := range colwidths {\n\t\t\tif i == 0 {\n\t\t\t\tfmts[i] = fmt.Sprintf(\"| %%%ds |\", width)\n\t\t\t\thrcs[i] = fmt.Sprintf(\"|%s|\", strings.Repeat(\"-\", width+2))\n\t\t\t} else if i == len(colwidths)-1 {\n\t\t\t\tfmts[i] = fmt.Sprintf(\" %%%ds |\\n\", width)\n\t\t\t\thrcs[i] = fmt.Sprintf(\"%s|\\n\", strings.Repeat(\"-\", width+2))\n\t\t\t} else {\n\t\t\t\tfmts[i] = fmt.Sprintf(\" %%%ds |\", width)\n\t\t\t\thrcs[i] = fmt.Sprintf(\"%s|\", strings.Repeat(\"-\", width+2))\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// single-column tables\n\t\tfmts[0] = fmt.Sprintf(\"| %%%ds |\\n\", colwidths[0])\n\t\thrcs[0] = fmt.Sprintf(\"|%s|\\n\", strings.Repeat(\"-\", colwidths[0]+2))\n\t}\n\n\t// horizontal rule\n\thr := strings.Join(hrcs, \"\")\n\n\tbuf := bytes.NewBuffer([]byte{})\n\n\tfmt.Fprint(buf, hr)\n\n\tfor j, col := range hdr {\n\t\tfmt.Fprintf(buf, fmts[j], col)\n\t}\n\n\tfmt.Fprintf(buf, hr)\n\n\tfor _, row := range rows {\n\t\tfor j, col := range row {\n\t\t\tfmt.Fprintf(buf, fmts[j], col)\n\t\t}\n\t}\n\n\tfmt.Fprintf(buf, hr)\n\n\treturn buf.String()\n}\n"
  },
  {
    "path": "hal/asciitable_test.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestAsciiTable(t *testing.T) {\n\tsamples := [][][]string{\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t},\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"left\", \"right\"},\n\t\t\t{\"one\", \"three\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"HEADER 1\", \"HDR 2\", \"LOL WUT\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t},\n\t\t{\n\t\t\t{\"Col 1\", \"Col 2\", \"3rd Column\", \"4th\", \"FIFTH\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t\t{\"hi\"},\n\t\t\t{\"\", \"\", \"\", \"-\", \"+\"},\n\t\t},\n\t}\n\n\tfor _, sample := range samples {\n\t\t// first row is the header, the rest is data rows\n\t\tout := AsciiTable(sample[0], sample[1:])\n\t\t// not a very useful test ... yet\n\t\tif len(out) == 0 {\n\t\t\tt.Fail()\n\t\t}\n\n\t\tfmt.Println(out)\n\t}\n}\n"
  },
  {
    "path": "hal/broker.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Broker is an instance of a broker that can send/receive events.\ntype Broker interface {\n\t// the text name of the broker, arbitrary, but usually \"slack\" or \"cli\"\n\tName() string\n\tSend(evt Evt)\n\tSendTable(evt Evt, header []string, rows [][]string)\n\tSendDM(evt Evt)\n\tSetTopic(roomId, topic string) error\n\tGetTopic(roomId string) (topic string, err error)\n\tLeave(roomId string) (err error)\n\tLooksLikeRoomId(room string) bool\n\tLooksLikeUserId(user string) bool\n\tRoomIdToName(id string) (name string)\n\tRoomNameToId(name string) (id string)\n\tUserIdToName(id string) (name string)\n\tUserNameToId(name string) (id string)\n\tStream(out chan *Evt)\n}\n"
  },
  {
    "path": "hal/cmd.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n/* While it's possible to use the standard library flags or an off-the-github\n * command-line parser, they have proven to be clunky and often hacky to use.\n * This API is purpose-built for building bot plugins, focusing on doing the\n * tedious parts of parsing commands without getting in the way.\n * Rules:\n *   1. \"*\" as user input means \"whatever, from the current context\" e.g. --room *\n *   2. \"*\" as a Cmd.Token means \"anything and everything remaining in argv\"\n */\n\n// supported time formats for ParamInst.Time()\nvar TimeFormats = [...]string{\n\t\"2006-01-02\",\n\t\"2006-01-02-07:00\",\n\t\"2006-01-02T15:04\",\n\t\"2006-01-02T15:04-07:00\",\n\t\"2006-01-02T15:04:05\",\n\t\"2006-01-02T15:04:05-07:00\",\n}\n\n// Cmd models a tree of commands and subcommands along with their parameters.\n// The tree will almost always be 1 or 2 levels deep. Deeper is possible but\n// unlikely to be much higher, KISS.\n// TODO: switc to maps for (kv|bool|idx)params and maybe subCmds\ntype Cmd struct {\n\ttoken      string // * => slurp everything remaining\n\tusage      string\n\tsubCmds    []*SubCmd\n\tkvparams   []*KVParam\n\tboolparams []*BoolParam\n\tidxparams  map[int]*IdxParam\n\taliases    []string\n\tprev       *Cmd // parent command, nil for root\n\tmustSubCmd bool // a subcommand is always required\n}\n\ntype SubCmd struct {\n\tcmd *Cmd\n\tCmd\n}\n\ntype CmdInst struct {\n\tcmd            *Cmd\n\tsubCmdInst     *SubCmdInst\n\tkvparaminsts   []*KVParamInst\n\tboolparaminsts []*BoolParamInst\n\tidxparaminsts  map[int]*IdxParamInst\n\tremainder      []string // args left over after parsing, usually empty\n}\n\ntype SubCmdInst struct {\n\tsubCmd *SubCmd\n\tCmdInst\n}\n\n// key/value parameters, e.g. \"--foo=bar\", \"foo=bar\", \"-f bar\", \"--foo bar\"\ntype KVParam struct {\n\tkey      string   // the \"foo\" in --foo, -f, foo=bar\n\taliases  []string // parameter aliases, e.g. foo => f\n\tusage    string   // usage string for generating help\n\trequired bool     // whether or not this parameter is required\n\tdef      string   // default value when omitted\n\thasdef   bool     // whether or not a default has been provided\n\tcmd      *Cmd     // the (top-level) command the param is attached to\n\tsubcmd   *SubCmd  // the subcommand the param is attached to\n}\n\n// keyed parameters that are boolean (flags), e.g. \"--foo\", \"-f\", \"foo=true\"\ntype BoolParam struct {\n\tkey      string\n\taliases  []string\n\tusage    string\n\trequired bool\n\tdef      bool\n\thasdef   bool\n\tcmd      *Cmd\n\tsubcmd   *SubCmd\n}\n\n// positional parameters (0 indexed)\ntype IdxParam struct {\n\tidx      int // positional arg index\n\tname     string\n\tusage    string\n\trequired bool\n\tdef      string\n\thasdef   bool\n\tcmd      *Cmd\n\tsubcmd   *SubCmd\n}\n\n// KVParamInst represents a key/value parameter found in the command\ntype KVParamInst struct {\n\tcmdinst    *CmdInst    // the top-level command\n\tsubcmdinst *SubCmdInst // the subcommand the param belongs to, nil for top-level\n\tparam      *KVParam\n\tfound      bool   // was the parameter set in the command?\n\tisdef      bool   // was the parameter set using a default?\n\targ        string // the original/unmodified argument (e.g. --foo, -f)\n\tkey        string // the key, e.g. \"foo\"\n\tvalue      string\n}\n\n// BoolParamInst represents a flag/boolean parameter found in the command\ntype BoolParamInst struct {\n\tcmdinst    *CmdInst\n\tsubcmdinst *SubCmdInst\n\tparam      *BoolParam\n\tfound      bool\n\tisdef      bool\n\targ        string\n\tkey        string\n\tvalue      bool\n}\n\n// IdxParamInst represents a positional parameter found in the command\ntype IdxParamInst struct {\n\tcmdinst    *CmdInst\n\tsubcmdinst *SubCmdInst\n\tparam      *IdxParam\n\tfound      bool\n\tisdef      bool\n\tidx        int\n\tname       string\n\tvalue      string\n}\n\n// tmpParamInst used by the parser to hold keyed parameters before attaching to commands/subcommands.\ntype tmpParamInst struct {\n\tcmd        *Cmd\n\tcmdinst    *CmdInst\n\tsubcmd     *SubCmd\n\tsubcmdinst *SubCmdInst\n\tfound      bool\n\targ        string\n\tkey        string\n\tvalue      string\n}\n\ntype stringValuedParamInst interface {\n\tFound() bool\n\tRequired() bool\n\tValue() string\n\tString() (string, error)\n\tInt() (int, error)\n\tFloat() (float64, error)\n\tBool() (bool, error)\n\terrParam() NamedParam\n}\n\n// cmdorsubcmd is used internally to pass either a Cmd or SubCmd\n// to a helper function so I don't have to copy/paste the code\ntype cmdorsubcmd interface {\n\tHasKVParam(string) bool\n\tHasBoolParam(string) bool\n\tHasIdxParam(int) bool\n\tGetKVParam(string) *KVParam\n\tGetBoolParam(string) *BoolParam\n\tGetIdxParam(int) *IdxParam\n\tappendKVParamInst(*KVParamInst)\n\tappendBoolParamInst(*BoolParamInst)\n\tappendIdxParamInst(*IdxParamInst)\n}\n\ntype NamedParam interface {\n\tName() string\n\tUsage() string\n\tIsRequired() bool\n}\n\ntype SubCmdNotFound struct {\n\targv []string\n}\n\nfunc (e SubCmdNotFound) Error() string {\n\treturn fmt.Sprintf(\"A subcommand is required but %q was provided.\", strings.Join(e.argv, \" \"))\n}\n\n// RequiredParamNotFound is returned when a parameter has Required=true\n// and a method was used to access the value but no value was set in the\n// command.\ntype RequiredParamNotFound struct {\n\tParam NamedParam\n}\n\n// Error fulfills the Error interface.\nfunc (e RequiredParamNotFound) Error() string {\n\treturn fmt.Sprintf(\"Parameter %q is required but not set.\", e.Param.Name())\n}\n\n// UnsupportedTimeFormatError is returned when a provided time string cannot\n// be parsed with one of the pre-defined time formats.\ntype UnsupportedTimeFormatError struct {\n\tvalue string\n}\n\n// Error fulfills the Error interface for UnsupportedTimeFormatError.\nfunc (e UnsupportedTimeFormatError) Error() string {\n\treturn fmt.Sprintf(\"Time string %q does not appear to be in a supported format.\", e.value)\n}\n\n// NewCmd returns an initialized Cmd.\nfunc NewCmd(token string, mustsubcmd bool) *Cmd {\n\tcmd := Cmd{token: token, mustSubCmd: mustsubcmd}\n\treturn &cmd\n}\n\n// ListSubCmds makes sure the SubCmds list is initialized and returns the list.\nfunc (c *Cmd) ListSubCmds() []*SubCmd {\n\tif c.subCmds == nil {\n\t\tc.subCmds = make([]*SubCmd, 0)\n\t}\n\n\treturn c.subCmds\n}\n\n// _kvparams makes sure the _kvparams list is initialized and returns the list.\nfunc (c *Cmd) _kvparams() []*KVParam {\n\tif c.kvparams == nil {\n\t\tc.kvparams = make([]*KVParam, 0)\n\t}\n\n\treturn c.kvparams\n}\n\n// _boolparams makes sure the _boolparams list is initialized and returns the list.\nfunc (c *Cmd) _boolparams() []*BoolParam {\n\tif c.boolparams == nil {\n\t\tc.boolparams = make([]*BoolParam, 0)\n\t}\n\n\treturn c.boolparams\n}\n\n// _idxparams makes sure the _idxparams map is initialized and returns the map.\nfunc (c *Cmd) _idxparams() map[int]*IdxParam {\n\tif c.idxparams == nil {\n\t\tc.idxparams = make(map[int]*IdxParam)\n\t}\n\n\treturn c.idxparams\n}\n\n// Aliases makes sure the Aliases list is initialized and returns the list.\nfunc (c *Cmd) Aliases() []string {\n\tif c.aliases == nil {\n\t\tc.aliases = make([]string, 0)\n\t}\n\n\treturn c.aliases\n}\n\n// assertZeroIdxParams panics if there are any IdxParam defined.\nfunc (c *Cmd) assertZeroIdxParams() {\n\tpps := c._idxparams()\n\tif len(pps) > 0 {\n\t\tlog.Panic(\"Illegal mixing of positional and key/value parameters.\")\n\t}\n}\n\n// assertZeroKeyParams panics if there are any BoolParam or KVParam defined.\nfunc (c *Cmd) assertZeroKeyParams() {\n\tkps := c._kvparams()\n\tbps := c._boolparams()\n\tif len(kps) > 0 || len(bps) > 0 {\n\t\tlog.Panic(\"Illegal mixing of positional and key/value parameters.\")\n\t}\n}\n\n// AddKVParam creates and adds a key/value parameter to the command handle\n// and returns the new parameter.\nfunc (c *Cmd) AddKVParam(key string, required bool) *KVParam {\n\tc.assertZeroIdxParams()\n\n\tp := KVParam{key: key}\n\tp.required = required\n\tp.cmd = c.Cmd()\n\n\tc.kvparams = append(c._kvparams(), &p)\n\n\treturn &p\n}\n\n// AddBoolParam adds a boolean/flag parameter to the command and returns the\n// new parameter.\nfunc (c *Cmd) AddBoolParam(key string, required bool) *BoolParam {\n\tc.assertZeroIdxParams()\n\n\tp := BoolParam{}\n\tp.key = key\n\tp.required = required\n\tp.cmd = c.Cmd()\n\n\tc.boolparams = append(c._boolparams(), &p)\n\n\treturn &p\n}\n\n// AddIdxParam adds a positional parameter to the command and returns the\n// new parameter.\nfunc (c *Cmd) AddIdxParam(position int, name string, required bool) *IdxParam {\n\tc.assertZeroKeyParams()\n\n\tips := c._idxparams()\n\n\tif _, exists := ips[position]; exists {\n\t\tlog.Panicf(\"position %d already has an IdxParam defined on this command\", position)\n\t}\n\n\tips[position] = &IdxParam{\n\t\tidx:      position,\n\t\tname:     name,\n\t\tusage:    name, // what you want most of the time\n\t\trequired: required,\n\t\tcmd:      c.Cmd(),\n\t}\n\n\treturn ips[position]\n}\n\n// AddKVParam creates and adds a key/value parameter to the subcommand\n// and returns the new parameter.\nfunc (c *SubCmd) AddKVParam(key string, required bool) *KVParam {\n\tc.assertZeroIdxParams()\n\n\tp := KVParam{key: key}\n\tp.required = required\n\tp.cmd = c.cmd\n\tp.subcmd = c\n\n\tc.kvparams = append(c._kvparams(), &p)\n\n\treturn &p\n}\n\n// AddBoolParam adds a boolean/flag parameter to the subcommand and returns the\n// new parameter.\nfunc (c *SubCmd) AddBoolParam(key string, required bool) *BoolParam {\n\tc.assertZeroIdxParams()\n\n\tp := BoolParam{}\n\tp.key = key\n\tp.required = required\n\tp.cmd = c.cmd\n\tp.subcmd = c\n\n\tc.boolparams = append(c._boolparams(), &p)\n\n\treturn &p\n}\n\n// AddIdxParam adds a positional parameter to the subcommand and returns the\n// new parameter.\nfunc (c *SubCmd) AddIdxParam(position int, name string, required bool) *IdxParam {\n\tc.assertZeroKeyParams()\n\n\tips := c._idxparams()\n\n\tif _, exists := ips[position]; exists {\n\t\tlog.Panicf(\"position %d already has an IdxParam defined on this subcommand\", position)\n\t}\n\n\tips[position] = &IdxParam{\n\t\tidx:      position,\n\t\tname:     name,\n\t\tusage:    name, // what you want most of the time\n\t\trequired: required,\n\t\tcmd:      c.cmd,\n\t\tsubcmd:   c,\n\t}\n\n\treturn ips[position]\n}\n\n// AddAlias adds an alias to the command and returns the paramter.\nfunc (c *Cmd) AddAlias(alias string) *Cmd {\n\tc.aliases = append(c.Aliases(), alias)\n\treturn c\n}\n\nfunc (s *SubCmd) AddAlias(alias string) *SubCmd {\n\ts.aliases = append(s.Aliases(), alias)\n\treturn s\n}\n\n// AddAlias adds an alias to the parameter and returns the paramter.\nfunc (p *KVParam) AddAlias(alias string) *KVParam {\n\tp.aliases = append(p.Aliases(), alias)\n\treturn p\n}\n\nfunc (c *Cmd) Parent() *Cmd {\n\treturn c.prev\n}\n\n// MustSubCmd returns bool indicating if a subcommand is required.\nfunc (c *Cmd) MustSubCmd() bool {\n\treturn c.mustSubCmd\n}\n\n// Usage returns the auto-generated usage string.\nfunc (c *Cmd) Usage() string {\n\tout := make([]string, 1)\n\tout[0] = c.token + \" - \" + c.usage\n\n\tfor _, scmd := range c.ListSubCmds() {\n\t\tparams := scmd.ListNamedParams()\n\t\topttxt := make([]string, len(params))\n\n\t\tfor i, p := range params {\n\t\t\tvar txt, required string\n\n\t\t\tif p.IsRequired() {\n\t\t\t\trequired = \" (required)\"\n\t\t\t}\n\n\t\t\tswitch p.(type) {\n\t\t\tcase *KVParam:\n\t\t\t\ttxt = fmt.Sprintf(\"-%s <%s>\", p.Name(), p.Name())\n\t\t\tcase *BoolParam:\n\t\t\t\ttxt = fmt.Sprintf(\"-%s\", p.Name())\n\t\t\tcase *IdxParam:\n\t\t\t\ttxt = p.Name()\n\t\t\t}\n\n\t\t\topttxt[i] = fmt.Sprintf(\"\\t\\t%s%s: %s\", txt, required, p.Usage())\n\t\t}\n\n\t\tout = append(out, \"\\t\"+scmd.Usage())\n\t\tout = append(out, opttxt...)\n\t}\n\n\treturn strings.Join(out, \"\\n\")\n}\n\n// SetUsage sets the usage string for the command. Returns the command.\nfunc (c *Cmd) SetUsage(usage string) *Cmd {\n\tc.usage = usage\n\treturn c\n}\n\n// SetUsage sets the subcommand's usage string.\nfunc (s *SubCmd) SetUsage(usage string) *SubCmd {\n\ts.usage = usage\n\treturn s\n}\n\n// Usage returns the auto-generated usage string for the Command Instance.\nfunc (c *CmdInst) Usage() string {\n\treturn c.cmd.Usage()\n}\n\nfunc (p *KVParam) Usage() string {\n\treturn p.usage\n}\n\nfunc (p *BoolParam) Usage() string {\n\treturn p.usage\n}\n\nfunc (p *IdxParam) Usage() string {\n\treturn p.usage\n}\n\n// SetUsage sets the usage string for the paremeter. Returns the parameter.\nfunc (p *KVParam) SetUsage(usage string) *KVParam {\n\tp.usage = usage\n\treturn p\n}\n\n// SetUsage sets the usage string for the paremeter. Returns the parameter.\nfunc (p *BoolParam) SetUsage(usage string) *BoolParam {\n\tp.usage = usage\n\treturn p\n}\n\n// SetUsage sets the usage string for the paremeter. Returns the parameter.\nfunc (p *IdxParam) SetUsage(usage string) *IdxParam {\n\tp.usage = usage\n\treturn p\n}\n\nfunc (p *KVParam) SetDefault(def string) *KVParam {\n\tp.def = def\n\tp.hasdef = true\n\treturn p\n}\n\nfunc (p *BoolParam) SetDefault(def bool) *BoolParam {\n\tp.def = def\n\tp.hasdef = true\n\treturn p\n}\n\nfunc (p *IdxParam) SetDefault(def string) *IdxParam {\n\tp.def = def\n\tp.hasdef = true\n\treturn p\n}\n\nfunc (p *KVParam) Key() string {\n\treturn p.key\n}\n\nfunc (p *BoolParam) Key() string {\n\treturn p.key\n}\n\nfunc (p *IdxParam) Idx() int {\n\treturn p.idx\n}\n\nfunc (p *KVParamInst) Key() string {\n\treturn p.key\n}\n\nfunc (p *BoolParamInst) Key() string {\n\treturn p.key\n}\n\nfunc (p *IdxParamInst) Idx() int {\n\treturn p.idx\n}\n\n// Name returns the key string. Mostly for use in printing errors, etc.\n// Implements NamedParam.\nfunc (p *KVParam) Name() string {\n\treturn p.key\n}\n\n// Name returns the key string. Mostly for use in printing errors, etc.\n// Implements NamedParam.\nfunc (p *BoolParam) Name() string {\n\treturn p.key\n}\n\n// Name returns the name given to the indexed param.\n// Implements NamedParam.\nfunc (p *IdxParam) Name() string {\n\treturn p.name\n}\n\nfunc (p *KVParam) IsRequired() bool {\n\treturn p.required\n}\nfunc (p *BoolParam) IsRequired() bool {\n\treturn p.required\n}\nfunc (p *IdxParam) IsRequired() bool {\n\treturn p.required\n}\n\n// Cmd returns the command the parameter belongs to. Panics if no command is attached.\nfunc (p *KVParam) Cmd() *Cmd {\n\tif p.cmd == nil {\n\t\tpanic(\"Can't call Cmd() on this KVParam because it is not attached to a Cmd!\")\n\t}\n\n\treturn p.cmd\n}\n\n// Cmd returns the command the parameter belongs to. Panics if no command is attached.\nfunc (p *BoolParam) Cmd() *Cmd {\n\tif p.cmd == nil {\n\t\tpanic(\"Can't call Cmd() on this BoolParam because it is not attached to a Cmd!\")\n\t}\n\n\treturn p.cmd\n}\n\n// Cmd returns the command the parameter belongs to. Panics if no command is attached.\nfunc (p *IdxParam) Cmd() *Cmd {\n\tif p.cmd == nil {\n\t\tpanic(\"Can't call Cmd() on this IdxParam because it is not attached to a Cmd!\")\n\t}\n\n\treturn p.cmd\n}\n\nfunc (p *KVParam) SubCmd() *SubCmd {\n\tif p.subcmd == nil {\n\t\tpanic(\"Can't call SubCmd() on this KVParam because it is not attached to a SubCmd!\")\n\t}\n\n\treturn p.subcmd\n}\n\nfunc (p *BoolParam) SubCmd() *SubCmd {\n\tif p.subcmd == nil {\n\t\tpanic(\"Can't call SubCmd() on this BoolParam because it is not attached to a SubCmd!\")\n\t}\n\n\treturn p.subcmd\n}\n\nfunc (p *IdxParam) SubCmd() *SubCmd {\n\tif p.subcmd == nil {\n\t\tpanic(\"Can't call SubCmd() on this IdxParam because it is not attached to a SubCmd!\")\n\t}\n\n\treturn p.subcmd\n}\n\nfunc (p *KVParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, isdef bool, value string) *KVParamInst {\n\treturn &KVParamInst{\n\t\tcmdinst:    cmdinst,\n\t\tsubcmdinst: subcmdinst,\n\t\tisdef:      isdef,\n\t\tparam:      p,\n\t\tkey:        p.key,\n\t\tvalue:      value,\n\t}\n}\n\nfunc (p *BoolParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, isdef bool, value bool) *BoolParamInst {\n\treturn &BoolParamInst{\n\t\tcmdinst:    cmdinst,\n\t\tsubcmdinst: subcmdinst,\n\t\tisdef:      isdef,\n\t\tparam:      p,\n\t\tkey:        p.key,\n\t\tvalue:      value,\n\t}\n}\n\nfunc (p *IdxParam) newInst(cmdinst *CmdInst, subcmdinst *SubCmdInst, isdef bool, value string) *IdxParamInst {\n\treturn &IdxParamInst{\n\t\tcmdinst:    cmdinst,\n\t\tsubcmdinst: subcmdinst,\n\t\tisdef:      isdef,\n\t\tparam:      p,\n\t\tidx:        p.idx,\n\t\tname:       p.name,\n\t\tvalue:      value,\n\t}\n}\n\n// Cmd returns the command the parameter belongs to. Panics if no command is attached.\nfunc (p *KVParamInst) Cmd() *Cmd {\n\tif p.param == nil {\n\t\tpanic(\"Can't call Cmd() on this KVParamInst because it is not attached to a KVParam!\")\n\t}\n\n\treturn p.param.Cmd()\n}\n\n// Cmd returns the command the parameter belongs to. Panics if no command is attached.\nfunc (p *BoolParamInst) Cmd() *Cmd {\n\tif p.param == nil {\n\t\tpanic(\"Can't call Cmd() on this BoolParamInst because it is not attached to a BoolPararm!\")\n\t}\n\n\treturn p.param.Cmd()\n}\n\n// Cmd returns the command the parameter belongs to. Panics if no command is attached.\nfunc (p *IdxParamInst) Cmd() *Cmd {\n\tif p.param == nil {\n\t\tpanic(\"Can't call Cmd() on this IdxParamInst because it is not attached to a IdxParam!\")\n\t}\n\n\treturn p.param.Cmd()\n}\n\nfunc (p *KVParamInst) SubCmdInst() *SubCmdInst {\n\tif p.subcmdinst == nil {\n\t\tpanic(\"Can't call SubCmdInst() on this KVParamInst because it is not attached to a SubCmdInst!\")\n\t}\n\n\treturn p.subcmdinst\n}\n\nfunc (p *BoolParamInst) SubCmdInst() *SubCmdInst {\n\tif p.subcmdinst == nil {\n\t\tpanic(\"Can't call SubCmdInst() on this BoolParamInst because it is not attached to a SubCmdInst!\")\n\t}\n\n\treturn p.subcmdinst\n}\n\nfunc (p *IdxParamInst) SubCmdInst() *SubCmdInst {\n\tif p.subcmdinst == nil {\n\t\tpanic(\"Can't call SubCmdInst() on this IdxParamInst because it is not attached to a SubCmd!\")\n\t}\n\n\treturn p.subcmdinst\n}\n\nfunc (p *KVParamInst) Found() bool {\n\treturn p.found\n}\n\nfunc (p *BoolParamInst) Found() bool {\n\treturn p.found\n}\n\nfunc (p *IdxParamInst) Found() bool {\n\treturn p.found\n}\n\nfunc (p *KVParamInst) Required() bool {\n\treturn p.param.required\n}\n\nfunc (p *BoolParamInst) Required() bool {\n\treturn p.param.required\n}\n\nfunc (p *IdxParamInst) Required() bool {\n\treturn p.param.required\n}\n\nfunc (p *KVParamInst) Param() *KVParam {\n\treturn p.param\n}\n\nfunc (p *BoolParamInst) Param() *BoolParam {\n\treturn p.param\n}\n\nfunc (p *IdxParamInst) Param() *IdxParam {\n\treturn p.param\n}\n\n// errParam is used to get an interface{} handle to return in errors.\n// See: RequiredParamNotFound\nfunc (p *KVParamInst) errParam() NamedParam {\n\treturn p.param\n}\n\n// errParam is used to get an interface{} handle to return in errors.\nfunc (p *BoolParamInst) errParam() NamedParam {\n\treturn p.param\n}\n\n// errParam is used to get an interface{} handle to return in errors.\nfunc (p *IdxParamInst) errParam() NamedParam {\n\treturn p.param\n}\n\n// Cmd returns the command it was called on. It does nothing and exists to\n// make it possible to format chained calls nicely.\nfunc (c *Cmd) Cmd() *Cmd {\n\treturn c\n}\n\nfunc (s *SubCmd) SubCmd() *SubCmd {\n\treturn s\n}\n\nfunc (c *Cmd) Token() string {\n\treturn c.token\n}\n\n// AddCmd adds a subcommand to the handle and returns the new (sub-)command.\nfunc (c *Cmd) AddSubCmd(token string) *SubCmd {\n\tsub := SubCmd{}\n\tsub.prev = c\n\tsub.token = token\n\n\tc.subCmds = append(c.ListSubCmds(), &sub)\n\n\treturn &sub\n}\n\nfunc (c *Cmd) GetKVParam(key string) *KVParam {\n\tfor _, p := range c._kvparams() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\nfunc (c *Cmd) GetBoolParam(key string) *BoolParam {\n\tfor _, p := range c._boolparams() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\n// GetIdxParam gets a positional parameter by its index.\nfunc (c *Cmd) GetIdxParam(idx int) *IdxParam {\n\tips := c._idxparams()\n\n\tif p, exists := ips[idx]; exists {\n\t\treturn p\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\nfunc (c *Cmd) HasKVParam(key string) bool {\n\tfor _, p := range c._kvparams() {\n\t\tif p.key == key {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *Cmd) HasBoolParam(key string) bool {\n\tfor _, p := range c._boolparams() {\n\t\tif p.key == key {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *Cmd) HasIdxParam(idx int) bool {\n\tips := c._idxparams()\n\t_, exists := ips[idx]\n\treturn exists\n}\n\n// TODO: remove this?\nfunc (c *Cmd) SubCmds() []*SubCmd {\n\treturn c.ListSubCmds()\n}\n\n// GetSubCmd gets a subcommand by its token. Returns nil for no match.\nfunc (c *Cmd) GetSubCmd(token string) *SubCmd {\n\tfor _, s := range c.ListSubCmds() {\n\t\tif s.token == token {\n\t\t\treturn s\n\t\t}\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\n// parse a list of argv-style strings (0 is always the command name e.g. []string{\"prefs\"})\n// foo bar --baz\n// foo --bar baz --version\n// foo bar=baz\n// foo x=y z=q init --foo baz\n// TODO: automatic emdash cleanup\n// TODO: enforce MustSubCmd\n// TODO: return errors instead of nil/panic\nfunc (c *Cmd) Process(argv []string) (*CmdInst, error) {\n\t// a hand-coded argument processor that evaluates the provided argv list\n\t// against the command definition and returns a CmdInst with all of the\n\t// available data parsed and ready to use with CmdInst/ParamInst methods.\n\n\t// the top-level command instance\n\ttopInst := CmdInst{cmd: c}\n\n\t// no arguments were provided\n\tif len(argv) == 1 {\n\t\tif c.mustSubCmd {\n\t\t\treturn nil, SubCmdNotFound{argv: argv}\n\t\t} else {\n\t\t\treturn &topInst, nil\n\t\t}\n\t}\n\n\tvar curSubCmdInst *SubCmdInst // the current subcommand - changes during parsing\n\tvar curSubCmdIdx int          // the idx the subcommand found in argv\n\tvar skipNext bool\n\tvar looseParams []*tmpParamInst\n\n\t// first pass: extract subcommands and parameters\n\tfor i, arg := range argv[1:] {\n\t\tif skipNext {\n\t\t\tskipNext = false\n\t\t\tcontinue\n\t\t}\n\n\t\tvar key, value, next string\n\t\tvar nextExists bool\n\n\t\tif i+2 < len(argv) {\n\t\t\tnext = argv[i+2]\n\t\t\tnextExists = true\n\t\t} else {\n\t\t\tnextExists = false\n\t\t}\n\n\t\tif c.HasIdxParam(i - 1) {\n\t\t\t// top-level command has positional parameters\n\t\t\tpi := IdxParamInst{\n\t\t\t\tcmdinst: &topInst,\n\t\t\t\tfound:   true,\n\t\t\t\tidx:     i - 1,\n\t\t\t\tparam:   c.GetIdxParam(i - 1),\n\t\t\t\tvalue:   arg,\n\t\t\t}\n\n\t\t\ttopInst.appendIdxParamInst(&pi)\n\t\t} else if curSubCmdInst != nil && curSubCmdInst.HasIdxParam(0) {\n\t\t\t// subcommand has positional parameters\n\t\t\tparamIdx := i - curSubCmdIdx - 1\n\n\t\t\tpi := IdxParamInst{\n\t\t\t\tcmdinst:    &topInst,\n\t\t\t\tsubcmdinst: curSubCmdInst,\n\t\t\t\tfound:      true,\n\t\t\t\tidx:        paramIdx,\n\t\t\t\tparam:      curSubCmdInst.GetIdxParam(paramIdx),\n\t\t\t\tvalue:      arg,\n\t\t\t}\n\n\t\t\tcurSubCmdInst.appendIdxParamInst(&pi)\n\t\t} else if strings.Contains(arg, \"=\") {\n\t\t\t// looks like a key=value or --key=value parameter\n\t\t\t// could be --foo=bar but all that matters is the \"foo\"\n\t\t\t// could be --foo=true for BoolParam and that's fine too\n\t\t\tkv := strings.SplitN(arg, \"=\", 2)\n\t\t\tkey = strings.TrimLeft(kv[0], \"-\")\n\t\t\tvalue = kv[1]\n\t\t\t// falls through, further processing below this if block...\n\t\t} else if looksLikeParam(arg) {\n\t\t\t// looks like a parameter\n\t\t\t// e.g. --foo bar -f bar\n\t\t\tkey = strings.TrimLeft(arg, \"-\")\n\t\t\t// TODO: this handles many instances of boolean flags that indicate true\n\t\t\t// simply by being present. There are likely some edge cases where this\n\t\t\t// doesn't work because the following param doesn't look like a param\n\t\t\t// then again maybe it's no big deal...\n\t\t\tif nextExists && !looksLikeParam(next) {\n\t\t\t\tvalue = next\n\t\t\t\tskipNext = true\n\t\t\t}\n\t\t\t// falls through, further processing below this if block...\n\t\t} else if curSubCmdInst == nil && c.HasSubCmdToken(arg) {\n\t\t\t// the first subcommand - the \"foo\" in \"!command foo bar --baz\"\n\t\t\tfor _, sc := range topInst.cmd.ListSubCmds() {\n\t\t\t\tif sc.token == arg {\n\t\t\t\t\tsci := SubCmdInst{subCmd: sc}\n\t\t\t\t\tsci.cmd = c\n\t\t\t\t\tcurSubCmdInst = &sci\n\t\t\t\t\ttopInst.subCmdInst = &sci\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontinue // processed a subcommand, move onto the next arg\n\t\t} else if curSubCmdInst != nil && curSubCmdInst.subCmd.HasSubCmdToken(arg) {\n\t\t\t// sub-subcommands - the \"bar\" or \"blargh\" in \"!command foo bar blargh --baz\"\n\t\t\tfor _, sc := range curSubCmdInst.subCmd.ListSubCmds() {\n\t\t\t\tif arg == sc.token {\n\t\t\t\t\tsci := SubCmdInst{subCmd: sc}\n\t\t\t\t\tsci.cmd = c\n\n\t\t\t\t\t// point the current subcommand to the new one\n\t\t\t\t\tcurSubCmdInst.subCmdInst = &sci\n\n\t\t\t\t\t// advance \"current\" to the new subcommand\n\t\t\t\t\tcurSubCmdInst = &sci\n\n\t\t\t\t\t// set the index where the subcommand was discovered for use\n\t\t\t\t\t// in extracting postitional parameters (above)\n\t\t\t\t\tcurSubCmdIdx = i\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcontinue // processed a subcommand, move onto the next arg\n\t\t} else {\n\t\t\t// leftover/unrecognized args go in .remainder\n\t\t\ttopInst.remainder = append(topInst.Remainder(), arg)\n\t\t\tcontinue\n\t\t}\n\n\t\tpinst := tmpParamInst{\n\t\t\tkey:     key,\n\t\t\targ:     arg,\n\t\t\tvalue:   value,\n\t\t\tfound:   true,\n\t\t\tcmd:     c,\n\t\t\tcmdinst: &topInst,\n\t\t}\n\n\t\t// the most recent subcommand seen gets the first shot at a parameter\n\t\t// !foo --bar baz --bar\n\t\t// !foo baz --bar\n\t\t// !foo --bar baz\n\t\tif curSubCmdInst != nil && curSubCmdInst.subCmd.HasKeyParam(key) {\n\t\t\t// the parameter belongs to the subcommand\n\t\t\tpinst.subcmd = curSubCmdInst.subCmd\n\t\t\tpinst.subcmdinst = curSubCmdInst\n\t\t\tpinst.attachKeyParam(curSubCmdInst)\n\t\t} else if c.HasKeyParam(key) {\n\t\t\t// the parameter belongs to the command\n\t\t\tpinst.attachKeyParam(&topInst)\n\t\t} else {\n\t\t\t// store (likely) out-of-order parameters to process after all args &\n\t\t\t// subcommands are discovered\n\t\t\tlooseParams = append(looseParams, &pinst)\n\t\t}\n\t}\n\n\tif c.mustSubCmd && topInst.subCmdInst == nil {\n\t\treturn nil, SubCmdNotFound{argv: argv}\n\t}\n\n\t// find a home for out-of-order parameters, panic if that fails since it's a bug\n\tfor _, linst := range looseParams {\n\t\tif topInst.subCmdInst == nil {\n\t\t\tpanic(\"found out-of-order params but no subcommand! Maybe bug, maybe I need to put a better error here...\")\n\t\t}\n\t\tlinst.findAndAttachKeyParam(topInst.subCmdInst)\n\t}\n\n\t// now that all the parameters have been parsed and attached to a *cmdInst,\n\t// find required parameters with defaults and create param instances\n\t// or return an error when a required param isn't found\n\n\t// check for required parameters on the command\n\tfor _, p := range c.kvparams {\n\t\tif p.required && !topInst.HasKVParamInst(p.Key()) {\n\t\t\tif p.hasdef {\n\t\t\t\tpinst := p.newInst(&topInst, nil, true, p.def)\n\t\t\t\ttopInst.appendKVParamInst(pinst)\n\t\t\t} else {\n\t\t\t\treturn nil, RequiredParamNotFound{p}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, p := range c.boolparams {\n\t\tif p.required && !topInst.HasBoolParamInst(p.Key()) {\n\t\t\tif p.hasdef {\n\t\t\t\tpinst := p.newInst(&topInst, nil, true, p.def)\n\t\t\t\ttopInst.appendBoolParamInst(pinst)\n\t\t\t} else {\n\t\t\t\treturn nil, RequiredParamNotFound{p}\n\t\t\t}\n\t\t}\n\t}\n\n\tfor idx, p := range c.idxparams {\n\t\tif p.required && !topInst.HasIdxParamInst(idx) {\n\t\t\tif p.hasdef {\n\t\t\t\tpinst := p.newInst(&topInst, nil, true, p.def)\n\t\t\t\ttopInst.appendIdxParamInst(pinst)\n\t\t\t} else {\n\t\t\t\treturn nil, RequiredParamNotFound{p}\n\t\t\t}\n\t\t}\n\t}\n\n\t// check subcommands one by one\n\tfor _, sci := range topInst.listSubCmdInst() {\n\t\t// go over each parameter and see if it's present\n\t\tfor _, p := range sci.subCmd._kvparams() {\n\t\t\t// see if it's required and was not found\n\t\t\tif p.required && !sci.HasKVParamInst(p.Key()) {\n\t\t\t\t// if there is a default, use it\n\t\t\t\tif p.hasdef {\n\t\t\t\t\tpinst := p.newInst(&topInst, sci, true, p.def)\n\t\t\t\t\tsci.appendKVParamInst(pinst)\n\t\t\t\t} else {\n\t\t\t\t\t// required parameter was missing, return an error\n\t\t\t\t\treturn nil, RequiredParamNotFound{p}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor _, p := range sci.subCmd._boolparams() {\n\t\t\tif p.required && !sci.HasBoolParamInst(p.Key()) {\n\t\t\t\tif p.hasdef {\n\t\t\t\t\tpinst := p.newInst(&topInst, sci, true, p.def)\n\t\t\t\t\tsci.appendBoolParamInst(pinst)\n\t\t\t\t} else {\n\t\t\t\t\treturn nil, RequiredParamNotFound{p}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tfor _, p := range sci.subCmd._idxparams() {\n\t\t\tif p.required && !sci.HasIdxParamInst(p.Idx()) {\n\t\t\t\tif p.hasdef {\n\t\t\t\t\tpinst := p.newInst(&topInst, sci, true, p.def)\n\t\t\t\t\tsci.appendIdxParamInst(pinst)\n\t\t\t\t} else {\n\t\t\t\t\treturn nil, RequiredParamNotFound{p}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &topInst, nil\n}\n\n// looksLikeBool checks to see if the provided value contains \"true\" or \"false\"\n// in any case combination.\nfunc looksLikeBool(val string) bool {\n\tlcval := strings.ToLower(val)\n\n\tif strings.Contains(lcval, \"true\") {\n\t\treturn true\n\t}\n\n\tif strings.Contains(lcval, \"false\") {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\n// looksLikeParam returns true if there is a leading - or an = in the string.\nfunc looksLikeParam(key string) bool {\n\tif strings.HasPrefix(key, \"-\") {\n\t\treturn true\n\t} else if strings.Contains(key, \"=\") {\n\t\treturn true\n\t} else {\n\t\treturn false\n\t}\n}\n\nfunc (tmp *tmpParamInst) attachKeyParam(whatever cmdorsubcmd) {\n\tvar haskvparam, hasboolparam bool\n\tswitch whatever.(type) {\n\tcase *SubCmdInst:\n\t\ti := whatever.(*SubCmdInst)\n\t\thaskvparam = i.subCmd.HasKVParam(tmp.key)\n\t\thasboolparam = i.subCmd.HasBoolParam(tmp.key)\n\tcase *CmdInst:\n\t\ti := whatever.(*CmdInst)\n\t\thaskvparam = i.cmd.HasKVParam(tmp.key)\n\t\thasboolparam = i.cmd.HasBoolParam(tmp.key)\n\t}\n\n\tif haskvparam {\n\t\tp := whatever.GetKVParam(tmp.key)\n\t\tpi := KVParamInst{\n\t\t\targ:        tmp.arg,\n\t\t\tcmdinst:    tmp.cmdinst,\n\t\t\tfound:      tmp.found,\n\t\t\tkey:        tmp.key,\n\t\t\tparam:      p,\n\t\t\tsubcmdinst: tmp.subcmdinst,\n\t\t\tvalue:      tmp.value,\n\t\t}\n\n\t\tswitch whatever.(type) {\n\t\tcase *CmdInst:\n\t\t\tci := whatever.(*CmdInst)\n\t\t\tci.kvparaminsts = append(ci.ListKVParamInsts(), &pi)\n\t\tcase *SubCmdInst:\n\t\t\tsci := whatever.(*SubCmdInst)\n\t\t\tsci.kvparaminsts = append(sci.ListKVParamInsts(), &pi)\n\t\t}\n\t} else if hasboolparam {\n\t\tvar val bool\n\t\tvar err error\n\t\t// a provided flag with an empty value is true\n\t\tif tmp.found && tmp.value == \"\" {\n\t\t\tval = true\n\t\t} else {\n\t\t\tval, err = strconv.ParseBool(tmp.value)\n\t\t\tif err != nil {\n\t\t\t\tlog.Panicf(\"invalid bool value %q for key %q\", tmp.value, tmp.key)\n\t\t\t}\n\t\t}\n\n\t\tp := whatever.GetBoolParam(tmp.key)\n\t\tpi := BoolParamInst{\n\t\t\targ:        tmp.arg,\n\t\t\tcmdinst:    tmp.cmdinst,\n\t\t\tfound:      tmp.found,\n\t\t\tkey:        tmp.key,\n\t\t\tparam:      p,\n\t\t\tsubcmdinst: tmp.subcmdinst,\n\t\t\tvalue:      val,\n\t\t}\n\n\t\tswitch whatever.(type) {\n\t\tcase *CmdInst:\n\t\t\tci := whatever.(*CmdInst)\n\t\t\tci.boolparaminsts = append(ci.ListBoolParamInsts(), &pi)\n\t\tcase *SubCmdInst:\n\t\t\tsci := whatever.(*SubCmdInst)\n\t\t\tsci.boolparaminsts = append(sci.ListBoolParamInsts(), &pi)\n\t\t}\n\t} else {\n\t\tlog.Panicf(\"BUG: arg %q does not have a matching parameter for key %q\", tmp.arg, tmp.key)\n\t}\n}\n\nfunc (tmp *tmpParamInst) findAndAttachKeyParam(sub *SubCmdInst) {\n\tif sub.HasBoolParam(tmp.key) || sub.HasKVParam(tmp.key) {\n\t\ttmp.attachKeyParam(sub)\n\t} else if sub.subCmdInst != nil {\n\t\ttmp.findAndAttachKeyParam(sub.subCmdInst)\n\t}\n}\n\n// listSubCmdInst returns a list of subcommand instances from the command line\n// in natural order, e.g. \"cmd sub1 -f sub2 -x sub3 -y\" => [sub1, sub2, sub3]\nfunc (c *CmdInst) listSubCmdInst() []*SubCmdInst {\n\tvar i int\n\tout := make([]*SubCmdInst, 0)\n\n\tif c.subCmdInst == nil {\n\t\treturn out\n\t}\n\n\tout = append(out, c.subCmdInst)\n\n\tfor {\n\t\tif out[i].subCmdInst != nil {\n\t\t\tout = append(out, out[i].subCmdInst)\n\t\t\ti++\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn out\n}\n\n// HasSubCmdToken returns whether or not the proivded token is defined as a subcommand.\nfunc (c *Cmd) HasSubCmdToken(token string) bool {\n\tif c == nil {\n\t\treturn false\n\t}\n\n\tfor _, sc := range c.ListSubCmds() {\n\t\tif token == sc.token {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// HasKeyParam returns true if there are any parameters defined with\n// the provided key of either key type (bool or kv).\nfunc (c *Cmd) HasKeyParam(key string) bool {\n\tif c == nil {\n\t\treturn false\n\t}\n\n\tfor _, p := range c._boolparams() {\n\t\tif key == p.key {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tfor _, p := range c._kvparams() {\n\t\tif key == p.key {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\n// ListNamedParams returns a list of all parameters via the interface NamedParam.\n// Mainly for use in printing options, etc..\nfunc (c *Cmd) ListNamedParams() []NamedParam {\n\tout := make([]NamedParam, 0)\n\n\tfor _, p := range c._boolparams() {\n\t\tout = append(out, p)\n\t}\n\n\tfor _, p := range c._kvparams() {\n\t\tout = append(out, p)\n\t}\n\n\t// use 2 passes to append idx parameters in order\n\tipm := c._idxparams()\n\tips := make([]*IdxParam, len(ipm))\n\tfor _, p := range c._idxparams() {\n\t\tips[p.idx] = p\n\t}\n\tfor _, p := range ips {\n\t\tout = append(out, p)\n\t}\n\n\treturn out\n}\n\n// SubCmdToken returns the subcommand's token string. Returns empty string\n// if there is no subcommand.\nfunc (c *CmdInst) SubCmdToken() string {\n\tif c.subCmdInst != nil {\n\t\treturn c.subCmdInst.subCmd.token\n\t}\n\n\treturn \"\"\n}\n\nfunc (c *SubCmdInst) SubCmdToken() string {\n\tif c.subCmdInst != nil {\n\t\treturn c.subCmdInst.subCmd.token\n\t}\n\n\treturn \"\"\n}\n\nfunc (c *CmdInst) SubCmdInst() *SubCmdInst {\n\treturn c.subCmdInst\n}\n\nfunc (c *CmdInst) HasKVParamInst(key string) bool {\n\tfor _, p := range c.ListKVParamInsts() {\n\t\tif p.key == key {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *CmdInst) HasKVParam(key string) bool {\n\treturn c.cmd.HasKVParam(key)\n}\n\nfunc (c *SubCmdInst) HasKVParam(key string) bool {\n\treturn c.subCmd.HasKVParam(key)\n}\n\nfunc (c *CmdInst) HasBoolParamInst(key string) bool {\n\tfor _, p := range c.ListBoolParamInsts() {\n\t\tif p.key == key {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *CmdInst) HasBoolParam(key string) bool {\n\treturn c.cmd.HasBoolParam(key)\n}\n\nfunc (c *CmdInst) HasIdxParamInst(idx int) bool {\n\tipis := c.mapIdxParamInsts()\n\t_, exists := ipis[idx]\n\treturn exists\n}\n\nfunc (c *CmdInst) HasIdxParam(idx int) bool {\n\treturn c.cmd.HasIdxParam(idx)\n}\n\nfunc (c *SubCmdInst) HasIdxParam(idx int) bool {\n\treturn c.subCmd.HasIdxParam(idx)\n}\n\n// GetKVParamInst gets a key/value parameter instance by its key.\nfunc (c *CmdInst) GetKVParamInst(key string) *KVParamInst {\n\tfor _, p := range c.ListKVParamInsts() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tif c.HasKVParam(key) {\n\t\t// not provided, return empty value with found: false\n\t\treturn &KVParamInst{\n\t\t\tparam:   c.GetKVParam(key),\n\t\t\tkey:     key,\n\t\t\tcmdinst: c,\n\t\t}\n\t} else {\n\t\tpanic(\"BUG: invalid KVParam key '\" + key + \"'\")\n\t}\n\n}\n\n// GetKVParamInst gets a key/value parameter instance by its key.\nfunc (c *SubCmdInst) GetKVParamInst(key string) *KVParamInst {\n\tfor _, p := range c.ListKVParamInsts() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tif c.HasKVParam(key) {\n\t\t// not provided, return empty value with found: false\n\t\treturn &KVParamInst{\n\t\t\tparam:      c.GetKVParam(key),\n\t\t\tkey:        key,\n\t\t\tcmdinst:    &c.CmdInst,\n\t\t\tsubcmdinst: c,\n\t\t}\n\t} else {\n\t\tpanic(\"BUG: invalid KVParam key '\" + key + \"'\")\n\t}\n}\n\nfunc (c *CmdInst) GetKVParam(key string) *KVParam {\n\tfor _, p := range c.cmd._kvparams() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\nfunc (c *SubCmdInst) GetKVParam(key string) *KVParam {\n\tfor _, p := range c.subCmd._kvparams() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tpanic(\"BUG: refusing to return nil\" + key)\n}\n\n// GetBoolParamInst gets a key/value parameter instance by its key.\nfunc (c *CmdInst) GetBoolParamInst(key string) *BoolParamInst {\n\tfor _, p := range c.ListBoolParamInsts() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\t// not provided, return empty value with found: false\n\tif c.HasBoolParam(key) {\n\t\treturn &BoolParamInst{\n\t\t\tparam:   c.GetBoolParam(key),\n\t\t\tkey:     key,\n\t\t\tcmdinst: c,\n\t\t}\n\t} else {\n\t\tpanic(\"BUG: invalid BoolParam key '\" + key + \"'\")\n\t}\n\n}\n\n// GetBoolParamInst gets a key/value parameter instance by its key.\nfunc (c *SubCmdInst) GetBoolParamInst(key string) *BoolParamInst {\n\tfor _, p := range c.ListBoolParamInsts() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\t// not provided, return empty value with found: false\n\tif c.HasBoolParam(key) {\n\t\treturn &BoolParamInst{\n\t\t\tparam:      c.GetBoolParam(key),\n\t\t\tkey:        key,\n\t\t\tcmdinst:    &c.CmdInst,\n\t\t\tsubcmdinst: c,\n\t\t}\n\t} else {\n\t\tpanic(\"BUG: invalid BoolParam key '\" + key + \"'\")\n\t}\n}\n\nfunc (c *CmdInst) GetBoolParam(key string) *BoolParam {\n\tfor _, p := range c.cmd._boolparams() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\nfunc (c *SubCmdInst) GetBoolParam(key string) *BoolParam {\n\tfor _, p := range c.subCmd._boolparams() {\n\t\tif p.key == key {\n\t\t\treturn p\n\t\t}\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\n// GetIdxParamInst gets a positional parameter instance by its index.\nfunc (c *CmdInst) GetIdxParamInst(idx int) *IdxParamInst {\n\tipis := c.mapIdxParamInsts()\n\tif p, exists := ipis[idx]; exists {\n\t\treturn p\n\t}\n\n\t// not provided, return empty value with found: false\n\tif c.HasIdxParam(idx) {\n\t\treturn &IdxParamInst{\n\t\t\tparam:   c.GetIdxParam(idx),\n\t\t\tidx:     idx,\n\t\t\tcmdinst: c,\n\t\t}\n\t} else {\n\t\tpanic(fmt.Sprintf(\"BUG: invalid IdxParam index: %d\", idx))\n\t}\n}\n\n// GetIdxParamInst gets an indexed parameter instance by its index.\nfunc (c *SubCmdInst) GetIdxParamInst(idx int) *IdxParamInst {\n\tipis := c.mapIdxParamInsts()\n\tif p, exists := ipis[idx]; exists {\n\t\treturn p\n\t}\n\n\t// not provided, return empty value with found: false\n\tif c.HasIdxParam(idx) {\n\t\treturn &IdxParamInst{\n\t\t\tparam:      c.GetIdxParam(idx),\n\t\t\tidx:        idx,\n\t\t\tcmdinst:    &c.CmdInst,\n\t\t\tsubcmdinst: c,\n\t\t}\n\t} else {\n\t\tpanic(fmt.Sprintf(\"BUG: invalid IdxParam index: %d\", idx))\n\t}\n}\n\n// GetIdxParamInsByNamet gets an indexed parameter instance by its name.\nfunc (c *CmdInst) GetIdxParamInstByName(name string) *IdxParamInst {\n\tips := c.cmd._idxparams()\n\tfor _, p := range ips {\n\t\tif p.name == name {\n\t\t\treturn c.GetIdxParamInst(p.idx)\n\t\t}\n\t}\n\n\tpanic(\"BUG: No indexed parameter with name: \" + name)\n}\n\n// GetIdxParamInstByName gets an indexed parameter instance by its name.\nfunc (c *SubCmdInst) GetIdxParamInstByName(name string) *IdxParamInst {\n\tips := c.subCmd._idxparams()\n\tfor _, p := range ips {\n\t\tif p.name == name {\n\t\t\treturn c.GetIdxParamInst(p.idx)\n\t\t}\n\t}\n\n\tpanic(\"BUG: No indexed parameter with name: \" + name)\n}\n\nfunc (c *CmdInst) GetIdxParam(idx int) *IdxParam {\n\tips := c.cmd._idxparams()\n\tif p, exists := ips[idx]; exists {\n\t\treturn p\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\nfunc (c *SubCmdInst) GetIdxParam(idx int) *IdxParam {\n\tips := c.subCmd._idxparams()\n\tif p, exists := ips[idx]; exists {\n\t\treturn p\n\t}\n\n\tpanic(\"BUG: refusing to return nil\")\n}\n\nfunc (c *CmdInst) appendKVParamInst(pi *KVParamInst) {\n\tc.kvparaminsts = append(c.ListKVParamInsts(), pi)\n}\n\nfunc (c *CmdInst) appendBoolParamInst(pi *BoolParamInst) {\n\tc.boolparaminsts = append(c.ListBoolParamInsts(), pi)\n}\n\nfunc (c *CmdInst) appendIdxParamInst(pi *IdxParamInst) {\n\tipis := c.mapIdxParamInsts()\n\tipis[pi.idx] = pi\n}\n\n// ListKVParamInsts initializes the kvparaminsts list on the fly and returns it.\nfunc (c *CmdInst) ListKVParamInsts() []*KVParamInst {\n\tif c.kvparaminsts == nil {\n\t\tc.kvparaminsts = make([]*KVParamInst, 0)\n\t}\n\n\treturn c.kvparaminsts\n}\n\n// ListBoolParamInsts initializes the boolparaminsts list on the fly and returns it.\nfunc (c *CmdInst) ListBoolParamInsts() []*BoolParamInst {\n\tif c.boolparaminsts == nil {\n\t\tc.boolparaminsts = make([]*BoolParamInst, 0)\n\t}\n\n\treturn c.boolparaminsts\n}\n\n// mapIdxParamInsts initializes the idxparaminsts list on the fly and returns it.\nfunc (c *CmdInst) mapIdxParamInsts() map[int]*IdxParamInst {\n\tif c.idxparaminsts == nil {\n\t\tc.idxparaminsts = make(map[int]*IdxParamInst)\n\t}\n\n\treturn c.idxparaminsts\n}\n\nfunc (c *CmdInst) ListIdxParamInsts() []*IdxParamInst {\n\tipis := c.mapIdxParamInsts()\n\tout := make([]*IdxParamInst, len(ipis))\n\n\tfor i, pi := range ipis {\n\t\tout[i] = pi\n\t}\n\n\treturn out\n}\n\n// Remainder initializes the remainder list on the fly and returns it.\nfunc (c *CmdInst) Remainder() []string {\n\tif c.remainder == nil {\n\t\tc.remainder = make([]string, 0)\n\t}\n\n\treturn c.remainder\n}\n\n// Aliases initializes the aliases list on the fly and returns it.\nfunc (p *KVParam) Aliases() []string {\n\tif p.aliases == nil {\n\t\tp.aliases = make([]string, 0)\n\t}\n\n\treturn p.aliases\n}\n\nfunc (p *KVParamInst) Value() string {\n\treturn p.value\n}\n\nfunc (p *BoolParamInst) Value() bool {\n\treturn p.value\n}\n\nfunc (p *IdxParamInst) Value() string {\n\treturn p.value\n}\n\n// Name returns the key string. Mostly for use in printing errors, etc.\n// Implements NamedParam.\nfunc (p *KVParamInst) Name() string {\n\treturn p.key\n}\n\n// Name returns the key string. Mostly for use in printing errors, etc.\n// Implements NamedParam.\nfunc (p *BoolParamInst) Name() string {\n\treturn p.key\n}\n\n// Name returns the name given to the indexed param.\n// Implements NamedParam.\nfunc (p *IdxParamInst) Name() string {\n\treturn p.name\n}\n\n// String returns the value as a string.\nfunc (p *KVParamInst) String() (string, error) {\n\tif !p.found && p.param.required {\n\t\treturn \"\", RequiredParamNotFound{p.param}\n\t}\n\n\treturn p.value, nil\n}\n\n// String returns the value as a string.\nfunc (p *BoolParamInst) String() (string, error) {\n\tif !p.found && p.param.required {\n\t\treturn \"\", RequiredParamNotFound{p.param}\n\t}\n\n\tif p.value {\n\t\treturn \"true\", nil\n\t} else {\n\t\treturn \"false\", nil\n\t}\n}\n\n// String returns the value as a string.\nfunc (p *IdxParamInst) String() (string, error) {\n\tif !p.found && p.param.required {\n\t\treturn \"\", RequiredParamNotFound{p.param}\n\t}\n\n\treturn p.value, nil\n}\n\n// intParam returns the value as an int. If the param is required and it was\n// not set, RequiredParamNotFound is returned. Additionally, any errors in\n// conversion are returned.\nfunc intParam(p stringValuedParamInst) (int, error) {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn 0, RequiredParamNotFound{p.errParam()}\n\t\t} else {\n\t\t\treturn 0, nil\n\t\t}\n\t}\n\n\tval, err := strconv.ParseInt(p.Value(), 10, 64)\n\treturn int(val), err // warning: doesn't handle overflow\n}\n\nfunc (p *KVParamInst) Int() (int, error) {\n\treturn intParam(p)\n}\n\nfunc (p *IdxParamInst) Int() (int, error) {\n\treturn intParam(p)\n}\n\n// Float returns the value of the parameter as a float. If the value cannot\n// be converted, an error will be returned. See: strconv.ParseFloat\nfunc floatParam(p stringValuedParamInst) (float64, error) {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn 0, RequiredParamNotFound{p.errParam()}\n\t\t} else {\n\t\t\treturn 0, nil\n\t\t}\n\t}\n\n\treturn strconv.ParseFloat(p.Value(), 64)\n}\n\nfunc (p *KVParamInst) Float() (float64, error) {\n\treturn floatParam(p)\n}\n\nfunc (p *IdxParamInst) Float() (float64, error) {\n\treturn floatParam(p)\n}\n\n// Bool returns the value of the parameter as a bool.\n// If the value is required and not set, returns RequiredParamNotFound.\n// If the value cannot be converted, an error will be returned.\n// See: strconv.ParseBool\nfunc boolParam(p stringValuedParamInst) (bool, error) {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn false, RequiredParamNotFound{p.errParam()}\n\t\t} else {\n\t\t\treturn false, nil\n\t\t}\n\t}\n\n\tstripped := strings.Trim(p.Value(), `'\"`)\n\treturn strconv.ParseBool(stripped)\n}\n\nfunc (p *KVParamInst) Bool() (bool, error) {\n\treturn boolParam(p)\n}\n\nfunc (p *IdxParamInst) Bool() (bool, error) {\n\treturn boolParam(p)\n}\n\n// Duration returns the value of the parameter as a Go time.Duration.\n// Day and Week (e.g. \"1w\", \"1d\") are converted to 168 and 24 hours respectively.\n// If the value is required and not set, returns RequiredParamNotFound.\n// If the value cannot be converted, an error will be returned.\n// See: time.ParseDuration\nfunc durationParam(p stringValuedParamInst) (time.Duration, error) {\n\tduration := p.Value()\n\tempty := time.Duration(0)\n\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn empty, RequiredParamNotFound{p.errParam()}\n\t\t} else {\n\t\t\treturn empty, nil\n\t\t}\n\t}\n\n\tif strings.HasSuffix(duration, \"w\") {\n\t\tweeks, err := strconv.Atoi(strings.TrimSuffix(duration, \"w\"))\n\t\tif err != nil {\n\t\t\treturn empty, fmt.Errorf(\"Could not convert duration %q: %s\", duration, err)\n\t\t}\n\n\t\treturn time.Hour * time.Duration(weeks*24*7), nil\n\t} else if strings.HasSuffix(duration, \"d\") {\n\t\tdays, err := strconv.Atoi(strings.TrimSuffix(duration, \"d\"))\n\t\tif err != nil {\n\t\t\treturn empty, fmt.Errorf(\"Could not convert duration %q: %s\", duration, err)\n\t\t}\n\t\treturn time.Hour * time.Duration(days*24), nil\n\t} else {\n\t\treturn time.ParseDuration(duration)\n\t}\n}\n\nfunc (p *KVParamInst) Duration() (time.Duration, error) {\n\treturn durationParam(p)\n}\n\nfunc (p *IdxParamInst) Duration() (time.Duration, error) {\n\treturn durationParam(p)\n}\n\n// Time returns the value of the parameter as a Go time.Time.\n// Many formats are attempted before giving up.\n// If the value is required and not set, returns RequiredParamNotFound.\n// If the value cannot be converted, an error will be returned.\n// See: TimeFormats in this package\n// See: time.ParseDuration\nfunc timeParam(p stringValuedParamInst) (time.Time, error) {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn time.Time{}, RequiredParamNotFound{p.errParam()}\n\t\t} else {\n\t\t\treturn time.Time{}, nil\n\t\t}\n\t}\n\n\tt := p.Value()\n\n\t// convert Z suffix to +00:00\n\tif strings.HasSuffix(t, \"Z\") {\n\t\tt = strings.TrimSuffix(t, \"Z\") + \"+00:00\"\n\t}\n\n\t// try all of the formats\n\tfor _, fmt := range TimeFormats {\n\t\tout, err := time.Parse(fmt, t)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t} else {\n\t\t\treturn out, nil\n\t\t}\n\t}\n\n\treturn time.Time{}, UnsupportedTimeFormatError{t}\n}\n\nfunc (p *KVParamInst) Time() (time.Time, error) {\n\treturn timeParam(p)\n}\n\nfunc (p *IdxParamInst) Time() (time.Time, error) {\n\treturn timeParam(p)\n}\n\n// MustString returns the value as a string. If it was required/not-set,\n// panic ensues. Empty string may be returned for not-required+not-set.\nfunc (p *KVParamInst) MustString() string {\n\tout, err := p.String()\n\tif p.Required() && err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn out\n}\n\nfunc (p *IdxParamInst) MustString() string {\n\tout, err := p.String()\n\tif p.Required() && err != nil {\n\t\tpanic(err)\n\t}\n\n\treturn out\n}\n\n// DefString returns the value as a string. Rules:\n// If the param is required and it was not set, return the provided default.\n// If the param is not required and it was not set, return the empty string.\n// If the param is set and the value is \"*\", return the provided default.\n// If the param is set, return the value.\nfunc defStringParam(p stringValuedParamInst, def string) string {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\t// not set, required\n\t\t\treturn def\n\t\t} else {\n\t\t\t// not set, not required\n\t\t\treturn \"\"\n\t\t}\n\t} else if p.Value() == \"*\" {\n\t\treturn def\n\t}\n\n\tout, err := p.String()\n\tif err != nil {\n\t\treturn def\n\t}\n\treturn out\n}\n\nfunc (p *KVParamInst) DefString(def string) string {\n\treturn defStringParam(p, def)\n}\n\nfunc (p *IdxParamInst) DefString(def string) string {\n\treturn defStringParam(p, def)\n}\n\n// DefInt returns the value as an int. See DefString for the rules.\nfunc defIntParam(p stringValuedParamInst, def int) int {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn def\n\t\t} else {\n\t\t\treturn 0\n\t\t}\n\t} else if p.Value() == \"*\" {\n\t\treturn def\n\t}\n\n\tout, err := p.Int()\n\tif err != nil {\n\t\treturn def\n\t}\n\treturn out\n}\n\nfunc (p *KVParamInst) DefInt(def int) int {\n\treturn defIntParam(p, def)\n}\n\nfunc (p *IdxParamInst) DefInt(def int) int {\n\treturn defIntParam(p, def)\n}\n\n// DefFloat returns the value as a float. See DefString for the rules.\nfunc defFloatParam(p stringValuedParamInst, def float64) float64 {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn def\n\t\t} else {\n\t\t\treturn 0\n\t\t}\n\t} else if p.Value() == \"*\" {\n\t\treturn def\n\t}\n\n\tout, err := p.Float()\n\tif err != nil {\n\t\treturn def\n\t}\n\treturn out\n}\n\n// DefBool returns the value as a bool. See DefString for the rules.\nfunc defBoolParam(p stringValuedParamInst, def bool) bool {\n\tif !p.Found() {\n\t\tif p.Required() {\n\t\t\treturn def\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t} else if p.Value() == \"*\" {\n\t\treturn def\n\t}\n\n\tout, err := p.Bool()\n\tif err != nil {\n\t\treturn def\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "hal/cmd_test.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestCmd(t *testing.T) {\n\t// example 1 - smoke test\n\toc := NewCmd(\"oncall\", false).\n\t\tSetUsage(\"search Pagerduty escalation policies for a string\")\n\toc.AddSubCmd(\"cache-status\")\n\toc.AddSubCmd(\"cache-interval\").AddIdxParam(0, \"interval\", true)\n\toc.AddSubCmd(\"help\").AddAlias(\"h\")\n\n\toc.GetSubCmd(\"cache-status\").SetUsage(\"check the status of the background caching job\")\n\toc.GetSubCmd(\"cache-interval\").SetUsage(\"set the background caching job interval\")\n\n\tvar res *CmdInst\n\tvar err error\n\t// make sure a command with no args doesn't blow up\n\tres, err = oc.Process([]string{\"!oncall\"})\n\tif err != nil {\n\t\tt.Fail()\n\t}\n\n\tres, err = oc.Process([]string{\"!oncall\", \"help\"})\n\tif err != nil {\n\t\tt.Fail()\n\t}\n\n\tres, err = oc.Process([]string{\"!oncall\", \"h\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t\tt.Fail()\n\t}\n\n\tres, err = oc.Process([]string{\"!oncall\", \"sre\"})\n\tif len(res.Remainder()) != 1 || res.Remainder()[0] != \"sre\" {\n\t\tt.Fail()\n\t}\n\n\tres, err = oc.Process([]string{\"!oncall\", \"cache-status\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t\tt.Fail()\n\t}\n\tif res.SubCmdToken() != \"cache-status\" {\n\t\tt.Fail()\n\t}\n\n\tres, err = oc.Process([]string{\"!oncall\", \"cache-interval\", \"1h\"})\n\tif err != nil {\n\t\tt.Error(err)\n\t\tt.Fail()\n\t}\n\tif res.SubCmdToken() != \"cache-interval\" {\n\t\tt.Fail()\n\t}\n\n\t// example 2\n\t// Alias: requiring explicit aliases instead of guessing seems right\n\tpc := NewCmd(\"prefs\", true)\n\tpc.AddSubCmd(\"set\").\n\t\tSetUsage(\"set a pref\").\n\t\tSubCmd().AddKVParam(\"key\", true).AddAlias(\"k\").SetUsage(\"ohai!\").\n\t\tSubCmd().AddKVParam(\"value\", true).AddAlias(\"v\").\n\t\tSubCmd().AddKVParam(\"room\", false).AddAlias(\"r\").\n\t\tSubCmd().AddKVParam(\"user\", false).AddAlias(\"u\").\n\t\tSubCmd().AddKVParam(\"broker\", false).AddAlias(\"b\")\n\n\tpc.AddSubCmd(\"get\").\n\t\tSubCmd().AddKVParam(\"key\", true).AddAlias(\"k\").\n\t\tSubCmd().AddKVParam(\"value\", true).AddAlias(\"v\").\n\t\tSubCmd().AddKVParam(\"room\", false).AddAlias(\"r\").\n\t\tSubCmd().AddKVParam(\"user\", false).AddAlias(\"u\").SetDefault(\"*\").\n\t\tSubCmd().AddKVParam(\"broker\", false).AddAlias(\"b\")\n\n\tpc.AddSubCmd(\"rm\").AddIdxParam(0, \"id\", true)\n\n\targv2 := strings.Split(\"prefs set --room * --user foo --broker console --key ohai --value nevermind\", \" \")\n\tres, err = pc.Process(argv2)\n\tif err != nil {\n\t\tt.Error(err)\n\t\tt.Fail()\n\t}\n\n\tif len(res.Remainder()) != 0 {\n\t\tt.Error(\"There should not be any remainder\")\n\t}\n\tif res.SubCmdToken() != \"set\" {\n\t\tt.Errorf(\"wrong subcommand. Expected %q, got %q\", \"set\", res.SubCmdToken())\n\t}\n\tif res.SubCmdInst() == nil {\n\t\tt.Error(\"result.SubCmdInst is nil when it should be an instance for 'set'\")\n\t\tt.FailNow()\n\t}\n\tsubcmd := res.SubCmdInst()\n\tif subcmd.GetKVParamInst(\"room\").MustString() != \"*\" {\n\t\tt.Errorf(\"wrong room, expected *, got %q\", subcmd.GetKVParamInst(\"room\").MustString())\n\t}\n\tif subcmd.GetKVParamInst(\"key\").MustString() != \"ohai\" {\n\t\tt.Errorf(\"wrong key, expected 'ohai', got %q\", subcmd.GetKVParamInst(\"key\").MustString())\n\t}\n\tif subcmd.GetKVParamInst(\"value\").MustString() != \"nevermind\" {\n\t\tt.Errorf(\"wrong value, expected 'nevermind', got %q\", subcmd.GetKVParamInst(\"value\").MustString())\n\t}\n\t// check that defaults are working\n\tdval := \"1234\"\n\trds := subcmd.GetKVParamInst(\"room\").DefString(dval)\n\tif rds != dval {\n\t\tt.Errorf(\"DefString returned %q, expected %q\", rds, dval)\n\t}\n\tirds := subcmd.GetKVParamInst(\"room\").DefInt(999)\n\tif irds != 999 {\n\t\tt.Errorf(\"DefString returned %d, expected 999\", irds)\n\t}\n\n\t// again with out-of-order parameters\n\targv3 := strings.Split(\"prefs --user bob --key testing get --value lol\", \" \")\n\tres, err = pc.Process(argv3)\n\tif err != nil {\n\t\tt.Error(err)\n\t\tt.Fail()\n\t}\n\tif len(res.Remainder()) != 0 {\n\t\tt.Error(\"There should not be any remainder\")\n\t}\n\tif res.SubCmdToken() != \"get\" {\n\t\tt.Errorf(\"wrong subcommand. Expected 'get', got %q\", res.SubCmdToken())\n\t}\n\tif res.SubCmdInst() == nil {\n\t\tt.Error(\"result.SubCmdInst is nil when it should be an instance for 'get'\")\n\t\tt.FailNow()\n\t}\n\tsubcmd = res.SubCmdInst()\n\tkvpi := subcmd.GetKVParamInst(\"key\")\n\tif kvpi == nil {\n\t\tt.Error(\"BUG: subcmd.GetKVParamInst('key') returned nil\")\n\t\tt.FailNow()\n\t}\n\tif kvpi.MustString() != \"testing\" {\n\t\tt.Errorf(\"wrong key, expected 'testing', got %q\", subcmd.GetKVParamInst(\"key\").MustString())\n\t}\n\n\targv4 := []string{\"!prefs\", \"rm\", \"4\"}\n\tres, err = pc.Process(argv4)\n\tif err != nil {\n\t\tt.Error(err)\n\t\tt.Fail()\n\t}\n\tif res.SubCmdToken() != \"rm\" {\n\t\tt.Errorf(\"Expected rm, got %q\", res.SubCmdToken())\n\t}\n\tpp := res.SubCmdInst().GetIdxParamInst(0)\n\tif pp.Value() != \"4\" {\n\t\tt.Errorf(\"wrong value from positional parameter. got %d, expected 4\", pp.idx)\n\t}\n\n\tdc := NewCmd(\"dc\", false)\n\tdc.AddKVParam(\"dc_required_kvparam_with_default\", true).SetDefault(\"this is the default\")\n\tsdc := dc.AddSubCmd(\"test\")\n\tsdc.AddKVParam(\"kvparam_with_default\", false).SetDefault(\"this is the default 1\")\n\tsdc.AddKVParam(\"required_kvparam_with_default\", true).SetDefault(\"this is the default 2\")\n\tsdc.AddKVParam(\"required_kvparam_without_default\", true)\n\tsdc.AddBoolParam(\"boolparam_with_default\", false).SetDefault(true)\n\tsdc.AddBoolParam(\"required_boolparam_with_default\", true).SetDefault(false)\n\tsdc.AddBoolParam(\"required_boolparam_without_default\", true)\n\n\tres, err = dc.Process([]string{\"dc\"})\n\tif err != nil {\n\t\tt.Errorf(\"command should parse ok with no arguments: %s\", err)\n\t\tt.Fail()\n\t}\n\n\tres, err = dc.Process([]string{\"dc\", \"--dc_required_kvparam_with_default\", \"whatever\"})\n\tif err != nil {\n\t\tt.Fail()\n\t}\n\n\tres, err = dc.Process([]string{\"dc\", \"test\"})\n\tif res != nil || err == nil {\n\t\tt.Errorf(\"subcommand should NOT parse ok with no arguments: %s\", err)\n\t\tt.Fail()\n\t}\n\n\tres, err = dc.Process([]string{\"dc\", \"test\", \"required_kvparam_without_default=yes\", \"--required_boolparam_without_default\"})\n\tif err != nil {\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "hal/counter.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\tdbsql \"database/sql\"\n)\n\nconst CounterTable = `\nCREATE TABLE IF NOT EXISTS counter (\n\t pkey    VARCHAR(191) NOT NULL,\n\t value   int,\n\t ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\t PRIMARY KEY(pkey)\n)`\n\nfunc GetCounter(key string) (value int, err error) {\n\tdb := SqlDB()\n\tSqlInit(CounterTable)\n\n\tsql := \"SELECT value FROM counter WHERE pkey=?\"\n\terr = db.QueryRow(sql, key).Scan(&value)\n\tif err == dbsql.ErrNoRows {\n\t\treturn 0, nil\n\t} else if err != nil {\n\t\tlog.Printf(\"GetCounter query failed: %s\", err)\n\t\treturn 0, err\n\t}\n\n\treturn value, nil\n}\n\nfunc SetCounter(key string, value int) error {\n\tdb := SqlDB()\n\tSqlInit(CounterTable)\n\n\tsql := `INSERT INTO counter (pkey,value) VALUES (?,?) ON DUPLICATE KEY UPDATE value=?`\n\n\t_, err := db.Exec(sql, key, value, value)\n\tif err != nil {\n\t\tlog.Printf(\"SetCounter upsert failed: %s\", err)\n\t}\n\n\treturn err\n}\n\nfunc IncrementCounter(key string) error {\n\tdb := SqlDB()\n\tSqlInit(CounterTable)\n\n\tsql := `INSERT INTO counter (pkey,value) VALUES (?,1) ON DUPLICATE KEY UPDATE value=value+1`\n\n\t_, err := db.Exec(sql, key)\n\tif err != nil {\n\t\tlog.Printf(\"IncrementCounter query failed: %s\", err)\n\t}\n\n\treturn err\n}\n\nfunc DecrementCounter(key string) error {\n\tdb := SqlDB()\n\tSqlInit(CounterTable)\n\n\tsql := `INSERT INTO counter (pkey,value) VALUES (?,-1) ON DUPLICATE KEY UPDATE value=value-1`\n\n\t_, err := db.Exec(sql, key)\n\tif err != nil {\n\t\tlog.Printf(\"DecrementCounter query failed: %s\", err)\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "hal/directory.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"sync\"\n\n\t\"github.com/juju/errors\"\n)\n\n// directory is a simple graph of directory-style information that can be linked\n// and queried on those links. Goals:\n// 1. avoid coupling between plugins\n// 2. make it easy to share data between plugins\n// 3. make it easier to link data across various systems (e.g. Pagerduty, company directory, Slack)\ntype directory struct {\n\tinitOnce sync.Once\n}\n\nconst DirNodeTable = `\nCREATE TABLE IF NOT EXISTS dir_node (\n\tpkey    VARCHAR(191) NOT NULL,\n\tkind    VARCHAR(191) NOT NULL,\n\tts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\tPRIMARY KEY(pkey, kind)\n)`\n\ntype DirNode struct {\n\tKey  string `json:\"key\"`\n\tKind string `json:\"kind\"`\n\tTs   int    `json:\"ts\"`\n}\n\nconst DirNodeAttrTable = `\nCREATE TABLE IF NOT EXISTS dir_node_attr (\n\tpkey    VARCHAR(191) NOT NULL,\n\tkind    VARCHAR(191) NOT NULL,\n\tattr    VARCHAR(191) NOT NULL,\n\tvalue   MEDIUMTEXT,\n\tts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\tPRIMARY KEY(pkey, kind, attr),\n\tINDEX (pkey, kind),\n\tFOREIGN KEY (pkey, kind) REFERENCES dir_node(pkey, kind) ON UPDATE CASCADE\n)`\n\ntype DirNodeAttr struct {\n\tKey   string `json:\"key\"`\n\tKind  string `json:\"kind\"`\n\tAttr  string `json:\"attr\"`\n\tValue string `json:\"value\"`\n\tTs    int    `json:\"ts\"`\n}\n\nconst DirEdgeTable = `\nCREATE TABLE IF NOT EXISTS dir_edge (\n\tkeyA    VARCHAR(191) NOT NULL,\n\tkindA   VARCHAR(191) NOT NULL,\n\tkeyB    VARCHAR(191) NOT NULL,\n\tkindB   VARCHAR(191) NOT NULL,\n\tts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\tPRIMARY KEY(keyA, kindA, keyB, kindB),\n\tINDEX (keyA, kindA),\n\tINDEX (keyB, kindB),\n\tFOREIGN KEY (keyA, kindA) REFERENCES dir_node(pkey, kind) ON UPDATE CASCADE,\n\tFOREIGN KEY (keyB, kindB) REFERENCES dir_node(pkey, kind) ON UPDATE CASCADE\n)`\n\ntype DirEdge struct {\n\tKeyA  string `json:\"key_a\"`\n\tKindA string `json:\"kind_a\"`\n\tKeyB  string `json:\"key_b\"`\n\tKindB string `json:\"kind_b\"`\n\tTs    int    `json:\"ts\"`\n}\n\nvar dirSingleton directory\n\nfunc Directory() *directory {\n\tdirSingleton.initOnce.Do(func() {\n\t\tSqlInit(DirNodeTable)\n\t\tSqlInit(DirNodeAttrTable)\n\t\tSqlInit(DirEdgeTable)\n\t})\n\n\treturn &dirSingleton\n}\n\nfunc (dir *directory) exec(sql string, params ...interface{}) error {\n\tdb := SqlDB()\n\n\t_, err := db.Exec(sql, params...)\n\tif err != nil {\n\t\treturn errors.Annotatef(err, \"SQL: %q, Values: %+q\", sql, params)\n\t}\n\n\treturn nil\n}\n\nfunc (dir *directory) query(sql string, params ...interface{}) ([][]string, error) {\n\tdb := SqlDB()\n\tout := make([][]string, 0)\n\n\trows, err := db.Query(sql, params...)\n\tif err != nil {\n\t\treturn out, errors.Annotatef(err, \"SQL: %q, Values: %+q\", sql, params)\n\t}\n\n\tdefer rows.Close()\n\n\tcols, err := rows.Columns()\n\tif err != nil {\n\t\treturn out, errors.Annotate(err, \"rows.Columns()\")\n\t}\n\n\tfor rows.Next() {\n\t\tirow := make([]interface{}, len(cols))\n\t\trow := make([]string, len(irow))\n\n\t\tfor i, _ := range irow {\n\t\t\tirow[i] = &row[i]\n\t\t}\n\n\t\terr := rows.Scan(irow...)\n\t\tif err != nil {\n\t\t\treturn out, errors.Annotate(err, \"rows.Scan()\")\n\t\t}\n\n\t\tout = append(out, row)\n\t}\n\n\treturn out, nil\n}\n\n// Put adds/updates a node for the given key/attr and creates edges for\n// the attributes where possible.\nfunc (dir *directory) Put(key, kind string, attrs map[string]string, edgeAttrs []string) error {\n\terr := dir.PutNode(key, kind)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif attrs == nil {\n\t\treturn errors.NotValidf(\"attrs cannot be nil\")\n\t}\n\n\tfor attr, value := range attrs {\n\t\terr := dir.PutNodeAttr(key, kind, attr, value)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// experimental: use the provided list of keys to try to create edges based on attributes\n\tfor _, ea := range edgeAttrs {\n\t\tif value, exists := attrs[ea]; exists {\n\t\t\tneighbors, err := dir.GetAttrNodes(ea, value)\n\t\t\tif err != nil {\n\t\t\t\treturn errors.Annotate(err, \"GetAttrNodes failed\")\n\t\t\t}\n\t\t\tfor _, neighbor := range neighbors {\n\t\t\t\tdir.PutEdge(key, kind, neighbor[0], neighbor[1])\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (dir *directory) PutNode(key, kind string) error {\n\tsql := `INSERT INTO dir_node (pkey, kind) VALUES (?, ?) ON DUPLICATE KEY UPDATE ts=NOW()`\n\treturn dir.exec(sql, key, kind)\n}\n\nfunc (dir *directory) HasNode(key, kind string) (bool, error) {\n\tsql := `SELECT pkey, kind FROM dir_node WHERE pkey=? AND kind=?`\n\n\tdata, err := dir.query(sql, &key, &kind)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif len(data) > 0 {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc (dir *directory) DelNode(key, kind string) error {\n\tsql := `DELETE FROM dir_node WHERE key=? AND kind=?`\n\treturn dir.exec(sql, key, kind)\n}\n\nfunc (dir *directory) PutNodeAttr(key, kind, attr, value string) error {\n\tsql := `INSERT INTO dir_node_attr (pkey, kind, attr, value) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE value=?, ts=NOW()`\n\treturn dir.exec(sql, key, kind, attr, value, value)\n}\n\n// GetAttrNodes takes an attribute and value and returns a list of nodes\n// that have (exactly) matching attributes.\nfunc (dir *directory) GetAttrNodes(attr, value string) ([][2]string, error) {\n\tout := make([][2]string, 0)\n\tsql := `SELECT pkey, kind FROM dir_node_attr WHERE attr=? AND value=? GROUP BY pkey, kind`\n\n\tdata, err := dir.query(sql, &attr, &value)\n\tif err != nil {\n\t\treturn out, err\n\t}\n\n\tfor _, item := range data {\n\t\t// item is []string, return must be [2]string\n\t\tout = append(out, [2]string{item[0], item[1]})\n\t}\n\n\treturn out, nil\n}\n\nfunc (dir *directory) HasEdge(keyA, kindA, keyB, kindB string) (bool, error) {\n\tsql := `SELECT \"y\" FROM dir_edge WHERE keyA=? AND kindA=? AND keyB=? AND kindB=?`\n\n\tdata, err := dir.query(sql, &keyA, &kindA, &keyB, &kindB)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tif len(data) > 0 {\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc (dir *directory) PutEdge(keyA, kindA, keyB, kindB string) error {\n\tsql := `INSERT INTO dir_edge (keyA, kindA, keyB, kindB) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE ts=NOW()`\n\treturn dir.exec(sql, keyA, kindA, keyB, kindB)\n}\n\nfunc (dir *directory) DelEdge(keyA, kindA, keyB, kindB string) error {\n\tsql := `DELETE FROM dir_edge WHERE keyA=? AND kindA=? AND keyB=? AND kindB=?`\n\treturn dir.exec(sql, keyA, kindA, keyB, kindB)\n}\n\nfunc (dir *directory) GetNeighbors(key, kind string) ([][2]string, error) {\n\tout := make([][2]string, 0)\n\n\tsql := `SELECT keyA, kindA, keyB, kindB FROM dir_edge WHERE (keyA=? AND kindA=?) OR (keyB=? AND kindB=?)`\n\n\tedges, err := dir.query(sql, &key, &kind, &key, &kind)\n\tif err != nil {\n\t\treturn out, err\n\t}\n\n\tfor _, e := range edges {\n\t\tif e[0] == key && e[1] == kind {\n\t\t\tout = append(out, [2]string{e[2], e[3]})\n\t\t} else {\n\t\t\tout = append(out, [2]string{e[0], e[1]})\n\t\t}\n\t}\n\n\treturn out, nil\n}\n\nfunc (dir *directory) GetEdges() ([]DirEdge, error) {\n\tsql := `SELECT keyA, kindA, keyB, kindB, UNIX_TIMESTAMP(ts) FROM dir_edge WHERE keyA != '' AND kindA != '' AND keyB != '' AND kindB != ''`\n\tdb := SqlDB()\n\tout := make([]DirEdge, 0)\n\n\trows, err := db.Query(sql)\n\tif err != nil {\n\t\treturn out, errors.Annotatef(err, \"SQL: %q\", sql)\n\t}\n\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trow := DirEdge{}\n\t\terr := rows.Scan(&row.KeyA, &row.KindA, &row.KeyB, &row.KindB, &row.Ts)\n\t\tif err != nil {\n\t\t\treturn out, errors.Annotate(err, \"rows.Scan()\")\n\t\t}\n\n\t\tout = append(out, row)\n\t}\n\n\treturn out, nil\n}\n\nfunc (dir *directory) GetNodes() ([]DirNode, error) {\n\tsql := `SELECT pkey, kind, UNIX_TIMESTAMP(ts) FROM dir_node WHERE pkey != '' AND kind != ''`\n\tdb := SqlDB()\n\tout := make([]DirNode, 0)\n\n\trows, err := db.Query(sql)\n\tif err != nil {\n\t\treturn out, errors.Annotatef(err, \"SQL: %q\", sql)\n\t}\n\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trow := DirNode{}\n\t\terr := rows.Scan(&row.Key, &row.Kind, &row.Ts)\n\t\tif err != nil {\n\t\t\treturn out, errors.Annotate(err, \"rows.Scan()\")\n\t\t}\n\n\t\tout = append(out, row)\n\t}\n\n\treturn out, nil\n}\n\nfunc (dir *directory) GetNodeAttrs() ([]DirNodeAttr, error) {\n\tsql := `SELECT pkey, kind, attr, value, UNIX_TIMESTAMP(ts) FROM dir_node_attr WHERE pkey != '' AND kind != '' AND attr != ''`\n\tdb := SqlDB()\n\tout := make([]DirNodeAttr, 0)\n\n\trows, err := db.Query(sql)\n\tif err != nil {\n\t\treturn out, errors.Annotatef(err, \"SQL: %q\", sql)\n\t}\n\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trow := DirNodeAttr{}\n\t\terr := rows.Scan(&row.Key, &row.Kind, &row.Attr, &row.Value, &row.Ts)\n\t\tif err != nil {\n\t\t\treturn out, errors.Annotate(err, \"rows.Scan()\")\n\t\t}\n\n\t\tout = append(out, row)\n\t}\n\n\treturn out, nil\n}\n\n/* test data\n\nINSERT INTO dir_node (kind, pkey) VALUES (\"AD\", \"angua@dwmail.com\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"AD\", \"carrot@dwmail.com\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"AD\", \"aching@dwmail.com\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"AD\", \"granny@dwmail.com\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"AD\", \"vetinari@dwmail.com\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"AD\", \"vimes@dwmail.com\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"AD\", \"nobbs@dwmail.com\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"slack\", \"Angua\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"slack\", \"Carrot\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"slack\", \"Tiffany\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"slack\", \"Mistress Weatherwax\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"slack\", \"Patrician\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"slack\", \"Sam\");\nINSERT INTO dir_node (kind, pkey) VALUES (\"slack\", \"Nobby\");\n\nINSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES (\"AD\", \"angua@dwmail.com\", \"slack\", \"Angua\");\nINSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES (\"AD\", \"carrot@dwmail.com\", \"slack\", \"Carrot\");\nINSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES (\"AD\", \"aching@dwmail.com\", \"slack\", \"Tiffany\");\nINSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES (\"AD\", \"granny@dwmail.com\", \"slack\", \"Mistress Weatherwax\");\nINSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES (\"AD\", \"vetinari@dwmail.com\", \"slack\", \"Patrician\");\nINSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES (\"AD\", \"vimes@dwmail.com\", \"slack\", \"Sam\");\nINSERT INTO dir_edge (kindA, keyA, kindB, keyB) VALUES (\"AD\", \"nobbs@dwmail.com\", \"slack\", \"Nobby\");\n\nINSERT INTO dir_node_attr (kind, pkey, attr, value) VALUES (\"AD\", \"angua@dwmail.com\", \"email\", \"angua@dwmail.com\");\nINSERT INTO dir_node_attr (kind, pkey, attr, value) VALUES (\"AD\", \"angua@dwmail.com\", \"sms\", \"5551234567\"\nINSERT INTO dir_node_attr (kind, pkey, attr, value) VALUES (\"AD\", \"carrot@dwmail.com\", \"sms\", \"5555555555\");\n*/\n"
  },
  {
    "path": "hal/event.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Evt is a generic container for events processed by the bot.\n// Event sources are responsible for copying the appropriate data into\n// the Evt fields. Routing and most plugins will not work if the body\n// isn't copied, at a minimum.\n// When ToUser and ToRoom are both true, the event will be delivered twice.\n// The original event should usually be attached to the Original\ntype Evt struct {\n\tID        string       `json:\"id\"`      // ID for the event (assigned by upstream or broker)\n\tBody      string       `json:\"body\"`    // body of the event, regardless of source\n\tCommand   string       `json:\"command\"` // optional command associated with the body, typically empty\n\tSubject   string       `json:\"subject\"` // the subject of the message, if available, typically empty\n\tRoom      string       `json:\"room\"`    // the room where the event originated\n\tRoomId    string       `json:\"room_id\"` // the room id from the source broker\n\tUser      string       `json:\"user\"`    // the username that created the event\n\tUserId    string       `json:\"user_id\"` // the user id from the source broker\n\tTime      time.Time    `json:\"time\"`    // timestamp of the event\n\tBroker    Broker       `json:\"broker\"`  // the broker the event came from\n\tIsChat    bool         `json:\"is_chat\"` // lets the broker differentiate chats and other events\n\tIsBot     bool         `json:\"is_bot\"`  // message was generated by the bot\n\tToUser    bool         `json:\"to_user\"` // when true, always deliver outgoing event via DM\n\tToRoom    bool         `json:\"to_room\"` // when true, always deliver outgoing event to the room\n\tToFunc    bool         `json:\"to_func\"` // when true, call the ReplyFunc instead of the usual reply path\n\tReplyFunc func(string) // a function to be called with a reply rather than the usual process\n\tOriginal  interface{}  // the original message container (e.g. slack.MessageEvent)\n\tinstance  *Instance    // used by the broker to provide plugin instance metadata\n\tisReply   bool         // the message is a reply\n\tisDM      bool         // the message is/should be a DM\n\tisTable   bool         // the message is a table that should be rendered\n\toob       interface{}  // oob data that needs to flow between stages (e.g. tables)\n}\n\n// Clone() returns a copy of the event with the same broker/room/user\n// and a current timestamp. Body, Command, and Subject will be empty.\n// Time is updated to the current time.\n// Original is carried through, so nil that if you don't want it preserved.\nfunc (e *Evt) Clone() Evt {\n\tout := Evt{\n\t\tID:       e.ID,\n\t\tRoom:     e.Room,\n\t\tRoomId:   e.RoomId,\n\t\tUser:     e.User,\n\t\tUserId:   e.UserId,\n\t\tTime:     time.Now(),\n\t\tBroker:   e.Broker,\n\t\tIsChat:   e.IsChat,\n\t\tIsBot:    e.IsBot,\n\t\tOriginal: e.Original,\n\t\tisReply:  e.isReply,\n\t\tisDM:     e.isDM,\n\t\tisTable:  e.isTable,\n\t\t// do not preserve oob\n\t}\n\n\treturn out\n}\n\n// Reply is a helper that crafts a new event from the provided string\n// and initiates the reply on the broker attached to the event.\n// The message is routed according to preferences and the ToUser/ToRoom\n// fields on the event. If no preferences are set for the user/room/plugin\n// the response will go to the room where the command originated.\n// The \"reply-via-dm\" preference can be set to \"true\" to default to\n// having replies to to DM instead of the room.\nfunc (e *Evt) Reply(msg string) {\n\tvar delivered bool\n\n\tif e.ToFunc {\n\t\te.ReplyFunc(msg)\n\t\tdelivered = true\n\t}\n\n\tif e.ToRoom {\n\t\te.ReplyToRoom(msg)\n\t\tdelivered = true\n\t}\n\n\tif e.ToUser {\n\t\te.ReplyDM(msg)\n\t\tdelivered = true\n\t}\n\n\tif delivered {\n\t\treturn\n\t}\n\n\treplyVia := e.AsPref().FindKey(\"reply-via-dm\").One()\n\n\t// One() sets Success to false for no results.\n\tif replyVia.Success {\n\t\tif replyVia.Value == \"true\" {\n\t\t\te.ReplyDM(msg)\n\t\t\treturn\n\t\t}\n\t}\n\n\t// replyVia might be false (or invalid) in which case it falls through to here\n\te.ReplyToRoom(msg)\n}\n\n// Replyf is the same as Reply but allows for string formatting using\n// fmt.Sprintf()\nfunc (e *Evt) Replyf(msg string, a ...interface{}) {\n\te.Reply(fmt.Sprintf(msg, a...))\n}\n\n// Error replies to the event with the provided error.\n// Future: need to figure out if there's going to be a kind of error\n// handling module in Hal for making errors visible in a logging room,\n// possibly on a different broker...\nfunc (e *Evt) Error(err error) {\n\te.Reply(fmt.Sprintf(\"%s\", err))\n}\n\n// ReplyTable sends a table of data back, formatting it according to\n// preferences.\n// TODO: move code from brokers/slack/broker.go/SendTable here\n// TODO: document preferences here\nfunc (e *Evt) ReplyTable(hdr []string, rows [][]string) {\n\tout := e.Clone() // may not be necessary\n\n\tif e.Broker != nil {\n\t\te.Broker.SendTable(out, hdr, rows)\n\t} else {\n\t\tpanic(\"hal.Evt.ReplyTable called with nil Broker!\")\n\t}\n}\n\n// ReplyDM makes it convenient to reply to a user via DM. The user is drawn\n// from the event's UserId field and passed to the broker's SendDM() method.\nfunc (e *Evt) ReplyDM(msg string) {\n\tout := e.Clone()\n\tout.Body = msg\n\te.Broker.SendDM(out)\n}\n\n// ReplyToRoom crafts a new event from the provided string\n// and sends it to the room the event originated from.\nfunc (e *Evt) ReplyToRoom(msg string) {\n\tout := e.Clone()\n\tout.Body = msg\n\n\tif e.Broker != nil {\n\t\te.Broker.Send(out)\n\t} else {\n\t\tpanic(\"hal.Evt.Reply called with nil Broker!\")\n\t}\n}\n\n// BrokerName returns the text name of current broker.\nfunc (e *Evt) BrokerName() string {\n\treturn e.Broker.Name()\n}\n\n// FindPrefs fetches the union of all matching settings from the database\n// for user, broker, room, and plugin.\n// Plugins can use the Prefs methods to filter from there.\nfunc (e *Evt) FindPrefs() Prefs {\n\tbroker := e.BrokerName()\n\tplugin := e.instance.Plugin.Name\n\treturn FindPrefs(e.User, broker, e.RoomId, plugin, \"\")\n}\n\n// InstanceSettings gets all the settings matching the settings defined\n// by the plugin's Settings field.\nfunc (e *Evt) InstanceSettings() Prefs {\n\tbroker := e.BrokerName()\n\tplugin := e.instance.Plugin.Name\n\n\tout := make(Prefs, 0)\n\n\tfor _, stg := range e.instance.Plugin.Settings {\n\t\t// ignore room-specific settings for other rooms\n\t\tif stg.Room != \"\" && stg.Room != e.RoomId {\n\t\t\tcontinue\n\t\t}\n\n\t\tpref := GetPref(\"\", broker, e.RoomId, plugin, stg.Key, stg.Default)\n\t\tout = append(out, pref)\n\t}\n\n\treturn out\n}\n\n// AsPref returns a a pref with user, room, broker, and plugin set using data\n// from the event handle.\nfunc (e *Evt) AsPref() Pref {\n\t// AsPref can be called without an instance for errors, make sure\n\t// instance is set before accessing fields\n\tvar plugin string\n\tif e.instance != nil {\n\t\tplugin = e.instance.Plugin.Name\n\t}\n\n\tp := Pref{\n\t\tUser:   e.UserId,\n\t\tRoom:   e.RoomId,\n\t\tBroker: e.BrokerName(),\n\t\tPlugin: plugin,\n\t}\n\n\treturn p\n}\n\n// BodyAsArgv does minimal parsing of the event body, returning an argv-like\n// array of strings with quoted strings intact (but with quotes removed).\n// The goal is shell-like, and is not a full implementation.\n// Leading/trailing whitespace is removed.\n// Escaping quotes, etc. is not supported.\nfunc (e *Evt) BodyAsArgv() []string {\n\t// use a simple RE rather than pulling in a package to do this\n\tre := regexp.MustCompile(`'[^']*'|\"[^\"]*\"|\\S+`)\n\tbody := strings.TrimSpace(e.Body)\n\targv := re.FindAllString(body, -1)\n\n\t// remove the outer quotes from quoted strings\n\tfor i, val := range argv {\n\t\tif strings.HasPrefix(val, `'`) && strings.HasSuffix(val, `'`) {\n\t\t\ttmp := strings.TrimPrefix(val, `'`)\n\t\t\targv[i] = strings.TrimSuffix(tmp, `'`)\n\t\t} else if strings.HasPrefix(val, `\"`) && strings.HasSuffix(val, `\"`) {\n\t\t\ttmp := strings.TrimPrefix(val, `\"`)\n\t\t\targv[i] = strings.TrimSuffix(tmp, `\"`)\n\t\t}\n\t}\n\n\treturn argv\n}\n\n// ForceToRoom clones the event and returns a copy with ToRoom set to true.\n// Takes priority over reply-via-dm routing.\n// Useful for chaining, e.g. evt.ToRoom().Replyf(\"go away!\").\nfunc (e *Evt) ForceToRoom() Evt {\n\tout := e.Clone()\n\tout.ToRoom = true\n\treturn out\n}\n\n// ForceToUser clones the event and returns a copy with ToUser set to true.\n// Takes priority over reply-via-dm routing.\n// Useful for chaining.\nfunc (e *Evt) ForceToUser() Evt {\n\tout := e.Clone()\n\tout.ToUser = true\n\treturn out\n}\n\nfunc (e *Evt) String() string {\n\treturn fmt.Sprintf(\"User: %q Room: %q Time: %q Body: %q\", e.User, e.Room, e.Time.String(), e.Body)\n}\n"
  },
  {
    "path": "hal/event_test.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n)\n\nfunc TestEvtBodyAsArgv(t *testing.T) {\n\tevt := Evt{}\n\tevt.Body = \"a simple flat test\"\n\targv := evt.BodyAsArgv()\n\n\tif len(argv) != 4 {\n\t\tfmt.Printf(\"expected 4 elements, got %d\", len(argv))\n\t\tt.Fail()\n\t}\n\n\t//            1     2      3    4            5     6              7                    8\n\tevt.Body = ` !foo --this -one \"is a little\" more (complicated) 'becuase of the quotes' OK`\n\targv = evt.BodyAsArgv()\n\n\tif len(argv) != 8 {\n\t\tfmt.Printf(\"expected 8 elements, got %d\", len(argv))\n\t\tt.Fail()\n\t}\n\n\t// leading/trailing whitespace should be stripped and embedded quotes\n\t// should be intact. Escaped quotes are not supported.\n\tevt.Body = `\t!bar \"perhaps 'this challenge' will\" '@%$*#@(**W(IOWIE-'------ break TEH BANK \"\" '' `\n\targv = evt.BodyAsArgv()\n\n\tif len(argv) != 9 {\n\t\tfmt.Printf(\"expected 9 elements, got %d\", len(argv))\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "hal/kv.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\tdbsql \"database/sql\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst KVTable = `\nCREATE TABLE IF NOT EXISTS kv (\n\t pkey    VARCHAR(191) NOT NULL,\n\t value   MEDIUMTEXT,\n\t expires DATETIME,\n\t ttl     INT NOT NULL DEFAULT 0, -- ttl 0 is forever\n\t ts      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\t PRIMARY KEY(pkey)\n)`\n\nvar kvLateInitOnce sync.Once\n\nfunc kvLazyInit() {\n\tkvLateInitOnce.Do(func() {\n\t\tSqlInit(KVTable)\n\t\tgo kvCleanup()\n\t})\n}\n\nfunc kvCleanup() {\n\tc := time.Tick(time.Minute)\n\n\tfor _ = range c {\n\t\tdb := SqlDB()\n\t\t_, err := db.Exec(\"DELETE FROM kv WHERE expires < NOW()\")\n\t\tif err != nil {\n\t\t\tlog.Printf(\"DELETE of expired keys from the DB failed: %s\", err)\n\t\t}\n\t}\n}\n\n// ExistsKV checks to see if a key exists in the kv. False if any errors are\n// encountered.\nfunc ExistsKV(key string) bool {\n\tkvLazyInit()\n\tdb := SqlDB()\n\n\tvar count int64\n\tsql := \"SELECT COUNT(pkey) FROM kv WHERE pkey=? AND expires > NOW()\"\n\terr := db.QueryRow(sql, key).Scan(&count)\n\tif err != nil {\n\t\tlog.Printf(\"ExistsKV query %q failed: %s\", sql, err)\n\t\treturn false\n\t}\n\n\treturn count > 0\n}\n\n// GetKV retreives a value from the database. Returns value,ok style. Returns\n// \"\", false if the query fails and \"\", true if there was no value available.\nfunc GetKV(key string) (string, bool) {\n\tkvLazyInit()\n\tdb := SqlDB()\n\n\tvar value string\n\tsql := \"SELECT value FROM kv WHERE pkey=? AND expires > NOW()\"\n\terr := db.QueryRow(sql, key).Scan(&value)\n\tif err == dbsql.ErrNoRows {\n\t\treturn \"\", true\n\t} else if err != nil {\n\t\tlog.Printf(\"GetKV query %q failed: %s\", sql, err)\n\t\treturn \"\", false\n\t}\n\n\treturn value, true\n}\n\n// SetKV inserts a new value in the database with the provided TTL. If the TTL\n// is 0, it defaults to 10 years.\nfunc SetKV(key, value string, ttl time.Duration) (err error) {\n\tkvLazyInit()\n\tdb := SqlDB()\n\tnow := time.Now()\n\n\tif ttl == 0 {\n\t\tttl = time.Hour * 24 * 3650\n\t}\n\n\tsql := \"INSERT INTO kv (pkey,value,expires,ttl) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE value=?, expires=?, ttl=?\"\n\n\texpires := now.Add(ttl)\n\tttlsecs := int(ttl.Seconds())\n\t_, err = db.Exec(sql, key, value, expires, ttlsecs, value, expires, ttlsecs)\n\n\tif err != nil {\n\t\tlog.Printf(\"SetKV query %q failed: %s\", sql, err)\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "hal/logger.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Logger provides a handle for using Hal's logging facility. Any Logger created\n// ultimately uses the same singleton.\ntype Logger struct {\n\tprefix string // logging prefix string - eventually will be prepended to all messages\n}\n\ntype LogEntry struct {\n\tTime    time.Time\n\tPrefix  string\n\tBody    string\n\tIsDebug bool\n}\n\n// logger contains the state for the logger\ntype logger struct {\n\tdebug       bool            // for enabling/disabling debug logs\n\tlogSinks    []chan LogEntry // a list of channels to receive log messages\n\tdbgSinks    []chan LogEntry // a list of channels to receive debug messages\n\tlogFwdQuit  chan struct{}   // used to quit the default log message forwarder\n\tdbgFwdQuit  chan struct{}   // used to quit the default debug message forwarder\n\tlogFwdClose chan struct{}   // used to signal it's ok to close the log channel\n\tdbgFwdClose chan struct{}   // used to signal it's ok to close the debug channel\n\tlistLock    sync.Mutex      // protect concurrent access to the sink lists\n\tonce        sync.Once       // initialize on first use\n}\n\n// makes log available inside Hal\nvar log Logger\n\n// the singleton logger state\nvar gl logger\n\n// String returns the LogEntry as a formatted log string.\nfunc (l *LogEntry) String() string {\n\tvar prefix string\n\n\tif l.Prefix != \"\" {\n\t\tprefix = \"[\" + l.Prefix + \"] \"\n\t}\n\n\treturn l.Time.Format(time.RFC3339) + \" \" + prefix + l.Body\n}\n\n// initialize allocates channels and starts the background goroutines\n// that forward output to stdout\nfunc (l *logger) initialize() {\n\tl.once.Do(func() {\n\t\tl.listLock.Lock()\n\t\tdefer l.listLock.Unlock()\n\n\t\tl.debug = true\n\t\tl.logSinks = make([]chan LogEntry, 1)\n\t\tl.dbgSinks = make([]chan LogEntry, 1)\n\t\tl.logSinks[0] = make(chan LogEntry, 10)\n\t\tl.dbgSinks[0] = make(chan LogEntry, 10)\n\t\tl.logFwdClose = make(chan struct{})\n\t\tl.dbgFwdClose = make(chan struct{})\n\n\t\t// always print logs & debug to stdout by default\n\t\tgo l.fwdStdout(l.logSinks[0], l.logFwdClose)\n\t\tgo l.fwdStdout(l.dbgSinks[0], l.dbgFwdClose)\n\t})\n}\n\n// fwdStdout is run as a goroutine to read off a channel and print to stdout.\nfunc (l *logger) fwdStdout(src chan LogEntry, closed chan struct{}) {\n\tfor out := range src {\n\t\tprint(out.String() + \"\\n\")\n\t}\n\n\tclosed <- struct{}{}\n}\n\n// SetPrefix sets a new prefix that will be prepended to every message from the logger handle.\nfunc (l *Logger) SetPrefix(prefix string) {\n\tl.prefix = prefix\n}\n\n// Printf formats the message and propagates it as a log message.\nfunc (l *Logger) Printf(msg string, a ...interface{}) {\n\tgl.initialize()\n\n\tout := LogEntry{\n\t\tTime:   time.Now(),\n\t\tPrefix: l.prefix,\n\t\tBody:   fmt.Sprintf(msg, a...),\n\t}\n\n\tfor _, sink := range gl.logSinks {\n\t\tsink <- out\n\t}\n}\n\n// Println merges the arguments and propagates the result as a log message.\nfunc (l *Logger) Println(a ...interface{}) {\n\tgl.initialize()\n\n\tout := LogEntry{\n\t\tTime:   time.Now(),\n\t\tPrefix: l.prefix,\n\t\tBody:   fmt.Sprintln(a...),\n\t}\n\n\tfor _, sink := range gl.logSinks {\n\t\tsink <- out\n\t}\n}\n\n// Debugf formats the message and propagates it. No work is performed if debugging\n// is disabled.\nfunc (l *Logger) Debugf(msg string, a ...interface{}) {\n\tgl.initialize()\n\n\tif gl.debug {\n\t\tout := LogEntry{\n\t\t\tTime:    time.Now(),\n\t\t\tPrefix:  l.prefix,\n\t\t\tBody:    fmt.Sprintf(msg, a...),\n\t\t\tIsDebug: true,\n\t\t}\n\n\t\tfor _, sink := range gl.dbgSinks {\n\t\t\tsink <- out\n\t\t}\n\t}\n}\n\n// Fatalf formats the message, propagates the log, then exits the program.\nfunc (l *Logger) Fatalf(msg string, a ...interface{}) {\n\tgl.initialize()\n\n\tout := LogEntry{\n\t\tTime:   time.Now(),\n\t\tPrefix: l.prefix,\n\t\tBody:   fmt.Sprintf(msg, a...),\n\t}\n\n\tfor i, sink := range gl.logSinks {\n\t\tsink <- out\n\t\tif i > 0 {\n\t\t\tclose(sink)\n\t\t}\n\t}\n\n\tl.DisableLogStdout()\n\tl.DisableDbgStdout()\n\n\tos.Exit(1)\n}\n\n// Panic panics immediately. No attempt is made to forward/propagate.\nfunc (l *Logger) Panic(msg string) {\n\tout := LogEntry{\n\t\tTime:   time.Now(),\n\t\tPrefix: l.prefix,\n\t\tBody:   msg,\n\t}\n\tpanic(out.String())\n}\n\n// Panicf formats a message and panics. Not propagated.\nfunc (l *Logger) Panicf(msg string, a ...interface{}) {\n\tout := LogEntry{\n\t\tTime:   time.Now(),\n\t\tPrefix: l.prefix,\n\t\tBody:   fmt.Sprintf(msg, a...),\n\t}\n\tpanic(out.String())\n}\n\n// IsDebug returns true of debug messages are enabled.\nfunc IsDebug() bool {\n\treturn gl.debug\n}\n\n// IsDebug returns true of debug messages are enabled.\nfunc (l *Logger) IsDebug() bool {\n\treturn gl.debug\n}\n\n// EnableDebug enables debug message propagation.\nfunc (l *Logger) EnableDebug() {\n\tgl.debug = true\n}\n\n// DisableDebug disables debug message propagation.\nfunc (l *Logger) DisableDebug() {\n\tgl.debug = false\n}\n\n// NewLogSink creates a new channel that will receive log messages.\n// It is allocated and ready to go on return. Do not close it.\nfunc (l *Logger) NewLogSink() chan LogEntry {\n\tgl.initialize()\n\tgl.listLock.Lock()\n\tdefer gl.listLock.Unlock()\n\n\tsink := make(chan LogEntry, 1000)\n\tgl.logSinks = append(gl.logSinks, sink)\n\treturn sink\n}\n\n// NewLogSink creates a new channel that will receive debug messages.\n// It is allocated and ready to go on return. Do not close it.\nfunc (l *Logger) NewDebugSink() chan LogEntry {\n\tgl.initialize()\n\tgl.listLock.Lock()\n\tdefer gl.listLock.Unlock()\n\n\tsink := make(chan LogEntry, 1000)\n\tgl.dbgSinks = append(gl.dbgSinks, sink)\n\treturn sink\n}\n\n// DisableLogStdout disables the automatic forwarding of log messages to stdout.\nfunc (l *Logger) DisableLogStdout() {\n\tgl.initialize()\n\tgl.listLock.Lock()\n\tdefer gl.listLock.Unlock()\n\n\tclose(gl.logSinks[0])\n\t<-gl.logFwdClose\n\tclose(gl.logFwdClose)\n\n\tgl.logSinks = gl.logSinks[1:]\n}\n\n// DisableDbgStdout disables the automatic forwarding of debug messages to stdout.\nfunc (l *Logger) DisableDbgStdout() {\n\tgl.initialize()\n\n\tgl.listLock.Lock()\n\tdefer gl.listLock.Unlock()\n\n\tclose(gl.dbgSinks[0])\n\t<-gl.dbgFwdClose\n\tclose(gl.dbgFwdClose)\n\n\tgl.dbgSinks = gl.dbgSinks[1:]\n}\n"
  },
  {
    "path": "hal/logger_test.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"testing\"\n)\n\nfunc TestLogger(t *testing.T) {\n\tl := Logger{}\n\n\tl.Printf(\"you SHOULD see this log message on stdout 1/2\")\n\tl.Debugf(\"you SHOULD see this debug message on stdout 2/2\")\n\n\tl.DisableDebug()\n\n\tl.Debugf(\"you should NOT see this debug message on stdout 1/1\")\n\n\t// these would most likely panic if something was wrong\n\tl.DisableDbgStdout()\n\tl.DisableLogStdout()\n\n\t// should print nothing, manually verifiable\n\tl.Printf(\"you should NOT see this log message on stdout 1/2\")\n\tl.Debugf(\"you should NOT see this debug message on stdout 2/2\")\n\n}\n"
  },
  {
    "path": "hal/periodic.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"math/rand\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype PeriodicFunc struct {\n\tName     string\n\tInterval time.Duration\n\tFunction func()\n\tNoRand   bool // set to true to disable randomizing the first execution\n\tlast     time.Time\n\tstatus   string\n\trunning  bool\n\ttick     <-chan time.Time\n\trun      chan time.Time\n\texit     chan struct{}\n\tstarting sync.WaitGroup\n\tstopping sync.WaitGroup\n\tinit     sync.Once\n\tmut      sync.Mutex\n}\n\nvar periodicData struct {\n\tfuncs []*PeriodicFunc\n\tmut   sync.Mutex\n}\n\nfunc init() {\n\tperiodicData.funcs = make([]*PeriodicFunc, 0)\n}\n\n// GetPeriodicFunc finds a periodic function by name and returns a pointer to it.\n// If the name is not found, nil is returned.\nfunc GetPeriodicFunc(name string) *PeriodicFunc {\n\tperiodicData.mut.Lock()\n\tdefer periodicData.mut.Unlock()\n\n\tfor _, pf := range periodicData.funcs {\n\t\tif pf.Name == name {\n\t\t\treturn pf\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// initialize internal fields, called automatically using pf.init.Do\nfunc (pf *PeriodicFunc) initialize() {\n\tperiodicData.mut.Lock()\n\tdefer periodicData.mut.Unlock()\n\n\tpf.status = \"initialized\"\n\tpf.exit = make(chan struct{})\n\tpf.tick = make(<-chan time.Time)\n\tpf.run = make(chan time.Time)\n}\n\n// loop is the goroutine's program loop\nfunc (pf *PeriodicFunc) loop() {\n\tpf.mut.Lock()\n\tpf.status = \"running\"\n\tpf.running = true\n\tpf.mut.Unlock()\n\n\tpf.starting.Done()\n\n\t// TODO: this should capture/handle panics like router.go does\npfLoop:\n\tfor {\n\t\tselect {\n\t\tcase <-pf.exit:\n\t\t\tpf.status = \"stopped\"\n\t\t\tbreak pfLoop\n\t\tcase t := <-pf.tick:\n\t\t\tlog.Debugf(\"PeriodicFunc tick %q @ %s\", pf.Name, t)\n\t\t\tpf.runFunc(t)\n\t\tcase t := <-pf.run:\n\t\t\tlog.Debugf(\"PeriodicFunc run %q @ %s\", pf.Name, t)\n\t\t\tpf.runFunc(t)\n\t\t}\n\t}\n\n\tpf.mut.Lock()\n\tpf.running = false\n\tpf.mut.Unlock()\n\n\tpf.stopping.Done()\n}\n\n// runFunc runs the provided function while holding the pf's mutex.\nfunc (pf *PeriodicFunc) runFunc(t time.Time) {\n\tpf.mut.Lock()\n\tdefer pf.mut.Unlock()\n\n\tpf.last = t\n\tpf.Function()\n}\n\n// Register puts a pf in the global list and makes it available to GetPeriodicFunc.\n// Anonymous pf's work fine but are not retreivable.\nfunc (pf *PeriodicFunc) Register() {\n\tfound := GetPeriodicFunc(pf.Name)\n\tif found != nil {\n\t\tlog.Debugf(\"Found duplicate name %q in list of PeriodicFuncs while registering.\", pf.Name)\n\t\treturn\n\t}\n\n\tperiodicData.mut.Lock()\n\tdefer periodicData.mut.Unlock()\n\n\tperiodicData.funcs = append(periodicData.funcs, pf)\n}\n\n// Start a periodic function.\nfunc (pf *PeriodicFunc) Start() {\n\tpf.init.Do(pf.initialize)\n\n\tpf.mut.Lock()\n\n\tpf.tick = time.Tick(pf.Interval)\n\tpf.starting.Add(1)\n\n\tgo func() {\n\t\t// avoid a thundering herd by sleeping for a random number of seconds\n\t\tif !pf.NoRand {\n\t\t\tpf.randSleep()\n\t\t}\n\n\t\tpf.loop() // may block on pf.mut until Unlock()\n\t}()\n\n\tpf.mut.Unlock()\n\n\tpf.starting.Wait() // wait for the goroutine to call Done()\n\n\t// run the first pass immediately\n\tpf.run <- time.Now()\n}\n\n// Stop a periodic function.\nfunc (pf *PeriodicFunc) Stop() {\n\tpf.init.Do(pf.initialize)\n\tpf.mut.Lock()\n\tdefer pf.mut.Unlock()\n\n\tpf.exit <- struct{}{}\n}\n\n// Bump schedules a periodic function to update outside of the scheduled times.\n// The value of pf.Last() is updated when this is used.\nfunc (pf *PeriodicFunc) Bump() {\n\tpf.init.Do(pf.initialize)\n\tpf.mut.Lock()\n\tdefer pf.mut.Unlock()\n\n\tpf.run <- time.Now()\n}\n\n// Status returns initialized/running/stopped state as a string.\nfunc (pf *PeriodicFunc) Status() string {\n\tpf.init.Do(pf.initialize)\n\tpf.mut.Lock()\n\tdefer pf.mut.Unlock()\n\n\treturn pf.status\n}\n\n// Last returns the wallclock time of the last run of the function.\nfunc (pf *PeriodicFunc) Last() time.Time {\n\tpf.init.Do(pf.initialize)\n\tpf.mut.Lock()\n\tdefer pf.mut.Unlock()\n\n\treturn pf.last\n}\n\n// randSleep selects a random number between 0 and 60 and sleeps that many\n// seconds before returning. While sleeping, the pf status is set to \"sleeping\".\nfunc (pf *PeriodicFunc) randSleep() {\n\tpf.mut.Lock()\n\tpf.status = \"sleeping\"\n\tpf.mut.Unlock()\n\n\trandSecs := rand.Intn(60)\n\ttime.Sleep(time.Second * time.Duration(randSecs))\n}\n"
  },
  {
    "path": "hal/persist_plugins.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nconst PLUGIN_INST_TABLE = `\nCREATE TABLE IF NOT EXISTS plugin_instances (\n\tplugin  varchar(191) NOT NULL,\n\tbroker  varchar(191) NOT NULL,\n\troom    varchar(191) NOT NULL,\n\tregex   varchar(191) NOT NULL DEFAULT \"\",\n\tts      TIMESTAMP,\n\tPRIMARY KEY(plugin, broker, room)\n)\n`\n\n// LoadInstances loads the previously saved plugin instance configuration\n// from the database and *merges* it with the plugin registry. This should be\n// idempotent if run multiple times.\n// TODO: decide if it makes sense to persist settings or just pull the prefs\n// each time.\nfunc (pr *pluginRegistry) LoadInstances() error {\n\tlog.Printf(\"Loading plugin instances from the database.\")\n\n\tdb := SqlDB()\n\n\terr := SqlInit(PLUGIN_INST_TABLE)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to initialize the plugin_instances table: %s\", err)\n\t\treturn err\n\t}\n\n\trows, err := db.Query(`SELECT plugin, broker, room, regex FROM plugin_instances`)\n\tif err != nil {\n\t\tlog.Printf(\"LoadInstances SQL query failed: %s\", err)\n\t\treturn err\n\t}\n\n\tdefer rows.Close()\n\n\tvar pname, bname, roomId, re string\n\tfor rows.Next() {\n\t\terr := rows.Scan(&pname, &bname, &roomId, &re)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"LoadInstances rows.Scan() failed: %s\", err)\n\t\t\treturn err\n\t\t}\n\n\t\t// check to see if there is already a runtime instance, create it\n\t\t// if it doesn't exist\n\t\tfound := pr.FindInstances(pname, bname, roomId)\n\t\tif len(found) == 0 {\n\t\t\t// instance is in the DB but not registered, do it now\n\t\t\tplugin, err := pr.GetPlugin(pname)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"%q is configured in the database but is not registered. Ignoring.\", pname)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tbroker := Router().GetBroker(bname)\n\t\t\tif broker == nil {\n\t\t\t\tlog.Fatalf(\"Broker %q does not exist.\", bname)\n\t\t\t}\n\n\t\t\tinst := plugin.Instance(roomId, broker)\n\t\t\tinst.Regex = re // RE can be overridden per instance\n\n\t\t\t// go over the settings and pull preferences before firing up the instance\n\t\t\tinst.LoadSettingsFromPrefs()\n\n\t\t\terr = inst.Register()\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Could not register plugin instance for plugin %q and room id %q: %s\",\n\t\t\t\t\tpname, roomId, err)\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else if len(found) == 1 {\n\t\t\t// already there, move on\n\t\t\tcontinue\n\t\t} else {\n\t\t\tlog.Fatalf(\"BUG: more than 1 plugin instance matched for plugin %q and room id %q\",\n\t\t\t\tpname, roomId)\n\t\t}\n\t}\n\n\tlog.Println(\"Done loading plugin instances.\")\n\n\treturn nil\n}\n\n// SaveInstances saves plugin instances configurations to the database.\nfunc (pr *pluginRegistry) SaveInstances() error {\n\tlog.Printf(\"Saving plugin instances to the database.\")\n\tdefer func() { log.Printf(\"Done saving plugin instances.\") }()\n\n\terr := SqlInit(PLUGIN_INST_TABLE)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to initialize the plugin_instances table: %s\", err)\n\t\treturn err\n\t}\n\n\tinstances := pr.InstanceList()\n\n\t// use a transaction to (relatively) safely wipe & rewrite the whole table\n\tdb := SqlDB()\n\ttx, err := db.Begin()\n\tstmt, err := tx.Prepare(`INSERT INTO plugin_instances\n\t                          (plugin, broker, room, regex)\n\t                         VALUES (?, ?, ?, ?)`)\n\n\t// clear the table before writing new records\n\t_, err = tx.Exec(\"TRUNCATE TABLE plugin_instances\")\n\n\tfor _, inst := range instances {\n\t\t_, err = stmt.Exec(inst.Plugin.Name, inst.Broker.Name(), inst.RoomId, inst.Regex)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"insert failed: %s\", err)\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = tx.Commit()\n\tif err != nil {\n\t\tlog.Printf(\"SaveInstances transaction failed: %s\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "hal/plugins.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"sync\"\n)\n\n// pluginRegistry contains the plugin registration data as a singleton\ntype pluginRegistry struct {\n\tplugins   []*Plugin   // registered plugins\n\tinstances []*Instance // instances of plugins\n\tmut       sync.Mutex  // concurrent access\n\tinit      sync.Once   // initialize the singleton once\n}\n\n// Plugin is a function with metadata to assist with message routing.\n// Plugins are registered at startup by the main program and wired up\n// to receive events when an instance is created e.g. by the pluginmgr\n// plugin.\n// The Command and Regex can be set to pre-filter messages. Regex should\n// only be used for custom REs. Most commands should use Command to set\n// a static string and the RE will be generated automatically and consistently.\ntype Plugin struct {\n\tName      string          // a unique name (used to launch instances)\n\tFunc      func(Evt)       // the code to execute for each matched event\n\tInit      func(*Instance) // plugin hook called at instance creation time\n\tCommand   string          // the name of the command for invocation, e.g. \"atlas\", \"uptime\"\n\tRegex     string          // the default regex match, to be used when Command isn't sufficient\n\tBotEvents bool            // set to true to receive events generated by the bot user\n\tSettings  Prefs           // required+autoloaded preferences + defaults\n\tSecrets   []string        // required+autoloaded secret key names\n}\n\n// Instance is an instance of a plugin tied to a room.\ntype Instance struct {\n\t*Plugin\n\tRoomId   string         // room id\n\tBroker   Broker         // the broker that produces events\n\tRegex    string         // a regex for filtering messages\n\tSettings Prefs          // runtime settings for the instance\n\tregex    *regexp.Regexp // the compiled regex\n}\n\nvar pluginRegSingleton pluginRegistry\n\nfunc PluginRegistry() *pluginRegistry {\n\tpluginRegSingleton.init.Do(func() {\n\t\tpluginRegSingleton.plugins = make([]*Plugin, 0)\n\t\tpluginRegSingleton.instances = make([]*Instance, 0)\n\t})\n\n\treturn &pluginRegSingleton\n}\n\n// Register registers a plugin with the bot.\nfunc (p *Plugin) Register() error {\n\tpr := PluginRegistry()\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tfor _, plugin := range pr.plugins {\n\t\tif plugin.Name == p.Name {\n\t\t\tlog.Printf(\"Ignoring multiple calls to Register() for plugin '%s'\", p.Name)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tpr.plugins = append(pr.plugins, p)\n\n\treturn nil\n}\n\n// Unregister removes a plugin from the bot.\nfunc (p *Plugin) Unregister() error {\n\tpr := PluginRegistry()\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tplugins := make([]*Plugin, len(pr.plugins)-1)\n\tvar i int\n\tfor _, plugin := range pr.plugins {\n\t\tif plugin.Name == p.Name {\n\t\t\tcontinue\n\t\t} else {\n\t\t\t// TODO: this might segfault if this is called on an unregistered or never-registered plugin\n\t\t\tplugins[i] = plugin\n\t\t\ti++\n\t\t}\n\t}\n\n\tpr.plugins = plugins\n\n\treturn nil\n}\n\n// Instance creates an instance of a plugin. It is *not* registered (and\n// therefore not considered by the router until that is done).\nfunc (p *Plugin) Instance(roomId string, broker Broker) *Instance {\n\ti := Instance{\n\t\tPlugin: p,\n\t\tRoomId: roomId,\n\t\tBroker: broker,\n\t\tRegex:  p.Regex,\n\t}\n\n\treturn &i\n}\n\n// Register an instance with the bot so that it starts receiving messages.\nfunc (inst *Instance) Register() error {\n\tpr := PluginRegistry()\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\t// default to the plugin's default if no RE was provided\n\tif inst.Regex == \"\" {\n\t\tinst.Regex = inst.Plugin.Regex\n\t}\n\t// TODO: the default regex still doesn't always show up\n\n\t// TODO: manually check/return the error so the bot doesn't crash\n\tinst.regex = regexp.MustCompile(inst.Regex)\n\n\t// call the instance init handler\n\tif inst.Plugin.Init != nil {\n\t\tinst.Plugin.Init(inst)\n\t}\n\n\t// once an instance is registered, the router will automatically\n\t// pick it up on the next message it processes\n\tpr.instances = append(pr.instances, inst)\n\n\tlog.Debugf(\"Registered plugin %q in room id %q on broker %q with RE match %q\",\n\t\tinst.Name, inst.RoomId, inst.Broker.Name(), inst.regex)\n\n\treturn nil\n}\n\n// Unregister removes an instance from the list of plugin instances.\nfunc (inst *Instance) Unregister() error {\n\tpr := PluginRegistry()\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tvar idx int\n\tfor j, i := range pr.instances {\n\t\t// TODO: verify if pointer equality is sufficient\n\t\tif i == inst {\n\t\t\tidx = j\n\t\t\tbreak\n\t\t}\n\t}\n\n\t// delete the instance from the list\n\tpr.instances = append(pr.instances[:idx], pr.instances[idx+1:]...)\n\n\tlog.Printf(\"Unregistered plugin '%s' from room id '%s'\", inst.Name, inst.RoomId)\n\n\treturn nil\n}\n\n// LoadSettingsFromPrefs loads all of the settings specified in the plugin\n// Settings list into the instance's Settings list. Any current settings\n// are replaced. The search is run with room and plugin set to whatever\n// values the instance has.\nfunc (inst *Instance) LoadSettingsFromPrefs() {\n\tpr := PluginRegistry()\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tips := inst.Plugin.Settings.Clone()\n\n\t// wipe the previous settings\n\tinst.Settings = make(Prefs, len(ips))\n\n\tfor i, ppref := range ips {\n\t\tppref.Room = inst.RoomId\n\t\tppref.Broker = inst.Broker.Name()\n\t\tppref.Plugin = inst.Plugin.Name\n\t\tipref := ppref.Get()\n\t\tinst.Settings[i] = ipref\n\t}\n}\n\n// SaveSettingsToPrefs saves runtime instance preferences to the prefs\n// table in the database.\nfunc (inst *Instance) SaveSettingsToPrefs() {\n\tpr := PluginRegistry()\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tfor _, ipref := range inst.Settings {\n\t\tipref.Set()\n\t}\n}\n\n// PluginList returns a snapshot of the plugin list at call time.\nfunc (pr *pluginRegistry) PluginList() []*Plugin {\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tout := make([]*Plugin, len(pr.plugins))\n\tcopy(out, pr.plugins) // intentional shallow copy\n\treturn out\n}\n\n// InstanceList returns a snapshot of the instance list at call time.\nfunc (pr *pluginRegistry) InstanceList() []*Instance {\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\t// this gets called in the router for every message that comes in, so it\n\t// might come to pass that this will perform poorly, but for now with a\n\t// relatively small number of instances we'll take the copy hit in exchange\n\t// for not having to think about concurrent access to the list\n\tout := make([]*Instance, len(pr.instances))\n\tcopy(out, pr.instances) // intentional shallow copy\n\treturn out\n}\n\n// GetPlugin returns the plugin specified by its name string.\nfunc (pr *pluginRegistry) GetPlugin(name string) (*Plugin, error) {\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tfor _, p := range pr.plugins {\n\t\tif p.Name == name {\n\t\t\treturn p, nil\n\t\t}\n\t}\n\n\treturn nil, errors.New(fmt.Sprintf(\"no such plugin: %q\", name))\n}\n\n// FindInstances returns the plugin instances that match the provided\n// room id, broker, and plugin name.\nfunc (pr *pluginRegistry) FindInstances(roomId, bname, plugin string) []*Instance {\n\tpr.mut.Lock()\n\tdefer pr.mut.Unlock()\n\n\tout := make([]*Instance, 0)\n\n\tfor _, i := range pr.instances {\n\t\tif i.Plugin.Name == plugin && i.Broker.Name() == bname && i.RoomId == roomId {\n\t\t\tout = append(out, i)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// ActivePluginList returns a list of plugins that have registered instances.\nfunc (pr *pluginRegistry) ActivePluginList() []*Plugin {\n\tout := make([]*Plugin, 0)\n\n\t// create a unique list of plugins in use by instances and return that\n\tfor _, i := range pr.InstanceList() {\n\t\tip := i.Plugin\n\n\t\tseen := false\n\t\tfor _, p := range out {\n\t\t\tif p.Name == ip.Name {\n\t\t\t\tseen = true\n\t\t\t}\n\t\t}\n\n\t\t// make sure each plugin is only inserted once\n\t\tif !seen {\n\t\t\tout = append(out, ip)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// InactivePluginList returns a list of plugins that do not have any registered instances.\nfunc (pr *pluginRegistry) InactivePluginList() []*Plugin {\n\tout := make([]*Plugin, 0)\n\tinst := pr.InstanceList()\n\n\t// for each plugin, add it to the out list only if there are no instances using it\n\tfor _, p := range pr.PluginList() {\n\t\tactive := false\n\t\tfor _, i := range inst {\n\t\t\tif p.Name == i.Plugin.Name {\n\t\t\t\tactive = true\n\t\t\t}\n\t\t}\n\n\t\tif !active {\n\t\t\tout = append(out, p)\n\t\t}\n\t}\n\n\treturn out\n}\n\nfunc (p *Plugin) String() string {\n\treturn p.Name\n}\n\nfunc (inst *Instance) String() string {\n\treturn fmt.Sprintf(\"%s/%s\", inst.Name, inst.RoomId)\n}\n"
  },
  {
    "path": "hal/prefs.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n)\n\n// provides a persistent configuration store\n\n// Order of precendence for prefs:\n// user -> room -> broker -> plugin -> global -> default\n\n// PrefsTable contains the SQL to create the prefs table\n// key field is called pkey because key is a reserved word\nconst PrefsTable = `\nCREATE TABLE IF NOT EXISTS prefs (\n\t id      INT NOT NULL AUTO_INCREMENT, -- only used for deleting/updating by id\n\t user    VARCHAR(191) DEFAULT \"\",\n\t room    VARCHAR(191) DEFAULT \"\",\n\t broker  VARCHAR(191) DEFAULT \"\",\n\t plugin  VARCHAR(191) DEFAULT \"\",\n\t pkey    VARCHAR(191) NOT NULL,\n\t value   MEDIUMTEXT,\n\t INDEX(id), -- required by mysql for non-PK auto_increment\n\t -- InnoDB limits indexes to 767 bytes so have the PK only index the first\n\t -- 32 characters of each column as a compromise\n\t -- (5 cols * 4 bytes * 32 chars = 640)\n\t PRIMARY KEY(user(32), room(32), broker(32), plugin(32), pkey(32))\n)`\n\n/*\n   -- test data, will remove once there are automated tests\n   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES (\"tobert\", \"\", \"\", \"\", \"foo\", \"user\");\n   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES (\"tobert\", \"CORE\", \"\", \"\", \"foo\",\n   \"user-room\");\n   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES (\"tobert\", \"CORE\", \"slack\", \"\", \"foo\",\n   \"user-room-broker\");\n   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES (\"tobert\", \"CORE\", \"slack\", \"uptime\",\n   \"foo\", \"user-room-broker-plugin\");\n   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES (\"tobert\", \"\", \"slack\", \"uptime\", \"foo\",\n   \"user-broker-plugin\");\n   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES (\"tobert\", \"CORE\", \"\", \"uptime\",\n   \"foo\", \"user-room-plugin\");\n   INSERT INTO prefs (user,room,broker,plugin,pkey,value) VALUES (\"tobert\", \"\", \"\", \"uptime\", \"foo\",\n   \"user-plugin\");\n*/\n\n// !prefs list --scope plugin --plugin autoresponder\n// !prefs get --scope room --plugin autoresponder --room CORE --key timezone\n// !prefs set --scope user --plugin autoresponder --room CORE\n\n// Pref is a key/value pair associated with a combination of user, plugin,\n// borker, or room.\ntype Pref struct {\n\tUser    string `json:\"user\"`\n\tPlugin  string `json:\"plugin\"`\n\tBroker  string `json:\"broker\"`\n\tRoom    string `json:\"room\"`\n\tKey     string `json:\"key\"`\n\tValue   string `json:\"value\"`\n\tDefault string `json:\"default\"`\n\tSuccess bool   `json:\"-\"`\n\tError   error  `json:\"-\"`\n\tId      int    `json:\"id\"`\n}\n\ntype Prefs []Pref\n\n// GetPref will retreive the most-specific preference from pref\n// storage using the parameters provided. This is a bit like pattern\n// matching. If no match is found, the provided default is returned.\n// TODO: explain this better\nfunc GetPref(user, broker, room, plugin, key, def string) Pref {\n\tpref := Pref{\n\t\tUser:    user,\n\t\tRoom:    room,\n\t\tBroker:  broker,\n\t\tPlugin:  plugin,\n\t\tKey:     key,\n\t\tDefault: def,\n\t}\n\n\tup := pref.Get()\n\tif up.Success {\n\t\treturn up\n\t}\n\n\t// no match, return the default\n\tpref.Value = def\n\treturn pref\n}\n\n// SetPref sets a preference and is shorthand for Pref{}.Set().\nfunc SetPref(user, broker, room, plugin, key, value string) error {\n\tpref := Pref{\n\t\tUser:   user,\n\t\tRoom:   room,\n\t\tBroker: broker,\n\t\tPlugin: plugin,\n\t\tKey:    key,\n\t\tValue:  value,\n\t}\n\n\treturn pref.Set()\n}\n\n// GetPrefs retrieves a set of preferences from the database. The\n// settings are matched exactly on user,broker,room,plugin.\n// e.g. GetPrefs(\"\", \"\", \"\", \"uptime\") would get only records that\n// have user/broker/room set to the empty string and room\n// set to \"uptime\". A record with user \"pford\" and plugin \"uptime\"\n// would not be included.\nfunc GetPrefs(user, broker, room, plugin string) Prefs {\n\tpref := Pref{\n\t\tUser:   user,\n\t\tBroker: broker,\n\t\tRoom:   room,\n\t\tPlugin: plugin,\n\t}\n\treturn pref.get()\n}\n\n// FindPrefs gets all records that match any of the inputs that are\n// not empty strings. (hint: user=\"x\", broker=\"y\"; WHERE user=? OR broker=?)\nfunc FindPrefs(user, broker, room, plugin, key string) Prefs {\n\tpref := Pref{\n\t\tUser:   user,\n\t\tBroker: broker,\n\t\tRoom:   room,\n\t\tPlugin: plugin,\n\t\tKey:    key,\n\t}\n\treturn pref.find(false)\n}\n\n// RmPrefId removes a preference from the database by its numeric id.\nfunc RmPrefId(id int) error {\n\tdb := SqlDB()\n\tSqlInit(PrefsTable)\n\n\t_, err := db.Exec(\"DELETE FROM prefs WHERE id=?\", &id)\n\treturn err\n}\n\n// Get retrieves a value from the database. If the database returns\n// an error, Success will be false and the Error field will be populated.\nfunc (in *Pref) Get() Pref {\n\tprefs := in.get()\n\n\tif len(prefs) == 1 {\n\t\treturn prefs[0]\n\t} else if len(prefs) > 1 {\n\t\tpanic(\"TOO MANY PREFS\")\n\t} else if len(prefs) == 0 {\n\t\tout := *in\n\t\t// only set success to false if there is also an error\n\t\t// queries with 0 rows are successful\n\t\tif out.Error != nil {\n\t\t\tout.Success = false\n\t\t} else {\n\t\t\tout.Success = true\n\t\t\tout.Value = out.Default\n\t\t}\n\t\treturn out\n\t}\n\n\tpanic(\"BUG: should be impossible to reach this point\")\n}\n\n// GetPrefs returns all preferences that match the fields set in the handle.\nfunc (in *Pref) GetPrefs() Prefs {\n\treturn in.get()\n}\n\nfunc (in *Pref) get() Prefs {\n\tdb := SqlDB()\n\tSqlInit(PrefsTable)\n\n\tsql := `SELECT user,room,broker,plugin,pkey,value,id\n\t        FROM prefs\n\t        WHERE user=?\n\t\t\t  AND room=?\n\t\t\t  AND broker=?\n\t\t\t  AND plugin=?`\n\tparams := []interface{}{&in.User, &in.Room, &in.Broker, &in.Plugin}\n\n\t// only query by key if it's specified, otherwise get all keys for the selection\n\tif in.Key != \"\" {\n\t\tsql += \" AND pkey=?\"\n\t\tparams = append(params, &in.Key)\n\t}\n\n\trows, err := db.Query(sql, params...)\n\tif err != nil {\n\t\tlog.Printf(\"Returning default due to SQL query failure: %s\", err)\n\t\treturn Prefs{}\n\t}\n\n\tdefer rows.Close()\n\n\tout := make(Prefs, 0)\n\n\tfor rows.Next() {\n\t\tp := *in\n\n\t\terr := rows.Scan(&p.User, &p.Room, &p.Broker, &p.Plugin, &p.Key, &p.Value, &p.Id)\n\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Returning default due to row iteration failure: %s\", err)\n\t\t\tp.Success = false\n\t\t\tp.Value = in.Default\n\t\t\tp.Error = err\n\t\t} else {\n\t\t\tp.Success = true\n\t\t\tp.Error = nil\n\t\t}\n\n\t\tout = append(out, p)\n\t}\n\n\treturn out\n}\n\n// Set writes the value and returns a new struct with the new value.\nfunc (in *Pref) Set() error {\n\tdb := SqlDB()\n\terr := SqlInit(PrefsTable)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to initialize the prefs table: %s\", err)\n\t\treturn err\n\t}\n\n\tsql := `INSERT INTO prefs\n\t\t\t\t\t\t(value,user,room,broker,plugin,pkey)\n\t\t\tVALUES (?,?,?,?,?,?)\n\t\t\tON DUPLICATE KEY\n\t\t\tUPDATE value=?, user=?, room=?, broker=?, plugin=?, pkey=?`\n\n\tparams := []interface{}{\n\t\t&in.Value, &in.User, &in.Room, &in.Broker, &in.Plugin, &in.Key,\n\t\t&in.Value, &in.User, &in.Room, &in.Broker, &in.Plugin, &in.Key,\n\t}\n\n\t_, err = db.Exec(sql, params...)\n\tif err != nil {\n\t\tlog.Printf(\"Pref.Set() write failed: %s\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Set writes the value and returns a new struct with the new value.\nfunc (in *Pref) Delete() error {\n\tdb := SqlDB()\n\n\terr := SqlInit(PrefsTable)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to initialize the prefs table: %s\", err)\n\t\treturn err\n\t}\n\n\tsql := `DELETE FROM prefs\n\t\t\tWHERE user=?\n\t\t\t  AND room=?\n\t\t\t  AND broker=?\n\t\t\t  AND plugin=?\n\t\t\t  AND pkey=?`\n\n\t// TODO: verify only one row was deleted\n\t_, err = db.Exec(sql, &in.User, &in.Room, &in.Broker, &in.Plugin, &in.Key)\n\tif err != nil {\n\t\tlog.Printf(\"Pref.Delete() write failed: %s\", err)\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Find retrieves all preferences from the database that match any field in the\n// handle's fields. If the Key field is set, it is matched first.\n// The resulting list is sorted before it is returned.\n// Unlike Get(), empty string fields are not included in the (generated) query\n// so it can potentially match a lot of rows.\n// Returns an empty list and logs upon errors.\nfunc (p Pref) Find() Prefs {\n\treturn p.find(false)\n}\n\nfunc (p Pref) FindKey(key string) Prefs {\n\tp.Key = key\n\treturn p.find(true)\n}\n\n// FindKey is like Find() but the provide key is required.\nfunc FindKey(key string) Prefs {\n\tp := Pref{Key: key}\n\treturn p.find(true)\n}\n\nfunc (p Pref) find(keyRequired bool) Prefs {\n\tdb := SqlDB()\n\tSqlInit(PrefsTable)\n\n\tfields := make([]string, 0)\n\tparams := make([]interface{}, 0)\n\n\t// NOTE: the order of these statements is important!\n\tif keyRequired {\n\t\t// ok for it to be \"\" to match no key, but still required\n\t\t// query is appended below\n\t\tparams = append(params, p.Key)\n\t}\n\n\tif p.User != \"\" {\n\t\tfields = append(fields, \"user=?\")\n\t\tparams = append(params, p.User)\n\t}\n\n\tif p.Room != \"\" {\n\t\tfields = append(fields, \"room=?\")\n\t\tparams = append(params, p.Room)\n\t}\n\n\tif p.Broker != \"\" {\n\t\tfields = append(fields, \"broker=?\")\n\t\tparams = append(params, p.Broker)\n\t}\n\n\tif p.Plugin != \"\" {\n\t\tfields = append(fields, \"plugin=?\")\n\t\tparams = append(params, p.Plugin)\n\t}\n\n\tif !keyRequired && p.Key != \"\" {\n\t\tfields = append(fields, \"pkey=?\")\n\t\tparams = append(params, p.Key)\n\t}\n\n\tq := bytes.NewBufferString(\"SELECT user,room,broker,plugin,pkey,value,id\\n\")\n\tq.WriteString(\"FROM prefs\\n\")\n\n\tif keyRequired || len(fields) > 0 {\n\t\tq.WriteString(\"\\nWHERE \")\n\t}\n\n\tif keyRequired {\n\t\tq.WriteString(\"pkey=? AND (\")\n\t}\n\n\t// TODO: maybe it's silly to make it easy for Find() to get all preferences\n\t// but let's cross that bridge when we come to it\n\tif len(fields) > 0 {\n\t\t// might make sense to add a param to this func to make it easy to\n\t\t// switch this between AND/OR for unions/intersections\n\t\tq.WriteString(strings.Join(fields, \"\\n  OR \"))\n\t}\n\n\tif keyRequired {\n\t\tq.WriteString(\"\\n)\")\n\t}\n\n\tout := make(Prefs, 0)\n\trows, err := db.Query(q.String(), params...)\n\tif err != nil {\n\t\tlog.Println(q.String())\n\t\tlog.Printf(\"Query failed: %s\", err)\n\t\treturn out\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\trow := Pref{}\n\t\terr = rows.Scan(&row.User, &row.Room, &row.Broker, &row.Plugin, &row.Key, &row.Value, &row.Id)\n\t\t// improbable in practice - follows previously mentioned conventions for errors\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Fetching a row failed: %s\\n\", err)\n\t\t\trow.Error = err\n\t\t\trow.Success = false\n\t\t\trow.Value = p.Default\n\t\t} else {\n\t\t\trow.Error = nil\n\t\t\trow.Success = true\n\t\t}\n\n\t\tout = append(out, row)\n\t}\n\n\tsort.Sort(out)\n\n\treturn out\n}\n\n// Clone returns a full/deep copy of the Prefs list.\nfunc (prefs Prefs) Clone() Prefs {\n\tout := make(Prefs, len(prefs))\n\n\tfor i, pref := range prefs {\n\t\tcopy := pref\n\t\tout[i] = copy\n\t}\n\n\treturn out\n}\n\n// One returns the most-specific preference from the Prefs according\n// to the precedence order of user>room>broker>plugin>global.\n//\nfunc (prefs Prefs) One() Pref {\n\tif len(prefs) == 0 {\n\t\treturn Pref{Success: false}\n\t}\n\n\tsort.Sort(prefs)\n\treturn prefs[0]\n}\n\n// SetKey returns a copy of the pref with the key set to the provided string.\n// Useful for chaining e.g. fooPrefs := p.SetKey(\"foo\").Find().\nfunc (pref Pref) SetKey(key string) Pref {\n\tpref.Key = key // already a copy\n\treturn pref\n}\n\n// SetUser returns a copy of the pref with the User set to the provided string.\nfunc (pref Pref) SetUser(user string) Pref {\n\tpref.User = user\n\treturn pref\n}\n\n// SetBroker returns a copy of the pref with the Broker set to the provided string.\nfunc (pref Pref) SetBroker(broker string) Pref {\n\t// TODO: validate?\n\tpref.Broker = broker\n\treturn pref\n}\n\n// User filters the preference list by user, returning a new Prefs\n// e.g. uprefs = prefs.User(\"adent\")\nfunc (prefs Prefs) User(user string) Prefs {\n\tout := make(Prefs, 0)\n\n\tfor _, pref := range prefs {\n\t\tif pref.User == user {\n\t\t\tout = append(out, pref)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Room filters the preference list by room, returning a new Prefs\n// e.g. instprefs = prefs.Room(\"magrathea\").Plugin(\"uptime\").Broker(\"slack\")\nfunc (prefs Prefs) Room(room string) Prefs {\n\tout := make(Prefs, 0)\n\n\tfor _, pref := range prefs {\n\t\tif pref.Room == room {\n\t\t\tout = append(out, pref)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Broker filters the preference list by broker, returning a new Prefs\nfunc (prefs Prefs) Broker(broker string) Prefs {\n\tout := make(Prefs, 0)\n\n\tfor _, pref := range prefs {\n\t\tif pref.Broker == broker {\n\t\t\tout = append(out, pref)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Plugin filters the preference list by plugin, returning a new Prefs\nfunc (prefs Prefs) Plugin(plugin string) Prefs {\n\tout := make(Prefs, 0)\n\n\tfor _, pref := range prefs {\n\t\tif pref.Plugin == plugin {\n\t\t\tout = append(out, pref)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Key filters the preference list by key, returning a new Prefs\nfunc (prefs Prefs) Key(key string) Prefs {\n\tout := make(Prefs, 0)\n\n\tfor _, pref := range prefs {\n\t\tif pref.Key == key {\n\t\t\tout = append(out, pref)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Value filters the preference list by key, returning a new Prefs\nfunc (prefs Prefs) Value(value string) Prefs {\n\tout := make(Prefs, 0)\n\n\tfor _, pref := range prefs {\n\t\tif pref.Value == value {\n\t\t\tout = append(out, pref)\n\t\t}\n\t}\n\n\treturn out\n}\n\n// Table returns Prefs as a 2d list ready to hand off to e.g. hal.AsciiTable()\nfunc (prefs Prefs) Table() [][]string {\n\tout := make([][]string, 1)\n\tout[0] = []string{\"User\", \"Room\", \"Broker\", \"Plugin\", \"Key\", \"Value\", \"ID\"}\n\n\tfor _, pref := range prefs {\n\t\tm := []string{\n\t\t\tpref.User,\n\t\t\tpref.Room,\n\t\t\tpref.Broker,\n\t\t\tpref.Plugin,\n\t\t\tpref.Key,\n\t\t\tpref.Value,\n\t\t\tfmt.Sprintf(\"%d\", pref.Id),\n\t\t}\n\n\t\tout = append(out, m)\n\t}\n\n\treturn out\n}\n\nfunc (ps Prefs) Len() int           { return len(ps) }\nfunc (ps Prefs) Swap(i, j int)      { ps[i], ps[j] = ps[j], ps[i] }\nfunc (ps Prefs) Less(i, j int) bool { return ps[i].precedence() < ps[j].precedence() }\n\nfunc (p *Pref) precedence() int {\n\tif !p.Success {\n\t\treturn 0\n\t}\n\tif p.User != \"\" {\n\t\treturn 5\n\t}\n\tif p.Room != \"\" {\n\t\treturn 4\n\t}\n\tif p.Broker != \"\" {\n\t\treturn 3\n\t}\n\tif p.Plugin != \"\" {\n\t\treturn 2\n\t}\n\tif p.Key != \"\" {\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunc (p *Pref) String() string {\n\treturn fmt.Sprintf(`Pref{\n\tUser:    %q,\n\tRoom:    %q,\n\tBroker:  %q,\n\tPlugin:  %q,\n\tKey:     %q,\n\tValue:   %q,\n\tDefault: %q,\n\tSuccess: %t,\n\tError:   %v,\n\tId:      %d,\n}`, p.User, p.Room, p.Broker, p.Plugin, p.Key, p.Value, p.Default, p.Success, p.Error, p.Id)\n\n}\n\nfunc (p *Prefs) String() string {\n\tdata := p.Table()\n\treturn AsciiTable(data[0], data[1:])\n}\n"
  },
  {
    "path": "hal/router.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"runtime/debug\"\n\t\"strings\"\n\t\"sync\"\n)\n\n// RouterCTX holds the router's context, including input/output chans.\ntype RouterCTX struct {\n\tbrokers map[string]Broker\n\tin      chan *Evt     // messages from brokers --> plugins\n\tout     chan *Evt     // messages from plugins --> brokers\n\tquit    chan struct{} // to shut down the router loop\n\tupdate  chan struct{} // to notify the router that the instance list changed\n\tmut     sync.Mutex\n\tinit    sync.Once\n}\n\ntype fwdBroker struct {\n\tfrom Broker\n\tto   Broker\n}\n\nvar routerSingleton RouterCTX\n\n// Router returns the singleton router context. The router is initialized\n// on the first call to this function.\nfunc Router() *RouterCTX {\n\trouterSingleton.init.Do(func() {\n\t\trouterSingleton.in = make(chan *Evt, 1000)\n\t\trouterSingleton.out = make(chan *Evt, 1000)\n\t\trouterSingleton.quit = make(chan struct{}, 1)\n\t\trouterSingleton.update = make(chan struct{}, 1)\n\t\trouterSingleton.brokers = make(map[string]Broker)\n\t})\n\n\treturn &routerSingleton\n}\n\n// forwardChan forwards events from one chan of to another.\n// TODO: figure out if this needs to check for closed channels, etc.\nfunc forwardChan(from, to chan *Evt) {\n\tfor {\n\t\tselect {\n\t\tcase evt := <-from:\n\t\t\tto <- evt\n\t\t}\n\t}\n}\n\n// AddBroker adds a broker to the router and starts forwarding\n// events between it and the router.\nfunc (r *RouterCTX) AddBroker(b Broker) {\n\tr.mut.Lock()\n\tdefer r.mut.Unlock()\n\n\tif _, exists := r.brokers[b.Name()]; exists {\n\t\tpanic(fmt.Sprintf(\"BUG: broker '%s' added > 1 times.\", b.Name()))\n\t}\n\n\tb2r := make(chan *Evt, 1000) // messages from the broker to the router\n\n\t// start the broker's event stream\n\tgo b.Stream(b2r)\n\n\t// forward events from the broker to the router's input channel\n\tgo forwardChan(b2r, r.in)\n\n\tr.brokers[b.Name()] = b\n}\n\nfunc (r *RouterCTX) Send(evt Evt) {\n\tr.in <- &evt\n}\n\n// GetBroker retrieves a broker handle by name.\nfunc (r *RouterCTX) GetBroker(name string) Broker {\n\tr.mut.Lock()\n\tdefer r.mut.Unlock()\n\n\tif broker, exists := r.brokers[name]; exists {\n\t\treturn broker\n\t}\n\n\treturn nil\n}\n\n// Brokers returns all brokers that have been added to the router.\n// The returned list is not in any particular order.\nfunc (r *RouterCTX) Brokers() []Broker {\n\tr.mut.Lock()\n\tdefer r.mut.Unlock()\n\n\tout := make([]Broker, len(r.brokers))\n\ti := 0\n\tfor _, b := range r.brokers {\n\t\tout[i] = b\n\t\ti++\n\t}\n\n\treturn out\n}\n\nfunc (r *RouterCTX) Quit() {\n\tr.quit <- struct{}{}\n}\n\n// Route is the main method for the router. It blocks and should be run in a\n// goroutine exactly once. Running more than one router in the same process\n// will result in shenanigans.\nfunc (r *RouterCTX) Route() {\n\tfor {\n\t\tselect {\n\t\tcase <-r.quit:\n\t\t\tclose(r.quit)\n\t\t\tbreak\n\t\tcase evt := <-r.in:\n\t\t\t// events are processed concurrently, plugins are not\n\t\t\tgo r.inEvent(evt)\n\t\tcase evt := <-r.out:\n\t\t\tgo r.outEvent(evt)\n\t\t}\n\t}\n}\n\n// inEvent processes one event and is intended to run in a goroutine.\nfunc (r *RouterCTX) inEvent(evt *Evt) {\n\tvar pname string // must be in the recovery handler's scope\n\n\t// detect invalid commands & count executions\n\tvar ranPlugins int\n\n\t// get a snapshot of the instance list\n\t// TODO: keep an eye on the cost of copying this list for every message\n\tpr := PluginRegistry()\n\tinstances := pr.InstanceList()\n\n\t// if a plugin panics, catch it & log it\n\t// TODO: report errors to a channel?\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tlog.Printf(\"recovered panic in plugin %q\\n\", pname)\n\t\t\tlog.Printf(\"panic: %q\", r)\n\t\t\tdebug.PrintStack()\n\t\t}\n\t}()\n\n\t// if the event doesn't have a command, try to extract one\n\tif evt.Command == \"\" {\n\t\tbody := strings.TrimSpace(evt.Body)\n\t\t// check for a leading \"!\", which indicates it's a command\n\t\tif strings.HasPrefix(body, \"!\") {\n\t\t\tparts := strings.SplitN(body, \" \", 2) // split off the first word\n\t\t\tif len(parts) > 0 {\n\t\t\t\tevt.Command = strings.TrimPrefix(parts[0], \"!\")\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, inst := range instances {\n\t\t// the recovery handler will pick this up in a panic to provide\n\t\t// the name of the plugin that caused the panic\n\t\tpname = inst.Plugin.Name\n\n\t\t// check if it's the correct room\n\t\tif evt.RoomId != inst.RoomId {\n\t\t\tcontinue\n\t\t}\n\n\t\t// only process IsBot messages if the plugin has asked for them\n\t\tif evt.IsBot && !inst.Plugin.BotEvents {\n\t\t\tcontinue\n\t\t}\n\n\t\t// plugins with no RE filter receive every event\n\t\tnoFilter := (inst.Regex == \"\" && inst.Plugin.Command == \"\")\n\t\t// events that match the RE filter are passed onto the plugin\n\t\tmatchesRegex := (inst.Regex != \"\" && inst.regex.MatchString(evt.Body))\n\t\t// events with a Command field that matches exactly are passed to the plugin\n\t\tisCommand := (inst.Plugin.Command != \"\" && evt.Command == inst.Plugin.Command)\n\n\t\t// forward to plugins when any of the above rules passes\n\t\tif noFilter || matchesRegex || isCommand {\n\t\t\t// this will copy the struct twice. It's intentional to avoid\n\t\t\t// mutating the evt between calls. The plugin func signature\n\t\t\t// forces the second copy.\n\t\t\tevtcpy := *evt\n\n\t\t\t// pass the plugin instance pointer to the plugin function so\n\t\t\t// it can access its fields for settings, etc.\n\t\t\tevtcpy.instance = inst\n\n\t\t\t// call the plugin function\n\t\t\t// this may block other plugins from processing the same event but\n\t\t\t// since it's already in a goroutine, other events won't be blocked\n\t\t\tinst.Func(evtcpy)\n\n\t\t\tranPlugins++\n\t\t}\n\t}\n\n\tif evt.IsBot {\n\t\treturn\n\t}\n\n\t// no plugins were executed but evt.Body looks like a command\n\tif ranPlugins == 0 && strings.HasPrefix(strings.TrimSpace(evt.Body), \"!\") {\n\t\t// automatically load the pluginmgr plugin if someone tries to use it and it wasn't attached\n\t\t// don't bother if the bot was built without pluginmgr, in which case err != nil\n\t\tif strings.HasPrefix(strings.TrimSpace(evt.Body), \"!plugin\") {\n\t\t\tmgr, err := pr.GetPlugin(\"pluginmgr\")\n\t\t\tif err == nil {\n\t\t\t\tinst := mgr.Instance(evt.RoomId, evt.Broker)\n\t\t\t\tevtcpy := *evt\n\t\t\t\tevtcpy.instance = inst\n\t\t\t\tinst.Func(evtcpy)\n\t\t\t}\n\t\t} else {\n\t\t\tevt.Replyf(\"invalid bot command: %q (IsBot: %t) (%d plugins were executed for the event).\", evt.Body, evt.IsBot, ranPlugins)\n\t\t}\n\t}\n}\n\nfunc (r *RouterCTX) outEvent(evt *Evt) {\n\t// TODO: fill in this stub\n\tlog.Printf(\"STUB: did nothing with event %s\", evt.String())\n}\n"
  },
  {
    "path": "hal/secrets.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/rand\"\n\t\"io\"\n\t\"sync\"\n)\n\n// secrets stores a plaintext key/value store for\n// sensitive data that the bot and plugins need to operate\n// along with methods for persisting encrypted copies to the database\ntype SecretStore struct {\n\tkey   []byte            // encryption key for persistence\n\tkeyed bool              // track whether the key has been set\n\tkv    map[string]string // the in-memory k/v store\n\tmut   sync.Mutex        // protect concurrent access\n\tinit  sync.Once         // singleton initialization\n\titbl  sync.Once         // table initialization\n}\n\nvar secrets SecretStore\n\n// SECRETS_TABLE holds encrypted key/value pairs along with their nonces.\n// uses VARBINARY instead of BINARY to avoid null termination issues.\nconst SECRETS_TABLE = `\nCREATE TABLE IF NOT EXISTS secrets (\n\tpkey  VARCHAR(191)     NOT NULL, -- plaintext key\n\tsval  VARBINARY(16384) NOT NULL, -- AES/GCM sealed value\n\tnonce VARBINARY(12)    NOT NULL, -- GCM nonce for the value\n\tts    TIMESTAMP,                 -- timestamp, for debugging/cleanup\n\tPRIMARY KEY(pkey)\n)`\n\n// for temporarily holding encrypted data\ntype ssRec struct {\n\tpkey  []byte\n\tsval  []byte\n\tnonce []byte\n}\n\n// 256-bit AES key and 96-bit nonce size in bytes\nconst KEY_SIZE = 32\nconst NONCE_SIZE = 12\n\n// Secrets returns a handle for accessing secrets managed by hal.\nfunc Secrets() *SecretStore {\n\tsecrets.init.Do(func() {\n\t\tsecrets.kv = make(map[string]string)\n\t\tsecrets.key = make([]byte, KEY_SIZE)\n\t\tsecrets.keyed = false\n\t})\n\n\treturn &secrets\n}\n\n// SetEncryptionKey sets the key used to encrypt/decrypt credentials\n// stored in the database. This needs to be called before anything\n// will work.\nfunc (ss *SecretStore) SetEncryptionKey(in []byte) {\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\t// do not rely on the caller's memory: make a copy\n\tdone := copy(ss.key, in)\n\n\t// catch unlikely errors and anyone trying to use a smaller key\n\tif done != KEY_SIZE {\n\t\tlog.Fatalf(\"BUG: SetEncryptionKey failed to store the key. Only %d bytes copied.\", done)\n\t}\n\n\tss.keyed = true\n}\n\n// Get returns the value of a key from the secret store.\n// If the key doesn't exist, empty string is returned.\n// To check existence, use Exists(string).\nfunc (ss *SecretStore) Get(key string) string {\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\tif _, exists := ss.kv[key]; exists {\n\t\treturn ss.kv[key]\n\t} else {\n\t\treturn \"\"\n\t}\n}\n\n// Exists checks to see if the provided key exists\n// in the secret store.\nfunc (ss *SecretStore) Exists(key string) bool {\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\t_, exists := ss.kv[key]\n\treturn exists\n}\n\n// Put adds a key/value to the in-memory secret store.\n// Put'ing a key that already exists overwrites the previous\n// value. The secret store is not persisted at this point,\n// an additional call to Save() is required.\nfunc (ss *SecretStore) Set(key, value string) {\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\tss.kv[key] = value\n}\n\n// Put is an alias for Set\nfunc (ss *SecretStore) Put(key, value string) {\n\tss.Set(key, value)\n}\n\n// Delete removes the key from the in-memory secret store.\n// This is not persisted.\nfunc (ss *SecretStore) Delete(key string) {\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\tdelete(ss.kv, key)\n}\n\n// Dump returns a copy of the kv store. DO NOT USE IN PLUGINS.\n// This returns an UNENCRYPTED copy of the kv store for CLI\n// tools and debugging. This might go away.\nfunc (ss *SecretStore) Dump() map[string]string {\n\tout := make(map[string]string)\n\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\tfor k, v := range ss.kv {\n\t\tout[k] = v\n\t}\n\n\treturn out\n}\n\n// Load secrets from the database and decrypt them into hal's in-memory secret\n// store. The database-side secrets will be added to the existing store,\n// overwriting on conflict (e.g. the database secrets).\n// Any errors during this process are fatal.\nfunc (ss *SecretStore) LoadFromDB() {\n\tif !ss.keyed {\n\t\tpanic(\"The secret store key has not been set!\")\n\t}\n\n\tss.initTable()\n\n\tdb := SqlDB()\n\n\trows, err := db.Query(\"SELECT pkey, sval, nonce FROM secrets\")\n\tif err != nil {\n\t\tlog.Fatalf(\"Could not read secrets from the database: %s\", err)\n\t}\n\n\tdefer rows.Close()\n\n\t// encrypted key/value and key/nonce\n\tencrypted := make([]ssRec, 0)\n\n\t// pull the encrypted data into memory\n\tfor rows.Next() {\n\t\tssr := ssRec{}\n\t\terr := rows.Scan(&ssr.pkey, &ssr.sval, &ssr.nonce)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not rows.Scan: %s\", err)\n\t\t}\n\n\t\tencrypted = append(encrypted, ssr)\n\t}\n\n\tgcm := ss.getGCM()\n\n\t// decrypt the keys/values into ss.kv\n\tfor _, ssr := range encrypted {\n\t\tvalue, err := gcm.Open(nil, ssr.nonce, ssr.sval, nil)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"value decryption failed: %s\\n\", err)\n\t\t}\n\n\t\tss.kv[string(ssr.pkey)] = string(value)\n\t}\n}\n\n// Serialize the secret store, encrypt it, and store it in the database.\n// Any errors during this process are fatal.\nfunc (ss *SecretStore) SaveToDB() {\n\tgcm := ss.getGCM()\n\tss.initTable()\n\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\tdb := SqlDB()\n\n\ttx, err := db.Begin()\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to create transaction for saving secrets: %s\", err)\n\t}\n\n\tinsert, err := tx.Prepare(`INSERT INTO secrets (pkey,sval,nonce) VALUES (?,?,?)`)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to prepare insert query: %s\", err)\n\t}\n\tdefer insert.Close()\n\n\t_, err = tx.Exec(`TRUNCATE TABLE secrets`)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to truncate secrets table: %s\", err)\n\t}\n\n\t// use a unique nonce for each key/value pair\n\t// TODO: ask infosec if using the nonce for both is OK\n\tfor key, val := range ss.kv {\n\t\tnonce, err := ss.mkNonce()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not create a new nonce: %s\", err)\n\t\t}\n\n\t\tsealed := gcm.Seal(nil, nonce, []byte(val), nil)\n\n\t\t_, err = insert.Exec(key, sealed, nonce)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not write encrypted key/value/nonce to DB: %s\", err)\n\t\t}\n\t}\n\n\terr = tx.Commit()\n\tif err != nil {\n\t\tlog.Fatalf(\"secrets.SaveToDB transaction failed: %s\", err)\n\t}\n}\n\n// initTable runs the table initialization statement once\nfunc (ss *SecretStore) initTable() {\n\tss.itbl.Do(func() {\n\t\terr := SqlInit(SECRETS_TABLE)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Failed to initialize the secrets table: %s\", err)\n\t\t}\n\t})\n}\n\nfunc (ss *SecretStore) WipeDB() {\n\tSqlDB().Exec(`TRUNCATE TABLE secrets`)\n}\n\nfunc (ss *SecretStore) InitDB() {\n\tss.initTable()\n\tss.SaveToDB()\n}\n\nfunc (ss *SecretStore) mkNonce() ([]byte, error) {\n\tnonce := make([]byte, NONCE_SIZE)\n\n\t_, err := io.ReadFull(rand.Reader, nonce)\n\tif err != nil {\n\t\tlog.Printf(\"Could not acquire nonce: %s\", err)\n\t\treturn nil, err\n\t}\n\n\treturn nonce, nil\n}\n\n// getGCM returns an AES/GCM cipher configured with the default nonce size.\nfunc (ss *SecretStore) getGCM() cipher.AEAD {\n\tif !ss.keyed {\n\t\tpanic(\"The secret store key has not been set!\")\n\t}\n\n\tss.mut.Lock()\n\tdefer ss.mut.Unlock()\n\n\tblock, err := aes.NewCipher(ss.key)\n\tif err != nil {\n\t\tlog.Fatalf(\"aes.NewCipher failed: %s\", err)\n\t}\n\n\tgcm, err := cipher.NewGCM(block)\n\tif err != nil {\n\t\tlog.Fatalf(\"cipher.NewGCM(aes block) failed: %s\", err)\n\t}\n\n\treturn gcm\n}\n"
  },
  {
    "path": "hal/secrets_test.go",
    "content": "package hal_test\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"testing\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nfunc TestSecretsBasic(t *testing.T) {\n\tsecrets := hal.Secrets()\n\n\t// make sure it returns the empty value\n\tif secrets.Get(\"whatever\") != \"\" {\n\t\tt.Fail()\n\t}\n\n\tif secrets.Exists(\"whatever\") {\n\t\tt.Fail()\n\t}\n\n\tsecrets.Put(\"whatever\", \"foo\")\n\n\tif !secrets.Exists(\"whatever\") {\n\t\tt.Fail()\n\t}\n\n\tif secrets.Get(\"whatever\") != \"foo\" {\n\t\tt.Fail()\n\t}\n\n\tsecrets.Delete(\"whatever\")\n\n\tif secrets.Exists(\"whatever\") {\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "hal/sqldb.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"database/sql\"\n\t\"strings\"\n\t\"sync\"\n\n\t_ \"github.com/go-sql-driver/mysql\"\n)\n\nvar sqldbSingleton *sql.DB\nvar initSqlDbOnce sync.Once\nvar sqlMapMutex sync.Mutex\nvar sqlInitCache map[string]struct{}\n\nconst SECRETS_KEY_DSN = \"hal.dsn\"\n\n// DB returns the database singleton.\nfunc SqlDB() *sql.DB {\n\tinitSqlDbOnce.Do(func() {\n\t\tsecrets := Secrets()\n\t\tdsn := secrets.Get(SECRETS_KEY_DSN)\n\t\tif dsn == \"\" {\n\t\t\tpanic(\"Startup error: SetSqlDB(dsn) must come before any calls to hal.SqlDB()\")\n\t\t}\n\n\t\tvar err error\n\t\tsqldbSingleton, err = sql.Open(\"mysql\", strings.TrimSpace(dsn))\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Could not connect to database: %s\\n\", err)\n\t\t}\n\n\t\t// make sure the connection is in full utf-8 mode\n\t\tsqldbSingleton.Exec(\"SET NAMES utf8mb4\")\n\n\t\terr = sqldbSingleton.Ping()\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Pinging database failed: %s\\n\", err)\n\t\t}\n\n\t\tsqlInitCache = make(map[string]struct{})\n\t})\n\n\treturn sqldbSingleton\n}\n\n// ForceSqlDBHandle can be used to forcibly replace the DB handle with another\n// one, e.g. go-sqlmock. This is mainly here for tests, but it's also useful for\n// things like examples/repl to operate with no database.\nfunc ForceSqlDBHandle(db *sql.DB) {\n\t// trigger the sync.Once so the init code doesn't fire\n\tinitSqlDbOnce.Do(func() {})\n\tsqldbSingleton = db\n}\n\n// SqlInit executes the provided SQL once per runtime.\n// Execution is not tracked across restarts so statements still need\n// to use CREATE TABLE IF NOT EXISTS or other methods of achieving\n// idempotent execution. Errors are returned unmodified, including\n// primary key violations, so you may ignore them as needed.\nfunc SqlInit(sqlTxt string) error {\n\tsqlMapMutex.Lock()\n\tdefer sqlMapMutex.Unlock()\n\n\tdb := SqlDB()\n\n\t// avoid a database round-trip by checking an in-memory cache\n\t// fall through and hit the DB on cold cache\n\tif _, exists := sqlInitCache[sqlTxt]; exists {\n\t\treturn nil\n\t}\n\n\t// clean up a little\n\tsqlTxt = strings.TrimSpace(sqlTxt)\n\tsqlTxt = strings.TrimSuffix(sqlTxt, \";\")\n\n\t// check if it's a simple create table, add engine/charset if unspecified\n\tlowSql := strings.ToLower(sqlTxt)\n\tif strings.HasPrefix(lowSql, \"create table\") && strings.HasSuffix(lowSql, \")\") {\n\t\t// looks like no engine or charset was specified, add it\n\t\t// \"utf8\" has incomplete support.  \"utf8mb4\" provides full utf8 support\n\t\t// https://mathiasbynens.be/notes/mysql-utf8mb4\n\t\tsqlTxt = sqlTxt + \" ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci\"\n\t}\n\n\t// execute the statement\n\t_, err := db.Exec(sqlTxt)\n\tif err != nil {\n\t\tlog.Printf(\"SqlInit() failed on statement '%s':\\n%s\", sqlTxt, err)\n\t\treturn err\n\t}\n\n\tsqlInitCache[sqlTxt] = struct{}{}\n\n\treturn nil\n}\n"
  },
  {
    "path": "hal/text2image.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// Utilities to render text to an image. Useful for sending things like\n// AsciiTable and Utf8Table to chat services that aren't consistent about\n// fixed-width rendering. Might be fun for plugins too...\n//\n// A fixed-width font is embedded in this file to avoid depdending on external\n// font files/paths.\n//\n// IBM VGA 8 font\n// via http://int10h.org/oldschool-pc-fonts/\n// Font License: Creative Commons Attribution-ShareAlike 4.0 International\n//\n// converted with a hacked-up version of dewinfont from\n// http://www.chiark.greenend.org.uk/~sgtatham/fonts/dewinfont\n//\n// This could be smaller, etc. with tighter encoding but I wanted\n// to keep the binascii encoding for easy tweaking.\n\nimport (\n\t\"image\"\n\t\"image/color\"\n\t\"image/draw\"\n\t\"strconv\"\n\t\"unicode/utf8\"\n)\n\n// FontChar is a single character of the font.\n// The Code field might go away. It was extracted from the original font\n// and was flaky so I switched to using the UTF8-string to index and locate\n// glyphs.\ntype FontChar struct {\n\tString string // the UTF-8 single character\n\tCode   uint\n\tWidth  uint\n\tValue  [16]uint8\n}\n\n// FontData is a font and its metadata.\ntype FontData struct {\n\tFacename  string\n\tHeight    int\n\tWidth     int\n\tAscent    int\n\tPointsize int\n\tWeight    int\n\tCharset   int\n\tr2char    map[rune]FontChar // index for quick lookup\n\tChars     [256]FontChar\n}\n\n// charRow converts a string representation of binary uint8 to uint8.\n// This is a bit inefficient but it keeps the font easy to edit and\n// it only needs to run once.\nfunc charRow(in string) (out uint8) {\n\tfor i, r := range in {\n\t\tif r == '1' {\n\t\t\tout |= (1 << uint8(i))\n\t\t}\n\t}\n\n\treturn out\n}\n\n// StringToChars takes a string and returns the FontChar\nfunc (fd *FontData) StringToChars(want string) []*FontChar {\n\trunes := utf8.RuneCountInString(want)\n\tout := make([]*FontChar, runes)\n\terrr := fd.r2char['█']\n\n\ti := 0\n\tfor _, r := range want {\n\t\tif c, exists := fd.r2char[r]; exists {\n\t\t\tout[i] = &c\n\t\t} else {\n\t\t\t// if the character is unknown, return a full block\n\t\t\tout[i] = &errr\n\t\t}\n\t\ti++\n\t}\n\n\treturn out\n}\n\n// StringToImages takes a string and returns a list of images\nfunc (fd *FontData) StringToImages(want string, clr color.Color) []image.Image {\n\tchars := fd.StringToChars(want)\n\tout := make([]image.Image, len(chars))\n\n\trect := image.Rect(0, 0, fd.Width, fd.Height)\n\n\t// TODO: maybe add a field to FontChar to cache or pregenerate the images\n\tfor i, c := range chars {\n\t\timg := image.NewRGBA(rect)\n\t\tvar x uint\n\t\tfor y, row := range c.Value {\n\t\t\tfor x = 0; x < c.Width; x++ {\n\t\t\t\tif row&uint8(1<<x) != 0 {\n\t\t\t\t\timg.Set(int(x), y, clr)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tout[i] = img\n\t}\n\n\treturn out\n}\n\n// StringToImage takes a string and renders it to an image.Image.\n// Newlines are not respected. See: StringsToImage().\nfunc (fd *FontData) StringToImage(want string, clr color.Color) image.Image {\n\trunes := utf8.RuneCountInString(want)\n\toutrect := image.Rect(0, 0, fd.Width*runes, fd.Height)\n\tout := image.NewRGBA(outrect)\n\n\tchars := fd.StringToImages(want, clr)\n\n\tfor i, char := range chars {\n\t\tsr := char.Bounds()\n\t\tdp := image.Pt(i*fd.Width, 0)\n\t\tr := image.Rectangle{dp, dp.Add(sr.Size())}\n\t\tdraw.Draw(out, r, char, sr.Min, draw.Src)\n\t}\n\n\treturn out\n}\n\n// StringsToImage renders an array of strings to an image, with each row in\n// the provided list as a line. The image size is dependent on the text\n// passed in.\nfunc (fd *FontData) StringsToImage(want []string, clr color.Color) image.Image {\n\tvar max int\n\tfor _, str := range want {\n\t\trunes := utf8.RuneCountInString(str)\n\t\tif runes > max {\n\t\t\tmax = runes\n\t\t}\n\t}\n\n\twidth := max * fd.Width\n\theight := len(want) * fd.Height\n\tsize := image.Rect(0, 0, width, height)\n\tout := image.NewRGBA(size)\n\n\tfor y, str := range want {\n\t\trow := fd.StringToImage(str, clr)\n\n\t\tsr := row.Bounds()\n\t\tdp := image.Pt(0, y*fd.Height)\n\t\tr := image.Rectangle{dp, dp.Add(sr.Size())}\n\n\t\tdraw.Draw(out, r, row, sr.Min, draw.Src)\n\t}\n\n\treturn out\n}\n\n// FixedFont returns a handle for the embedded 8x16 VGA font.\nfunc FixedFont() *FontData {\n\tfd := FontData{\n\t\tFacename:  \"Bm437 IBM VGA8\",\n\t\tHeight:    16,\n\t\tWidth:     8,\n\t\tAscent:    12,\n\t\tPointsize: 12,\n\t\tWeight:    400,\n\t\tCharset:   255,\n\t}\n\n\tfd.Chars = [256]FontChar{\n\t\tFontChar{\n\t\t\tString: string('\\u0000'),\n\t\t\tCode:   0x0000,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"☺\",\n\t\t\tCode:   0x0001,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"10000001\"),\n\t\t\t\tcharRow(\"10100101\"),\n\t\t\t\tcharRow(\"10000001\"),\n\t\t\t\tcharRow(\"10000001\"),\n\t\t\t\tcharRow(\"10111101\"),\n\t\t\t\tcharRow(\"10011001\"),\n\t\t\t\tcharRow(\"10000001\"),\n\t\t\t\tcharRow(\"10000001\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"☻\",\n\t\t\tCode:   0x0002,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11000011\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♥\",\n\t\t\tCode:   0x0003,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♦\",\n\t\t\tCode:   0x0004,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♣\",\n\t\t\tCode:   0x0005,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♠\",\n\t\t\tCode:   0x0006,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"•\",\n\t\t\tCode:   0x0007,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"◘\",\n\t\t\tCode:   0x0008,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"11000011\"),\n\t\t\t\tcharRow(\"11000011\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"○\",\n\t\t\tCode:   0x0009,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01000010\"),\n\t\t\t\tcharRow(\"01000010\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♂\",\n\t\t\tCode:   0x000A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11000011\"),\n\t\t\t\tcharRow(\"10011001\"),\n\t\t\t\tcharRow(\"10111101\"),\n\t\t\t\tcharRow(\"10111101\"),\n\t\t\t\tcharRow(\"10011001\"),\n\t\t\t\tcharRow(\"11000011\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♀\",\n\t\t\tCode:   0x000B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011110\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00011010\"),\n\t\t\t\tcharRow(\"00110010\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♀\",\n\t\t\tCode:   0x000C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♪\",\n\t\t\tCode:   0x000D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111111\"),\n\t\t\t\tcharRow(\"00110011\"),\n\t\t\t\tcharRow(\"00111111\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11100000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"♫\",\n\t\t\tCode:   0x000E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111111\"),\n\t\t\t\tcharRow(\"01100011\"),\n\t\t\t\tcharRow(\"01111111\"),\n\t\t\t\tcharRow(\"01100011\"),\n\t\t\t\tcharRow(\"01100011\"),\n\t\t\t\tcharRow(\"01100011\"),\n\t\t\t\tcharRow(\"01100011\"),\n\t\t\t\tcharRow(\"01100111\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"☼\",\n\t\t\tCode:   0x000F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"11100111\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"►\",\n\t\t\tCode:   0x0010,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"10000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11100000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"10000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"◄\",\n\t\t\tCode:   0x0011,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000010\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00011110\"),\n\t\t\t\tcharRow(\"00111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00111110\"),\n\t\t\t\tcharRow(\"00011110\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000010\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"↕\",\n\t\t\tCode:   0x0012,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"‼\",\n\t\t\tCode:   0x0013,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"¶\",\n\t\t\tCode:   0x0014,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111111\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"01111011\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"§\",\n\t\t\tCode:   0x0015,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▬\",\n\t\t\tCode:   0x0016,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"↨\",\n\t\t\tCode:   0x0017,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"↑\",\n\t\t\tCode:   0x0018,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"↓\",\n\t\t\tCode:   0x0019,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"→\",\n\t\t\tCode:   0x001A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"←\",\n\t\t\tCode:   0x001B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"∟\",\n\t\t\tCode:   0x001C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"↔\",\n\t\t\tCode:   0x001D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00101000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00101000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▲\",\n\t\t\tCode:   0x001E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▼\",\n\t\t\tCode:   0x001F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \" \",\n\t\t\tCode:   0x0020,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"!\",\n\t\t\tCode:   0x0021,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"\\\"\",\n\t\t\tCode:   0x0022,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00100100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"#\",\n\t\t\tCode:   0x0023,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"$\",\n\t\t\tCode:   0x0024,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"10000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"%\",\n\t\t\tCode:   0x0025,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"10000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"&\",\n\t\t\tCode:   0x0026,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"'\",\n\t\t\tCode:   0x0027,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"(\",\n\t\t\tCode:   0x0028,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \")\",\n\t\t\tCode:   0x0029,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"*\",\n\t\t\tCode:   0x002A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"+\",\n\t\t\tCode:   0x002B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \",\",\n\t\t\tCode:   0x002C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"-\",\n\t\t\tCode:   0x002D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \".\",\n\t\t\tCode:   0x002E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"/\",\n\t\t\tCode:   0x002F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000010\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"10000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"0\",\n\t\t\tCode:   0x0030,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"1\",\n\t\t\tCode:   0x0031,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"2\",\n\t\t\tCode:   0x0032,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"3\",\n\t\t\tCode:   0x0033,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"4\",\n\t\t\tCode:   0x0034,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"5\",\n\t\t\tCode:   0x0035,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"6\",\n\t\t\tCode:   0x0036,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"7\",\n\t\t\tCode:   0x0037,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"8\",\n\t\t\tCode:   0x0038,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"9\",\n\t\t\tCode:   0x0039,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \":\",\n\t\t\tCode:   0x003A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \";\",\n\t\t\tCode:   0x003B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"<\",\n\t\t\tCode:   0x003C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"=\",\n\t\t\tCode:   0x003D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \">\",\n\t\t\tCode:   0x003E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"?\",\n\t\t\tCode:   0x003F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"@\",\n\t\t\tCode:   0x0040,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"A\",\n\t\t\tCode:   0x0041,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"B\",\n\t\t\tCode:   0x0042,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"C\",\n\t\t\tCode:   0x0043,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"D\",\n\t\t\tCode:   0x0044,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"E\",\n\t\t\tCode:   0x0045,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100010\"),\n\t\t\t\tcharRow(\"01101000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"01101000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100010\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"F\",\n\t\t\tCode:   0x0046,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100010\"),\n\t\t\t\tcharRow(\"01101000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"01101000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"G\",\n\t\t\tCode:   0x0047,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111010\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"H\",\n\t\t\tCode:   0x0048,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"I\",\n\t\t\tCode:   0x0049,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"J\",\n\t\t\tCode:   0x004A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"K\",\n\t\t\tCode:   0x004B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"L\",\n\t\t\tCode:   0x004C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100010\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"M\",\n\t\t\tCode:   0x004D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11101110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"N\",\n\t\t\tCode:   0x004E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"11110110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"11001110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"O\",\n\t\t\tCode:   0x004F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"P\",\n\t\t\tCode:   0x0050,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Q\",\n\t\t\tCode:   0x0051,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"R\",\n\t\t\tCode:   0x0052,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"S\",\n\t\t\tCode:   0x0053,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"T\",\n\t\t\tCode:   0x0054,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"01011010\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"U\",\n\t\t\tCode:   0x0055,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"V\",\n\t\t\tCode:   0x0056,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"W\",\n\t\t\tCode:   0x0057,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11101110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"X\",\n\t\t\tCode:   0x0058,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Y\",\n\t\t\tCode:   0x0059,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Z\",\n\t\t\tCode:   0x005A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"10000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"[\",\n\t\t\tCode:   0x005B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"\\\\\",\n\t\t\tCode:   0x005C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"10000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11100000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00011100\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000010\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"]\",\n\t\t\tCode:   0x005D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"^\",\n\t\t\tCode:   0x005E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"_\",\n\t\t\tCode:   0x005F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"`\",\n\t\t\tCode:   0x0060,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"a\",\n\t\t\tCode:   0x0061,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"b\",\n\t\t\tCode:   0x0062,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"c\",\n\t\t\tCode:   0x0063,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"d\",\n\t\t\tCode:   0x0064,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"e\",\n\t\t\tCode:   0x0065,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"f\",\n\t\t\tCode:   0x0066,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01100100\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"g\",\n\t\t\tCode:   0x0067,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"h\",\n\t\t\tCode:   0x0068,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"i\",\n\t\t\tCode:   0x0069,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"j\",\n\t\t\tCode:   0x006A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"k\",\n\t\t\tCode:   0x006B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"l\",\n\t\t\tCode:   0x006C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"m\",\n\t\t\tCode:   0x006D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11101100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"n\",\n\t\t\tCode:   0x006E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"o\",\n\t\t\tCode:   0x006F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"p\",\n\t\t\tCode:   0x0070,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"q\",\n\t\t\tCode:   0x0071,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"r\",\n\t\t\tCode:   0x0072,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"s\",\n\t\t\tCode:   0x0073,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"t\",\n\t\t\tCode:   0x0074,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"u\",\n\t\t\tCode:   0x0075,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"v\",\n\t\t\tCode:   0x0076,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"w\",\n\t\t\tCode:   0x0077,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11010110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"x\",\n\t\t\tCode:   0x0078,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"y\",\n\t\t\tCode:   0x0079,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"z\",\n\t\t\tCode:   0x007A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"{\",\n\t\t\tCode:   0x007B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"|\",\n\t\t\tCode:   0x007C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"}\",\n\t\t\tCode:   0x007D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"~\",\n\t\t\tCode:   0x007E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"⌂\",\n\t\t\tCode:   0x007F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Ç\",\n\t\t\tCode:   0x00C7,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ü\",\n\t\t\tCode:   0x00FC,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"é\",\n\t\t\tCode:   0x00E9,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"â\",\n\t\t\tCode:   0x00E2,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ä\",\n\t\t\tCode:   0x00E4,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"à\",\n\t\t\tCode:   0x00E0,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"å\",\n\t\t\tCode:   0x00E5,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ç\",\n\t\t\tCode:   0x00E7,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ê\",\n\t\t\tCode:   0x00EA,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ë\",\n\t\t\tCode:   0x00EB,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"è\",\n\t\t\tCode:   0x00E8,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ï\",\n\t\t\tCode:   0x00EF,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"î\",\n\t\t\tCode:   0x00EE,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ì\",\n\t\t\tCode:   0x00EC,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Ä\",\n\t\t\tCode:   0x00C4,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Å\",\n\t\t\tCode:   0x00C5,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"É\",\n\t\t\tCode:   0x00C9,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"æ\",\n\t\t\tCode:   0x00E6,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"01101110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Æ\",\n\t\t\tCode:   0x00C6,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ô\",\n\t\t\tCode:   0x00F4,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00010000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ö\",\n\t\t\tCode:   0x00F6,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ò\",\n\t\t\tCode:   0x00F2,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"û\",\n\t\t\tCode:   0x00FB,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ù\",\n\t\t\tCode:   0x00F9,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ÿ\",\n\t\t\tCode:   0x00FF,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Ö\",\n\t\t\tCode:   0x00D6,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Ü\",\n\t\t\tCode:   0x00DC,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"¢\",\n\t\t\tCode:   0x00A2,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"£\",\n\t\t\tCode:   0x00A3,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01100100\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"11111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"¥\",\n\t\t\tCode:   0x00A5,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"₧\",\n\t\t\tCode:   0x20A7,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"11000100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ƒ\",\n\t\t\tCode:   0x0192,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"á\",\n\t\t\tCode:   0x00E1,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"í\",\n\t\t\tCode:   0x00ED,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ó\",\n\t\t\tCode:   0x00F3,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ú\",\n\t\t\tCode:   0x00FA,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ñ\",\n\t\t\tCode:   0x00F1,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Ñ\",\n\t\t\tCode:   0x00D1,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11100110\"),\n\t\t\t\tcharRow(\"11110110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11011110\"),\n\t\t\t\tcharRow(\"11001110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ª\",\n\t\t\tCode:   0x00AA,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"º\",\n\t\t\tCode:   0x00BA,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"¿\",\n\t\t\tCode:   0x00BF,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"⌐\",\n\t\t\tCode:   0x2310,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"¬\",\n\t\t\tCode:   0x00AC,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"½\",\n\t\t\tCode:   0x00BD,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"10000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"¼\",\n\t\t\tCode:   0x00BC,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000010\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"11001110\"),\n\t\t\t\tcharRow(\"10011110\"),\n\t\t\t\tcharRow(\"00111110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"¡\",\n\t\t\tCode:   0x00A1,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"«\",\n\t\t\tCode:   0x00AB,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"»\",\n\t\t\tCode:   0x00BB,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"░\",\n\t\t\tCode:   0x2591,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t\tcharRow(\"00010001\"),\n\t\t\t\tcharRow(\"01000100\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▒\",\n\t\t\tCode:   0x2592,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t\tcharRow(\"01010101\"),\n\t\t\t\tcharRow(\"10101010\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▓\",\n\t\t\tCode:   0x2593,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t\tcharRow(\"11011101\"),\n\t\t\t\tcharRow(\"01110111\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"│\",\n\t\t\tCode:   0x2502,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"┤\",\n\t\t\tCode:   0x2524,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╡\",\n\t\t\tCode:   0x2561,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╢\",\n\t\t\tCode:   0x2562,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╖\",\n\t\t\tCode:   0x2556,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╕\",\n\t\t\tCode:   0x2555,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╣\",\n\t\t\tCode:   0x2563,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11110110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"11110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"║\",\n\t\t\tCode:   0x2551,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╗\",\n\t\t\tCode:   0x2557,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"11110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╝\",\n\t\t\tCode:   0x255D,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11110110\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╜\",\n\t\t\tCode:   0x255C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╛\",\n\t\t\tCode:   0x255B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"┐\",\n\t\t\tCode:   0x2510,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"└\",\n\t\t\tCode:   0x2514,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"┴\",\n\t\t\tCode:   0x2534,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"┬\",\n\t\t\tCode:   0x252C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"├\",\n\t\t\tCode:   0x251C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"─\",\n\t\t\tCode:   0x2500,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"┼\",\n\t\t\tCode:   0x253C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╞\",\n\t\t\tCode:   0x255E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╟\",\n\t\t\tCode:   0x255F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╚\",\n\t\t\tCode:   0x255A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110111\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╔\",\n\t\t\tCode:   0x2554,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111111\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╩\",\n\t\t\tCode:   0x2569,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11110111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╦\",\n\t\t\tCode:   0x2566,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11110111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╠\",\n\t\t\tCode:   0x2560,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110111\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00110111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"═\",\n\t\t\tCode:   0x2550,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╬\",\n\t\t\tCode:   0x256C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11110111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11110111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╧\",\n\t\t\tCode:   0x2567,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╨\",\n\t\t\tCode:   0x2568,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╤\",\n\t\t\tCode:   0x2564,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╥\",\n\t\t\tCode:   0x2565,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╙\",\n\t\t\tCode:   0x2559,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╘\",\n\t\t\tCode:   0x2558,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╒\",\n\t\t\tCode:   0x2552,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╓\",\n\t\t\tCode:   0x2553,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╫\",\n\t\t\tCode:   0x256B,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t\tcharRow(\"00110110\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"╪\",\n\t\t\tCode:   0x256A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"┘\",\n\t\t\tCode:   0x2518,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"┌\",\n\t\t\tCode:   0x250C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011111\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"█\",\n\t\t\tCode:   0x2588,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▄\",\n\t\t\tCode:   0x2584,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▌\",\n\t\t\tCode:   0x258C,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t\tcharRow(\"11110000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▐\",\n\t\t\tCode:   0x2590,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"▀\",\n\t\t\tCode:   0x2580,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"α\",\n\t\t\tCode:   0x03B1,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ß\",\n\t\t\tCode:   0x00DF,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11001100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Γ\",\n\t\t\tCode:   0x0393,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"π\",\n\t\t\tCode:   0x03C0,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Σ\",\n\t\t\tCode:   0x03A3,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"σ\",\n\t\t\tCode:   0x03C3,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"µ\",\n\t\t\tCode:   0x00B5,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"τ\",\n\t\t\tCode:   0x03C4,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Φ\",\n\t\t\tCode:   0x03A6,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Θ\",\n\t\t\tCode:   0x0398,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"Ω\",\n\t\t\tCode:   0x03A9,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"11101110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"δ\",\n\t\t\tCode:   0x03B4,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011110\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00111110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"01100110\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"∞\",\n\t\t\tCode:   0x221E,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"φ\",\n\t\t\tCode:   0x03C6,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000011\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"11011011\"),\n\t\t\t\tcharRow(\"11110011\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ε\",\n\t\t\tCode:   0x03B5,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011100\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"∩\",\n\t\t\tCode:   0x2229,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"11000110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"≡\",\n\t\t\tCode:   0x2261,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"±\",\n\t\t\tCode:   0x00B1,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11111111\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"≥\",\n\t\t\tCode:   0x2265,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000110\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"≤\",\n\t\t\tCode:   0x2264,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"⌠\",\n\t\t\tCode:   0x2320,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001110\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00011011\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"⌡\",\n\t\t\tCode:   0x2321,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"÷\",\n\t\t\tCode:   0x00F7,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111110\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"≈\",\n\t\t\tCode:   0x2248,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110110\"),\n\t\t\t\tcharRow(\"11011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"°\",\n\t\t\tCode:   0x00B0,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"∙\",\n\t\t\tCode:   0x2219,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"·\",\n\t\t\tCode:   0x00B7,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00011000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"√\",\n\t\t\tCode:   0x221A,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00001111\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"00001100\"),\n\t\t\t\tcharRow(\"11101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00111100\"),\n\t\t\t\tcharRow(\"00011100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"ⁿ\",\n\t\t\tCode:   0x207F,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"01101100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"²\",\n\t\t\tCode:   0x00B2,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01110000\"),\n\t\t\t\tcharRow(\"11011000\"),\n\t\t\t\tcharRow(\"00110000\"),\n\t\t\t\tcharRow(\"01100000\"),\n\t\t\t\tcharRow(\"11001000\"),\n\t\t\t\tcharRow(\"11111000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: \"■\",\n\t\t\tCode:   0x25A0,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"01111100\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t\tFontChar{\n\t\t\tString: string('\\u00A0'),\n\t\t\tCode:   0x00A0,\n\t\t\tWidth:  8,\n\t\t\tValue: [16]uint8{\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t\tcharRow(\"00000000\"),\n\t\t\t},\n\t\t},\n\t}\n\n\t// most of the use is going from a unicode rune to font char data\n\tfd.r2char = make(map[rune]FontChar)\n\tfor _, c := range fd.Chars {\n\t\tr, _ := utf8.DecodeRuneInString(c.String)\n\t\tfd.r2char[r] = c\n\t}\n\n\treturn &fd\n}\n\n// ParseColor parses a 6-byte or 8-byte hex color string (HTML-style) and\n// returns a color.RGBA. If anything goes wrong during parsing, it returns\n// the default provided.\nfunc (fd *FontData) ParseColor(in string, def color.Color) color.Color {\n\tif len(in) != 6 && len(in) != 8 {\n\t\treturn def\n\t}\n\tvar r, g, b, a uint8\n\n\tif r32, err := strconv.ParseInt(\"0x\"+in[0:2], 0, 32); err == nil {\n\t\tr = uint8(r32)\n\t} else {\n\t\tlog.Println(err)\n\t\treturn def\n\t}\n\n\tif g32, err := strconv.ParseInt(\"0x\"+in[2:4], 0, 32); err == nil {\n\t\tg = uint8(g32)\n\t} else {\n\t\tlog.Println(err)\n\t\treturn def\n\t}\n\n\tif b32, err := strconv.ParseInt(\"0x\"+in[4:6], 0, 32); err == nil {\n\t\tb = uint8(b32)\n\t} else {\n\t\tlog.Println(err)\n\t\treturn def\n\t}\n\n\tif len(in) == 8 {\n\t\tif a32, err := strconv.ParseInt(\"0x\"+in[6:8], 0, 32); err == nil {\n\t\t\ta = uint8(a32)\n\t\t} else {\n\t\t\tlog.Println(err)\n\t\t\treturn def\n\t\t}\n\t} else {\n\t\ta = 255\n\t}\n\n\treturn color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}\n}\n"
  },
  {
    "path": "hal/text2image_test.go",
    "content": "package hal_test\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"github.com/netflix/hal-9001/hal\"\n\t\"image/color\"\n\t\"testing\"\n)\n\nfunc Testtext2image(t *testing.T) {\n\tdef := color.RGBA{R: 1, G: 1, B: 1, A: 1}\n\n\tsamples := map[string][4]uint32{\n\t\t\"ffffff\":   [4]uint32{255, 255, 255, 255},\n\t\t\"ffffffff\": [4]uint32{255, 255, 255, 255},\n\t\t\"000000ff\": [4]uint32{0, 0, 0, 255},\n\t\t\"000000aa\": [4]uint32{0, 0, 0, 170},\n\t\t\"88888888\": [4]uint32{136, 136, 136, 136},\n\t\t\"888888\":   [4]uint32{136, 136, 136, 255},\n\t\t\"f79e10\":   [4]uint32{247, 158, 16, 255},\n\t\t\"f79e10ff\": [4]uint32{247, 158, 16, 255},\n\t}\n\n\tfd := hal.FixedFont()\n\n\tfor str, expected := range samples {\n\t\tresult := fd.ParseColor(str, def)\n\t\tt.Logf(\"%q => %q\\n\", str, result)\n\n\t\tr, g, b, a := result.RGBA()\n\n\t\tif r != expected[0] {\n\t\t\tt.Fail()\n\t\t}\n\t\tif g != expected[1] {\n\t\t\tt.Fail()\n\t\t}\n\t\tif b != expected[2] {\n\t\t\tt.Fail()\n\t\t}\n\t\tif a != expected[3] {\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "hal/ttlcache.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype ttlCache struct {\n\titems map[string]interface{}\n\ttimes map[string]time.Time\n\tttls  map[string]time.Duration\n\tmut   sync.Mutex // concurrent access\n\tinit  sync.Once  // initialize the singleton once\n}\n\nvar ttlcache ttlCache\n\nfunc Cache() *ttlCache {\n\tttlcache.init.Do(func() {\n\t\tttlcache.items = make(map[string]interface{})\n\t\tttlcache.times = make(map[string]time.Time)\n\t\tttlcache.ttls = make(map[string]time.Duration)\n\t})\n\n\treturn &ttlcache\n}\n\n// Get retreives a cached value and stores the result in the value pointed to by v.\n// The time to live is returned and may be 0 to indicate the item is expired.\n// e.g.\n// policies := []EscalationPolicy{}\n// err = hal.Cache().Set(\"pagerduty.escalation_policies\", &policies, time.Hour)\n// ttl, err := hal.Cache().Get(\"pagerduty.escalation_policies\", &policies)\n// if err != nil { panic(err) }\n// if ttl == 0 { panic(\"stale cache!\") }\nfunc (cache *ttlCache) Get(key string, v interface{}) (time.Duration, error) {\n\tcache.mut.Lock()\n\tdefer cache.mut.Unlock()\n\n\tttl := time.Duration(0)\n\tage := time.Now().Sub(cache.times[key])\n\tif age.Seconds() < cache.ttls[key].Seconds() {\n\t\t// not expired, compute the ttl\n\t\tttlsecs := cache.ttls[key].Seconds() - age.Seconds()\n\t\tttl = time.Duration(int(ttlsecs)) * time.Second\n\t}\n\n\tcached := cache.items[key]\n\tvtype := reflect.TypeOf(v)\n\tctype := reflect.TypeOf(cached)\n\n\t// make sure the input type matches the type in the cache\n\tif vtype != ctype {\n\t\tmsg := fmt.Sprintf(\"Type mismatch: got %q, expected %q\", vtype.Name(), ctype.Name())\n\t\treturn ttl, errors.New(msg)\n\t}\n\n\t// make sure it's a pointer and is not nil\n\tvval := reflect.ValueOf(v)\n\tif vval.Kind() != reflect.Ptr || vval.IsNil() {\n\t\treturn ttl, errors.New(\"The second argument of Get() must be a non-nil pointer.\")\n\t}\n\n\t// set the value\n\tcval := reflect.ValueOf(cached)\n\tvval.Elem().Set(cval.Elem())\n\n\treturn ttl, nil\n}\n\nfunc (cache *ttlCache) Set(key string, v interface{}, ttl time.Duration) {\n\tcache.mut.Lock()\n\tdefer cache.mut.Unlock()\n\n\tcache.items[key] = v\n\tcache.times[key] = time.Now()\n\tcache.ttls[key] = ttl\n}\n\nfunc (cache *ttlCache) Delete(key string) {\n\tcache.mut.Lock()\n\tdefer cache.mut.Unlock()\n\n\tdelete(cache.items, key)\n\tdelete(cache.times, key)\n\tdelete(cache.ttls, key)\n}\n\nfunc (cache *ttlCache) Exists(key string) bool {\n\tcache.mut.Lock()\n\tdefer cache.mut.Unlock()\n\n\t_, exists := cache.ttls[key]\n\treturn exists\n}\n\n// Age returns the age as time.Duration for the given key. Returns duration 0\n// for keys that don't exist.\nfunc (cache *ttlCache) Age(key string) time.Duration {\n\tcache.mut.Lock()\n\tdefer cache.mut.Unlock()\n\n\tif val, exists := cache.times[key]; exists {\n\t\treturn time.Now().Sub(val)\n\t}\n\n\treturn time.Duration(0)\n}\n\n// Ttl returns the time-to-live for the given key. Returns a TTL of 0 for\n// keys that don't exist.\nfunc (cache *ttlCache) Ttl(key string) time.Duration {\n\tcache.mut.Lock()\n\tdefer cache.mut.Unlock()\n\n\tif val, exists := cache.ttls[key]; exists {\n\t\treturn val\n\t}\n\n\treturn time.Duration(0)\n}\n"
  },
  {
    "path": "hal/ttlcache_test.go",
    "content": "package hal_test\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\ntype Whatever struct {\n\tField1 string\n\tField2 int\n\tField3 map[string]string\n}\n\nfunc TestTtlCache(t *testing.T) {\n\tw1 := Whatever{\n\t\tField1: \"testing\",\n\t\tField2: 9,\n\t\tField3: map[string]string{\"foo\": \"bar\"},\n\t}\n\n\tcache := hal.Cache()\n\tcache.Set(\"whatever\", &w1, time.Hour*24)\n\n\tw2 := Whatever{}\n\tttl, err := cache.Get(\"whatever\", &w2)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tif ttl == 0 {\n\t\tt.Error(\"ttl expired way too early\")\n\t\tt.Fail()\n\t}\n\n\tif w2.Field2 != w1.Field2 {\n\t\tt.Error(\"Field2 doesn't match\")\n\t\tt.Fail()\n\t}\n\n\tif w2.Field3[\"foo\"] != \"bar\" {\n\t\tt.Error(\"Field3 doesn't match\")\n\t\tt.Fail()\n\t}\n}\n"
  },
  {
    "path": "hal/utf8table.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// Utf8Table is like AsciiTable but it uses UTF8 characters for the table\n// borders. It should look nice when rendered to an image by Hal's text->img.\nfunc Utf8Table(hdr []string, rows [][]string) string {\n\tif len(rows) == 0 {\n\t\treturn \"NO DATA TO DISPLAY\"\n\t} else if len(rows[0]) == 0 {\n\t\tpanic(\"BUG: the first row seems to be empty!\")\n\t}\n\n\t// find the needed width of each column\n\tcolwidths := make([]int, len(hdr))\n\t// start with the headers' widths\n\tfor j, col := range hdr {\n\t\tcolwidths[j] = len(col)\n\t}\n\n\t// bump to the size of any larger cells\n\tfor i, row := range rows {\n\t\t// handle empty/short rows gracefully by reallocating which\n\t\t// results in a default value of \"\"\n\t\tif len(row) < len(hdr) {\n\t\t\tnewrow := make([]string, len(hdr))\n\t\t\tcopy(newrow[0:len(row)], row)\n\t\t\trows[i] = newrow\n\t\t\trow = newrow\n\t\t}\n\n\t\tfor j, col := range row {\n\t\t\tif colwidths[j] < len(col) {\n\t\t\t\tcolwidths[j] = len(col)\n\t\t\t}\n\t\t}\n\t}\n\n\t// generate format strings for the columns\n\tfmts := make([]string, len(colwidths))\n\thdrtop := make([]string, len(colwidths)) // top frame, above header row\n\thdrbot := make([]string, len(colwidths)) // top frame, below header row\n\ttblbot := make([]string, len(colwidths)) // bottom of table\n\n\t/* some unicode references - these are available in Hal's image renderer\n\t   ╔═════╦═════╗ ╔═══╤═══╗ ╔══╤══╤══╗\n\t   ║     ║     ║ ╟───┼───╢ ╟──┼──┼──╢\n\t   ╚═════╩═════╝ ╚═══╧═══╝ ╚══╧══╧══╝\n\t*/\n\tif len(colwidths) > 1 {\n\t\tfor i, width := range colwidths {\n\t\t\tif i == 0 {\n\t\t\t\t// first column\n\t\t\t\tfmts[i] = fmt.Sprintf(\"║ %%%ds │\", width)\n\t\t\t\thdrtop[i] = fmt.Sprintf(\"╔%s╤\", strings.Repeat(\"═\", width+2))\n\t\t\t\thdrbot[i] = fmt.Sprintf(\"╟%s┼\", strings.Repeat(\"─\", width+2))\n\t\t\t\ttblbot[i] = fmt.Sprintf(\"╚%s╧\", strings.Repeat(\"═\", width+2))\n\t\t\t} else if i == len(colwidths)-1 {\n\t\t\t\t// last column\n\t\t\t\tfmts[i] = fmt.Sprintf(\" %%%ds ║\\n\", width)\n\t\t\t\thdrtop[i] = fmt.Sprintf(\"%s╗\\n\", strings.Repeat(\"═\", width+2))\n\t\t\t\thdrbot[i] = fmt.Sprintf(\"%s╢\\n\", strings.Repeat(\"─\", width+2))\n\t\t\t\ttblbot[i] = fmt.Sprintf(\"%s╝\\n\", strings.Repeat(\"═\", width+2))\n\t\t\t} else {\n\t\t\t\t// inner columns\n\t\t\t\tfmts[i] = fmt.Sprintf(\" %%%ds │\", width)\n\t\t\t\thdrtop[i] = fmt.Sprintf(\"%s╤\", strings.Repeat(\"═\", width+2))\n\t\t\t\thdrbot[i] = fmt.Sprintf(\"%s┼\", strings.Repeat(\"─\", width+2))\n\t\t\t\ttblbot[i] = fmt.Sprintf(\"%s╧\", strings.Repeat(\"═\", width+2))\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// single-column tables\n\t\tfmts[0] = fmt.Sprintf(\"║ %%%ds ║\\n\", colwidths[0])\n\t\thdrtop[0] = fmt.Sprintf(\"╔%s╗\\n\", strings.Repeat(\"═\", colwidths[0]+2))\n\t\thdrbot[0] = fmt.Sprintf(\"╟%s╢\\n\", strings.Repeat(\"─\", colwidths[0]+2))\n\t\ttblbot[0] = fmt.Sprintf(\"╚%s╝\\n\", strings.Repeat(\"═\", colwidths[0]+2))\n\t}\n\n\tbuf := bytes.NewBuffer([]byte{})\n\n\t// top of header (frame)\n\t// ╔══════╤═══════╗\n\tfmt.Fprint(buf, strings.Join(hdrtop, \"\"))\n\n\t// header columns (text + frame)\n\t// ║ left │ right ║\n\tfor j, col := range hdr {\n\t\tfmt.Fprintf(buf, fmts[j], col)\n\t}\n\n\t// between header & data (frame)\n\t// ╟──────┼───────╢\n\tfmt.Fprintf(buf, strings.Join(hdrbot, \"\"))\n\n\t// data rows\n\t// ║  one │ three ║\n\t// ║  two │       ║\n\tfor _, row := range rows {\n\t\tfor j, col := range row {\n\t\t\tfmt.Fprintf(buf, fmts[j], col)\n\t\t}\n\t}\n\n\t// bottom frame\n\t// ╚══════╧═══════╝\n\tfmt.Fprintf(buf, strings.Join(tblbot, \"\"))\n\n\treturn buf.String()\n}\n"
  },
  {
    "path": "hal/utf8table_test.go",
    "content": "package hal\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestUtf8Table(t *testing.T) {\n\tsamples := [][][]string{\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t},\n\t\t{\n\t\t\t{\"hdr\"},\n\t\t\t{\"one\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"left\", \"right\"},\n\t\t\t{\"one\", \"three\"},\n\t\t\t{\"two\"},\n\t\t},\n\t\t{\n\t\t\t{\"HEADER 1\", \"HDR 2\", \"LOL WUT\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t},\n\t\t{\n\t\t\t{\"Col 1\", \"Col 2\", \"3rd Column\", \"4th\", \"FIFTH\"},\n\t\t\t{\"one\", \"two\", \"three\"},\n\t\t\t{\"four\", \"five\", \"six\"},\n\t\t\t{\"hi\"},\n\t\t\t{\"\", \"\", \"\", \"-\", \"+\"},\n\t\t},\n\t}\n\n\tvar results [5]string\n\n\tresults[0] = `\n╔═════╗\n║ hdr ║\n╟─────╢\n║ one ║\n╚═════╝\n`\n\n\tresults[1] = `\n╔═════╗\n║ hdr ║\n╟─────╢\n║ one ║\n║ two ║\n╚═════╝\n`\n\n\tresults[2] = `\n╔══════╤═══════╗\n║ left │ right ║\n╟──────┼───────╢\n║  one │ three ║\n║  two │       ║\n╚══════╧═══════╝\n`\n\n\tresults[3] = `\n╔══════════╤═══════╤═════════╗\n║ HEADER 1 │ HDR 2 │ LOL WUT ║\n╟──────────┼───────┼─────────╢\n║      one │   two │   three ║\n║     four │  five │     six ║\n╚══════════╧═══════╧═════════╝\n`\n\n\tresults[4] = `\n╔═══════╤═══════╤════════════╤═════╤═══════╗\n║ Col 1 │ Col 2 │ 3rd Column │ 4th │ FIFTH ║\n╟───────┼───────┼────────────┼─────┼───────╢\n║   one │   two │      three │     │       ║\n║  four │  five │        six │     │       ║\n║    hi │       │            │     │       ║\n║       │       │            │   - │     + ║\n╚═══════╧═══════╧════════════╧═════╧═══════╝\n`\n\n\tfor i, sample := range samples {\n\t\t// first row is the header, the rest is data rows\n\t\tout := Utf8Table(sample[0], sample[1:])\n\n\t\tif len(out) == 0 {\n\t\t\tt.Fail()\n\t\t}\n\n\t\ttrout := strings.TrimSpace(out)\n\t\ttrres := strings.TrimSpace(results[i])\n\n\t\tif trout != trres {\n\t\t\tt.Logf(\"Got: \\n%s\\nExpected:\\n%s\\n\", trout, trres)\n\t\t\tt.Fail()\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "plugins/archive/plugin.go",
    "content": "package archive\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n\t\"github.com/nlopes/slack\"\n)\n\nvar log hal.Logger\n\n// ArchiveEntry is a single event observed by the archive plugin.\ntype ArchiveEntry struct {\n\tID        string    `json:\"id\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n\tUser      string    `json:\"user\"`\n\tRoom      string    `json:\"room\"`\n\tBroker    string    `json:\"broker\"`\n\tBody      string    `json:\"body\"`\n\tReactions []string  `json:\"reactions\"`\n}\n\n// ArchiveTable stores events for posterity.\n// The brokers currently supported do not provide a surrogate event id\n// and instead rely on the timestamp/user/room for identity.\nconst ArchiveTable = `\nCREATE TABLE IF NOT EXISTS archive (\n  id       VARCHAR(191),\n  user     VARCHAR(191),\n  room     VARCHAR(191),\n  broker   VARCHAR(191),\n  ts       TIMESTAMP,\n  body     TEXT,\n  PRIMARY KEY (id,user,room,broker)\n)`\n\nconst ReactionTable = `\nCREATE TABLE IF NOT EXISTS reactions (\n  id       VARCHAR(191),\n  user     VARCHAR(191),\n  room     VARCHAR(191),\n  broker   VARCHAR(191),\n  ts       TIMESTAMP,\n  reaction VARCHAR(191),\n  PRIMARY KEY (ts,user,room,broker)\n)`\n\nfunc Register() {\n\tarchive := hal.Plugin{\n\t\tName:      \"message_archive\",\n\t\tFunc:      archiveRecorder,\n\t\tBotEvents: true,\n\t}\n\tarchive.Register()\n\n\treactions := hal.Plugin{\n\t\tName:      \"reaction_tracker\",\n\t\tFunc:      archiveReaction,\n\t\tBotEvents: true,\n\t}\n\treactions.Register()\n\n\t// apply the schema to the database as necessary\n\thal.SqlInit(ArchiveTable)\n\thal.SqlInit(ReactionTable)\n\n\thttp.HandleFunc(\"/v1/archive\", httpGetArchive)\n}\n\n// ArchiveRecorder inserts every message received into the database for use\n// by other parts of the system.\nfunc archiveRecorder(evt hal.Evt) {\n\t// ignore non-chat events for the archive (e.g. reaction added, etc.)\n\tif !evt.IsChat {\n\t\treturn\n\t}\n\n\t// ignore bot commands prefixed with !\n\tif strings.HasPrefix(strings.TrimSpace(evt.Body), \"!\") {\n\t\treturn\n\t}\n\n\tsql := `INSERT INTO archive (id, user, room, broker, ts, body) VALUES (?, ?, ?, ?, ?, ?)`\n\t_, err := hal.SqlDB().Exec(sql, evt.ID, evt.UserId, evt.RoomId, evt.BrokerName(), evt.Time, evt.Body)\n\tif err != nil {\n\t\tlog.Printf(\"Could not insert event into archive: %s\\n\", err)\n\t}\n}\n\n// archiveReactionAdded switches on the type of the original message and calls a\n// broker-specific function to pull out the reaction and write it to the database.\nfunc archiveReaction(evt hal.Evt) {\n\t// ignore events marked as chats since they can't be reactions\n\tif evt.IsChat {\n\t\treturn\n\t}\n\n\tswitch evt.Original.(type) {\n\tcase *slack.ReactionAddedEvent:\n\t\tlog.Printf(\"adding reaction: (%T) %q\\n\", evt.Original, evt.Body)\n\t\trae := evt.Original.(*slack.ReactionAddedEvent)\n\t\tinsertReaction(evt.Time, rae.Item.Timestamp, evt.UserId, evt.RoomId, evt.BrokerName(), rae.Reaction)\n\tcase *slack.ReactionRemovedEvent:\n\t\tlog.Printf(\"deleting reaction: (%T) %q\\n\", evt.Original, evt.Body)\n\t\trre := evt.Original.(*slack.ReactionRemovedEvent)\n\n\t\t// TODO: handle files & file comments\n\t\tdeleteReaction(rre.Item.Timestamp, evt.UserId, rre.Item.Channel, evt.BrokerName(), rre.Reaction)\n\tdefault:\n\t\treturn\n\t}\n}\n\nfunc insertReaction(ts time.Time, id, user, room, broker, reaction string) {\n\tsql := `INSERT INTO reactions (id,user,room,broker,ts,reaction) VALUES (?,?,?,?,?,?)`\n\t_, err := hal.SqlDB().Exec(sql, id, user, room, broker, ts, reaction)\n\tif err != nil {\n\t\tlog.Printf(\"Could not insert reaction into reactions table: %s\\n\", err)\n\t}\n}\n\nfunc deleteReaction(id, user, room, broker, reaction string) {\n\tsql := `DELETE FROM reactions WHERE id=? AND user=? AND room=? AND broker=? AND reaction=?`\n\t_, err := hal.SqlDB().Exec(sql, id, user, room, broker, reaction)\n\tif err != nil {\n\t\tlog.Printf(\"Could not delete reaction from reactions table: %s\\n\", err)\n\t}\n}\n\n// httpGetArchive retreives the 50 latest items from the event archive.\nfunc httpGetArchive(w http.ResponseWriter, r *http.Request) {\n\taes, err := FetchArchive(50)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"could not fetch message archive: '%s'\", err), 500)\n\t\treturn\n\t}\n\n\tjs, err := json.Marshal(aes)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"could not marshal archive to json: '%s'\", err), 500)\n\t\treturn\n\t}\n\n\tw.Write(js)\n}\n\n// FetchArchive selects messages from the archive table up to the provided number of messages limit.\nfunc FetchArchive(limit int) ([]*ArchiveEntry, error) {\n\tdb := hal.SqlDB()\n\n\t// joining reactions in here for now - might be better to let the client do it\n\t// but for now get something working\n\t// This pulls back multiple rows if there are multiple reactions. The row iteration\n\t// below uses a map to dedupe the archive rows and put reactions into a list.\n\t// This might be better written with GROUP_CONCAT later...\n\tsql := `SELECT a.id AS id,\n\t               UNIX_TIMESTAMP(a.ts) AS ts,\n\t\t\t\t   a.user AS user,\n\t\t\t\t   a.room AS room,\n\t\t\t\t   a.broker AS broker,\n\t\t\t\t   a.body AS body,\n\t\t\t\t   IFNULL(r.reaction,\"\") AS reaction\n\t          FROM archive a\n\t\t\t  LEFT OUTER JOIN reactions r ON ( r.id = a.id AND r.room = a.room )\n\t\t\t  WHERE a.ts < ? AND a.ts > ?\n\t\t\t  GROUP BY a.id\n\t\t\t  ORDER BY a.ts DESC`\n\n\tnow := time.Now()\n\tyesterday := now.Add(-time.Hour * 24)\n\trows, err := db.Query(sql, &now, &yesterday)\n\tif err != nil {\n\t\tlog.Printf(\"archive query failed: %s\\n\", err)\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tentries := make(map[string]*ArchiveEntry)\n\n\tfor rows.Next() {\n\t\tvar ts int64\n\t\tvar id, room, user, broker, body, reaction string\n\t\terr = rows.Scan(&id, &ts, &user, &room, &broker, &body, &reaction)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Row iteration failed: %s\\n\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif entry, exists := entries[id]; exists {\n\t\t\tif reaction != \"\" {\n\t\t\t\tentry.Reactions = append(entry.Reactions, reaction)\n\t\t\t}\n\t\t} else {\n\t\t\tae := ArchiveEntry{\n\t\t\t\tID:        id,\n\t\t\t\tTimestamp: time.Unix(ts, 0),\n\t\t\t\tBroker:    broker,\n\t\t\t\tBody:      body,\n\t\t\t\tReactions: []string{},\n\t\t\t}\n\n\t\t\tif reaction != \"\" {\n\t\t\t\tae.Reactions = append(ae.Reactions, reaction)\n\t\t\t}\n\n\t\t\t// convert ids to names\n\t\t\tbroker := hal.Router().GetBroker(ae.Broker)\n\t\t\tae.Room = broker.RoomIdToName(room)\n\t\t\tae.User = broker.UserIdToName(user)\n\n\t\t\tentries[id] = &ae\n\t\t}\n\t}\n\n\t// hmm might want to sort these before sending...\n\taes := make([]*ArchiveEntry, len(entries))\n\tvar i int\n\tfor _, ae := range entries {\n\t\taes[i] = ae\n\t\ti++\n\t}\n\n\treturn aes, nil\n}\n"
  },
  {
    "path": "plugins/blabber/plugin.go",
    "content": "// blabber records events as word pairs with counts and can\n// use that data to generate text\n// This is an experiment and work-in-progress. I haven't built\n// one of these before...\npackage blabber\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\ntype wncRow struct {\n\tword  string\n\tnext  string\n\tcount int\n}\n\ntype qFrag struct {\n\tempty  bool\n\tsql    string\n\tparams []interface{}\n}\n\n// a table tracking words said in the attached chatroom, hopefully\n// suitable for quick & dirty markov-style chatterbot stuff\nconst BLABBERWORDS_TABLE = `\nCREATE TABLE IF NOT EXISTS blabberwords (\n  word     VARCHAR(191),  -- the word seen in the room\n  user     VARCHAR(191),  -- the user who said it\n  room     VARCHAR(191),  -- the chat room it was said in\n  next     VARCHAR(191),  -- the word after it\n  count    int,           -- how many times this pair has been seen\n  ts       TIMESTAMP,     -- when it was last said (not indexed for now)\n  PRIMARY KEY (word, user, room, next)\n)`\n\nfunc Register() {\n\tbw := hal.Plugin{\n\t\tName: \"blabberwords\",\n\t\tFunc: bwCounter,\n\t}\n\tbw.Register()\n\n\tbb := hal.Plugin{\n\t\tName:    \"blab\",\n\t\tFunc:    blab,\n\t\tCommand: \"blab\",\n\t}\n\tbb.Register()\n\n\t// apply the schema to the database as necessary\n\thal.SqlInit(BLABBERWORDS_TABLE)\n}\n\nfunc bwCounter(evt hal.Evt) {\n\tparts := evt.BodyAsArgv()\n\n\t// ignore really short lines or commands\n\t// TODO: ignore the bot too and add prefs for things to ignore\n\tif len(parts) < 2 || strings.HasPrefix(strings.TrimSpace(parts[0]), \"!\") {\n\t\treturn\n\t}\n\n\tdb := hal.SqlDB()\n\n\tsql := `INSERT INTO blabberwords\n\t          (word,user,room,next,count)\n\t        VALUES (?, ?, ?, ?, 1)\n\t        ON DUPLICATE KEY UPDATE\n\t\t\t  count=values(count) + 1`\n\n\tquery, err := db.Prepare(sql)\n\tif err != nil {\n\t\tlog.Printf(\"Could not prepare insert query: %s\", err)\n\t\treturn\n\t}\n\n\tfor i, word := range parts {\n\t\tnext := \"\"\n\t\t// first word will have word=\"\", next=\"first\"\n\t\t// last word will have word=\"whatever\", next=\"\"\n\t\tif i == 0 {\n\t\t\tnext = word\n\t\t\tword = \"\"\n\t\t} else if i < len(parts)-1 {\n\t\t\tnext = parts[i+1]\n\t\t}\n\n\t\ttword := strings.TrimRight(word, \".?!\")\n\t\ttnext := strings.TrimRight(next, \".?!\")\n\n\t\t_, err = query.Exec(tword, evt.User, evt.Room, tnext)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"prepared insert into blabberwords failed: %s\", err)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\n// !blab --user atobey\n// !blab --user atobey --room incidents\n// !blab --room incidents\n// !blab --user atobey,dhahn,jhorowitz ???\n// !blab --user dhahn\n// TODO: figure out a non-insane way to build a sentence around a specific word or words\nfunc blab(evt hal.Evt) {\n\tusers := []string{}\n\trooms := []string{}\n\targv := evt.BodyAsArgv()\n\n\tfor i, arg := range argv {\n\t\tswitch arg {\n\t\tcase \"--user\":\n\t\t\tfound := extractArgs(argv, i)\n\t\t\tusers = append(users, found...)\n\t\tcase \"--room\":\n\t\t\tfound := extractArgs(argv, i)\n\t\t\trooms = append(rooms, found...)\n\t\t}\n\t}\n\n\tuserFrag := mkQueryFragment(\"user\", users)\n\troomFrag := mkQueryFragment(\"room\", rooms)\n\n\t// start with a random first word given the provided constraints\n\tfirst := firstWord(userFrag, roomFrag)\n\twords := []wncRow{first}\n\tfor {\n\t\tnext := nextWord(words[len(words)-1], userFrag, roomFrag)\n\t\twords = append(words, next)\n\n\t\tlog.Printf(\"BLAB: %+v\", words)\n\n\t\t// found a last word\n\t\tif next.next == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\t// stop trying after 20 words\n\t\tif len(words) > 20 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tevt.Reply(rowsToString(words))\n}\n\n// for now, completely random, will add in probability later...\nfunc nextWord(current wncRow, userFrag, roomFrag qFrag) wncRow {\n\tsqlbuf := bytes.NewBufferString(\"SELECT word,next,count FROM blabberwords WHERE word=? \")\n\tparams := []interface{}{current.next}\n\n\tif !userFrag.empty {\n\t\tsqlbuf.WriteString(\" AND \")\n\t\tsqlbuf.WriteString(userFrag.sql)\n\t\tparams = append(params, userFrag.params...)\n\t}\n\n\tif !roomFrag.empty {\n\t\tsqlbuf.WriteString(\" AND \")\n\t\tsqlbuf.WriteString(roomFrag.sql)\n\t\tparams = append(params, roomFrag.params...)\n\t}\n\n\trows := getRows(sqlbuf.String(), params)\n\n\tif len(rows) == 0 {\n\t\tlog.Printf(\"blabber.nextWord got 0 rows, returning empty row\")\n\t\treturn wncRow{\"\", \"\", 0}\n\t}\n\n\tidx := rand.Intn(len(rows) - 1)\n\treturn rows[idx]\n}\n\nfunc rowsToString(rows []wncRow) string {\n\twords := make([]string, len(rows))\n\n\tfor i, val := range rows {\n\t\twords[i] = val.word\n\t}\n\n\treturn strings.Join(words, \" \")\n}\n\nfunc getRows(sql string, params []interface{}) []wncRow {\n\tdb := hal.SqlDB()\n\n\tlog.Printf(\"Running query: %q\\n%+v\\n\", sql, params)\n\n\trows, err := db.Query(sql, params...)\n\tif err != nil {\n\t\tlog.Printf(\"blabberwords query %q failed: %s\", sql, err)\n\t\treturn []wncRow{}\n\t}\n\n\twncs := []wncRow{}\n\tfor rows.Next() {\n\t\twnc := wncRow{}\n\t\terr = rows.Scan(&wnc.word, &wnc.next, &wnc.count)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"blabberwords query scan failed: %s\", err)\n\t\t\treturn wncs\n\t\t}\n\n\t\twncs = append(wncs, wnc)\n\t}\n\n\treturn wncs\n}\n\nfunc firstWord(userFrag, roomFrag qFrag) wncRow {\n\tsqlbuf := bytes.NewBufferString(\"SELECT word,next,count FROM blabberwords WHERE word='' \")\n\tparams := []interface{}{}\n\n\tif !userFrag.empty {\n\t\tsqlbuf.WriteString(\" AND \")\n\t\tsqlbuf.WriteString(userFrag.sql)\n\t\tparams = append(params, userFrag.params...)\n\t}\n\n\tif !roomFrag.empty {\n\t\tsqlbuf.WriteString(\" AND \")\n\t\tsqlbuf.WriteString(roomFrag.sql)\n\t\tparams = append(params, roomFrag.params...)\n\t}\n\n\t// will get back a list (potentially large) of candidates\n\twncs := getRows(sqlbuf.String(), params)\n\n\t// when now rows are returned, just say \"FAIL\"\n\tif len(wncs) == 0 {\n\t\treturn wncRow{\"FAIL\", \"\", 0}\n\t}\n\n\tidx := rand.Intn(len(wncs) - 1)\n\n\treturn wncs[idx]\n}\n\nfunc mkQueryFragment(col string, list []string) qFrag {\n\tif len(list) == 0 {\n\t\treturn qFrag{true, \"\", []interface{}{}}\n\t}\n\n\tparams := make([]interface{}, len(list))\n\tfrags := make([]string, len(list))\n\n\tfor i, item := range list {\n\t\tfrags[i] = fmt.Sprintf(\"%s=?\", col)\n\t\tparams[i] = item\n\t}\n\n\tsql := \" ( \" + strings.Join(frags, \" OR \") + \" ) \"\n\n\treturn qFrag{false, sql, params}\n}\n\nfunc extractArgs(argv []string, i int) []string {\n\tout := []string{}\n\n\t// out of bounds, nothing to do\n\tif i < len(argv)-2 {\n\t\treturn out\n\t}\n\n\tclean := strings.Replace(argv[i+1], \" \", \"\", -1)\n\treturn strings.Split(clean, \",\")\n}\n"
  },
  {
    "path": "plugins/cross_the_streams/plugin.go",
    "content": "// cross_the_streams replicates messages between brokers\npackage cross_the_streams\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\n// Register makes this plugin available to the system.\nfunc Register() {\n\tplugin := hal.Plugin{\n\t\tName: \"cross_the_streams\",\n\t\tFunc: crossStreams,\n\t\t//  source: Pref.Room / Pref.Broker\n\t\tSettings: hal.Prefs{\n\t\t\thal.Pref{Plugin: \"cross_the_streams\", Key: \"to.broker\"},\n\t\t\thal.Pref{Plugin: \"cross_the_streams\", Key: \"to.room\"},\n\t\t},\n\t}\n\n\tplugin.Register()\n}\n\n// crossStreams looks at events it recieves and repeats them\n// to a different broker.\nfunc crossStreams(evt hal.Evt) {\n\tprefs := evt.InstanceSettings()\n\ttbPrefs := prefs.Key(\"to.broker\")\n\ttrPrefs := prefs.Key(\"to.room\")\n\n\t// no matches, move on\n\tif len(tbPrefs) == 0 || len(trPrefs) == 0 {\n\t\treturn\n\t}\n\n\ttoBroker := tbPrefs[0].Value\n\ttoRoomId := trPrefs[0].Value\n\n\ttb := hal.Router().GetBroker(toBroker)\n\tif tb != nil {\n\t\ttoRoom := tb.RoomIdToName(toRoomId)\n\t\tbody := fmt.Sprintf(\"%s %s@%s: %s\", evt.Time, evt.User, evt.Room, evt.Body)\n\t\tout := hal.Evt{\n\t\t\tBody:   body,\n\t\t\tRoom:   toRoom,\n\t\t\tRoomId: toRoomId,\n\t\t\tTime:   evt.Time,\n\t\t\tBroker: tb,\n\t\t}\n\t\ttb.Send(out)\n\t} else {\n\t\tlog.Printf(\"hal.Router does not know about a broker named %q\", toBroker)\n\t}\n}\n"
  },
  {
    "path": "plugins/docker/plugin.go",
    "content": "// Package docker allows users to attach a Docker image to a room and interact\n// with it over its stdin/stdout.\npackage docker\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"os/exec\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nconst Name = \"docker\"\n\nconst Usage = `\nExamples:\n!docker images\n!docker run\n`\n\n// Register makes this plugin available to the system.\nfunc Register() {\n\tplugin := hal.Plugin{\n\t\tName:    Name,\n\t\tFunc:    docker,\n\t\tCommand: \"docker\",\n\t}\n\n\tplugin.Register()\n}\n\nfunc docker(evt hal.Evt) {\n\targv := evt.BodyAsArgv()\n\n\tif len(argv) < 2 {\n\t\tevt.Reply(Usage)\n\t\treturn\n\t}\n\n\tswitch argv[1] {\n\tcase \"images\":\n\t\timages(evt)\n\tcase \"run\":\n\t\tif len(argv) < 3 {\n\t\t\tevt.Replyf(\"docker run requires an image id!\\n%s\", Usage)\n\t\t\treturn\n\t\t}\n\t\trun(evt, argv)\n\t}\n}\n\n// TODO: the idea is to be able to run an interactive container that may be more\n// than a single command, e.g. an old-school question/answer script that asks a\n// few questions then does some work. This will probably require a timeout\n// and some way to either signal which container you're messaging or spawn a\n// DM room for the purpose and perhaps send the output back to the originating\n// room. The DM approach is likely least complex, even across brokers.\nfunc run(evt hal.Evt, argv []string) {\n\t// danger! insecure! Demo code ;)\n\tcmd := exec.Command(\"/usr/bin/docker\", argv[1:]...)\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\tevt.Replyf(\"Encountered an error while running 'docker run %s': %s\", argv[2], err)\n\t}\n\n\tevt.Reply(string(out))\n}\n\nfunc images(evt hal.Evt) {\n\tcmd := exec.Command(\"/usr/bin/docker\", \"images\")\n\tout, err := cmd.Output()\n\tif err != nil {\n\t\tevt.Replyf(\"Encountered an error while running 'docker images': %s\", err)\n\t}\n\n\tevt.Reply(string(out))\n}\n"
  },
  {
    "path": "plugins/google_calendar/google.go",
    "content": "package google_calendar\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n\n\t\"golang.org/x/oauth2\"\n\t\"golang.org/x/oauth2/google\"\n\t\"google.golang.org/api/calendar/v3\"\n)\n\nconst oauthJsonKey = `google-calendar-oauth-client-json`\n\n// a simplified calendar event returned by getEvents\ntype CalEvent struct {\n\tStart       time.Time\n\tEnd         time.Time\n\tAllDay      bool\n\tName        string\n\tDescription string\n}\n\ntype GoogleError struct {\n\tParent error\n}\n\nfunc (e GoogleError) Error() string {\n\treturn fmt.Sprintf(\"Failed while communicating with Google Calender: %s\", e.Parent.Error())\n}\n\ntype PrefMissingError struct{}\n\nfunc (e PrefMissingError) Error() string {\n\treturn `the calendar-id pref must be set for the room. Try:\n!prefs set --room * --plugin google_calendar --key calendar-id --value <id>`\n}\n\ntype SecretMissingError struct{}\n\nfunc (e SecretMissingError) Error() string {\n\treturn \"the google-calendar-oauth-client-json secret must be set. Contact the bot admin.\"\n}\n\nfunc getEvents(calendarId string, now time.Time) ([]CalEvent, error) {\n\t// TODO: figure out if it's feasible to have one secret per bot or\n\t// if it really needs to be per-calendar or room\n\t// TODO: this should probably be passed to this function rather than\n\t// making this file require hal\n\tsecrets := hal.Secrets()\n\tjsonData := secrets.Get(\"google-calendar-oauth-client-json\")\n\tif jsonData == \"\" {\n\t\treturn nil, SecretMissingError{}\n\t}\n\n\tconfig, err := google.JWTConfigFromJSON([]byte(jsonData), calendar.CalendarReadonlyScope)\n\tif err != nil {\n\t\treturn nil, GoogleError{err}\n\t}\n\tclient := config.Client(oauth2.NoContext)\n\tcal, err := calendar.New(client)\n\tif err != nil {\n\t\treturn nil, GoogleError{err}\n\t}\n\n\tmin := now.Add(time.Hour * -1).Format(time.RFC3339)\n\tmax := now.Add(time.Hour * 24).Format(time.RFC3339)\n\tevents, err := cal.Events.List(calendarId).\n\t\tShowDeleted(false).\n\t\tSingleEvents(true).\n\t\tTimeMin(min).\n\t\tTimeMax(max).\n\t\tDo()\n\n\tif err != nil {\n\t\treturn nil, GoogleError{err}\n\t}\n\n\tout := make([]CalEvent, len(events.Items))\n\tfor i, event := range events.Items {\n\t\tvar start, end time.Time\n\t\tvar allday bool\n\n\t\t// try twice to parse the time fields:\n\t\t// all-day events have a date field and datetime is empty\n\t\tif event.Start.DateTime != \"\" {\n\t\t\tstart, err = time.Parse(time.RFC3339, event.Start.DateTime)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to parse start time from calendar event: %s\", err)\n\t\t\t}\n\t\t} else if event.Start.Date != \"\" {\n\t\t\t// the timezone seems to always be blank - not sure if it's a bug in\n\t\t\t// the API or expected behavior. Either way, the downstream code\n\t\t\t// evaluating the returned time will have to check for all-day\n\t\t\t// and do the right thing\n\t\t\t// leaving this here (and in the end block below) for now while I\n\t\t\t// investigate what's going on\n\t\t\tzone, err := time.LoadLocation(event.Start.TimeZone)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to parse start date TimeZone %q from calendar event: %s\", event.Start.TimeZone, err)\n\t\t\t}\n\n\t\t\tstart, err = time.ParseInLocation(\"2006-01-02\", event.Start.Date, zone)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to parse start date from all-day calendar event: %s\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tallday = true\n\t\t} else {\n\t\t\tlog.Println(\"event start time/date are both empty!\")\n\t\t\tcontinue\n\t\t}\n\n\t\tif event.End.DateTime != \"\" {\n\t\t\tend, err = time.Parse(time.RFC3339, event.End.DateTime)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to parse end time from calendar event: %s\", err)\n\t\t\t}\n\t\t} else if event.End.Date != \"\" {\n\t\t\tzone, err := time.LoadLocation(event.End.TimeZone)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to parse end date TimeZone %q from calendar event: %s\", event.End.TimeZone, err)\n\t\t\t}\n\n\t\t\tlog.Debugf(\"endZone: %q\", event.End.TimeZone)\n\n\t\t\tend, err = time.ParseInLocation(\"2006-01-02\", event.End.Date, zone)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Failed to parse end date from all-day calendar event: %s\", err)\n\t\t\t}\n\t\t\t// the event actually ends at 00:00:00 the next day so add a day\n\t\t\tend = end.AddDate(0, 0, 1)\n\n\t\t\tallday = true\n\t\t} else {\n\t\t\tlog.Println(\"event end time/date are both empty!\")\n\t\t\tcontinue\n\t\t}\n\n\t\tout[i].Start = start\n\t\tout[i].End = end\n\t\tout[i].AllDay = allday\n\t\tout[i].Name = event.Summary\n\t\tout[i].Description = event.Description\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "plugins/google_calendar/plugin.go",
    "content": "package google_calendar\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// TODO: announce start / end\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\nconst Usage = `!gcal (silence|status|expire|reload)\n!gcal silence 4h\n!gcal reload\n\n\nEven when attached, this plugin will not do anything until it is fully configured\nfor the room. At a mininum the calendar-id needs to be set. One or all of autoreply,\nannounce-start, and announce-end should be set to true to make anything happen.\n\nSetting up:\n\n    !prefs set --room <roomid> --plugin google_calendar --key calendar-id --value <calendar link>\n\n    autoreply: when set to true, the bot will reply with a message for any activity in the\n    room during hours when an event exists on the calendar. If the event has a description\n    set, that will be the text sent to the room. Otherwise a default message is generated.\n    !prefs set --room <roomid> --plugin google_calendar --key autoreply --value true\n\n    announce-(start|end): the bot will automatically announce when an event is starting or\n    ending. The event's description will be included if it is not empty.\n    !prefs set --room <roomid> --plugin google_calendar --key announce-start --value true\n    !prefs set --room <roomid> --plugin google_calendar --key announce-end --value true\n\n    timezone: optional, tells the bot which timezone to report dates in\n    !prefs set --room <roomid> --plugin google_calendar --key timezone --value America/Los_Angeles\n`\n\nconst DefaultTz = \"America/Los_Angeles\"\nconst DefaultMsg = \"Calendar event: %q\"\n\ntype Config struct {\n\tRoomId        string\n\tCalendarId    string\n\tTimezone      time.Location\n\tAutoreply     bool\n\tAnnounceStart bool\n\tAnnounceEnd   bool\n\tCalEvents     []CalEvent\n\tEvtsSinceLast int\n\tmut           sync.Mutex\n\tconfigTs      time.Time\n\tcalTs         time.Time\n}\n\nvar configCache map[string]*Config\nvar topMut sync.Mutex\nvar mentionWords = [...]string{\"@here\", \"@all\"}\n\nfunc init() {\n\tconfigCache = make(map[string]*Config)\n\tlog.SetPrefix(\"plugins/google_calendar\")\n}\n\nfunc Register() {\n\tp := hal.Plugin{\n\t\tName: \"google_calendar\",\n\t\tFunc: handleEvt,\n\t\tInit: initData,\n\t}\n\n\tp.Register()\n}\n\n// initData primes the cache and starts the background goroutine\nfunc initData(inst *hal.Instance) {\n\ttopMut.Lock()\n\tconfig := Config{RoomId: inst.RoomId}\n\tconfigCache[inst.RoomId] = &config\n\ttopMut.Unlock()\n\n\tpf := hal.PeriodicFunc{\n\t\tName:     \"google_calendar-\" + inst.RoomId,\n\t\tInterval: time.Minute * 10,\n\t\tFunction: func() { updateCachedCalEvents(inst.RoomId) },\n\t}\n\tpf.Register()\n\n\tgo func() {\n\t\ttime.Sleep(time.Second * 5)\n\t\tpf.Start()\n\t}()\n}\n\n// handleEvt handles events coming in from the chat system. It does not interact\n// directly with the calendar API and relies on the background goroutine to populate\n// the cache.\nfunc handleEvt(evt hal.Evt) {\n\t// don't process non-chat or messages with an empty body\n\tif !evt.IsChat || evt.Body == \"\" {\n\t\treturn\n\t}\n\n\tif strings.HasPrefix(strings.TrimSpace(evt.Body), \"!\") {\n\t\thandleCommand(&evt)\n\t\treturn\n\t}\n\n\tnow := time.Now()\n\n\t// use the hal kv store to prevent spamming\n\t// the spam keys are written with a 1 hour TTL so there's no need to examine the time\n\t// except for debugging purposes\n\tuserSpamKey := getSpamKey(\"user\", evt.UserId)\n\tuserTs, _ := hal.GetKV(userSpamKey)\n\t// users can !gcal silence to silence the messages for the whole room e.g. during an incident\n\troomSpamKey := getSpamKey(\"room\", evt.RoomId)\n\troomTs, _ := hal.GetKV(roomSpamKey)\n\n\t// always reply to @here/@everyone, etc.\n\tvar isBroadcast bool\n\tfor _, mention := range mentionWords {\n\t\tif strings.Contains(evt.Body, mention) {\n\t\t\tisBroadcast = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tconfig := getCachedConfig(evt.RoomId, now)\n\tcalEvents, err := config.getCachedCalEvents(now)\n\tif err != nil {\n\t\tnospamReplyf(&evt, \"Error while getting calendar data: %s\", err)\n\t\treturn\n\t}\n\n\tlog.Debugf(\"handleEvt checking message. Replied to user at: %q. Replied to room at: %q. %d events since last reply\", userTs, roomTs, config.EvtsSinceLast)\n\n\t// count events since the last notification to the room\n\tif roomTs != \"\" {\n\t\tconfig.EvtsSinceLast++\n\n\t\t// wait for at least 20 events before notifying again\n\t\t// TODO: should this be configurable?\n\t\tif config.EvtsSinceLast > 20 {\n\t\t\t// some events have passed and the message has likely been scrolled\n\t\t\t// off most screens so let it hit the room again\n\t\t\troomTs = \"\"\n\t\t}\n\t}\n\n\t// the user/room has been notified in the last hour, nothing to do now\n\tif !isBroadcast && (userTs != \"\" || roomTs != \"\") {\n\t\tlog.Printf(\"Not responding to message because a reply was sent already. user @ %q, room @ %q\", userTs, roomTs)\n\t\treturn\n\t}\n\n\tfor _, e := range calEvents {\n\t\tlog.Debugf(\"Autoreply: %t, Now: %q, Start: %q, End: %q\", config.Autoreply, now.String(), e.Start.String(), e.End.String())\n\t\tlog.Debugf(\"e.Description: %q, e.Name: %q\", e.Description, e.Name)\n\t\tif config.Autoreply && e.Start.Before(now) && e.End.After(now) {\n\t\t\tmsg := e.Description\n\t\t\tif msg == \"\" {\n\t\t\t\tmsg = fmt.Sprintf(DefaultMsg, e.Name)\n\t\t\t}\n\n\t\t\tevt.Reply(msg)\n\n\t\t\texpire := e.End.Sub(now)\n\t\t\thal.SetKV(userSpamKey, now.Format(time.RFC3339), expire) // only notify each user once per calendar event\n\t\t\thal.SetKV(roomSpamKey, now.Format(time.RFC3339), expire) // only notify the room again if it gets busy\n\t\t\tlog.Debugf(\"will not notify room %q for 10 minutes or the user %q for 2 hours\", roomSpamKey, userSpamKey)\n\n\t\t\tconfig.EvtsSinceLast = 0\n\n\t\t\tbreak // only notify once even if there are overlapping entries\n\t\t}\n\t}\n}\n\n// nospamReplyf keeps track of error replies and only replies with the same message\n// once per hour.\nfunc nospamReplyf(evt *hal.Evt, msg string, a ...interface{}) {\n\terrSpamKey := getSpamKey(\"err\", evt.RoomId)\n\terrStr, _ := hal.GetKV(errSpamKey)\n\n\treply := fmt.Sprintf(msg, a...)\n\n\tif errStr == reply {\n\t\tlog.Println(reply)\n\t\treturn\n\t}\n\n\tevt.Reply(reply)\n\n\thal.SetKV(errSpamKey, reply, time.Hour)\n}\n\nfunc handleCommand(evt *hal.Evt) {\n\targv := evt.BodyAsArgv()\n\n\tif argv[0] != \"!gcal\" {\n\t\treturn\n\t}\n\n\tif len(argv) < 2 {\n\t\tevt.Replyf(Usage)\n\t\treturn\n\t}\n\n\tnow := time.Now()\n\tconfig := getCachedConfig(evt.RoomId, now)\n\n\tswitch argv[1] {\n\tcase \"status\":\n\t\tevt.Replyf(\"Calendar cache is %.f minutes old. Config cache is %.f minutes old.\",\n\t\t\tnow.Sub(config.calTs).Minutes(), now.Sub(config.configTs).Minutes())\n\tcase \"help\":\n\t\tevt.Replyf(Usage)\n\tcase \"expire\":\n\t\tconfig.expireCaches()\n\t\tevt.Replyf(\"config & calendar caches expired\")\n\tcase \"reload\":\n\t\tconfig.expireCaches()\n\t\tupdateCachedCalEvents(evt.RoomId)\n\t\tevt.Replyf(\"reload complete\")\n\tcase \"silence\":\n\t\tif len(argv) == 3 {\n\t\t\td, err := time.ParseDuration(argv[2])\n\t\t\tif err != nil {\n\t\t\t\tevt.Replyf(\"Invalid silence duration %q: %s\", argv[2], err)\n\t\t\t} else {\n\t\t\t\tkey := getSpamKey(\"room\", evt.RoomId)\n\t\t\t\thal.SetKV(key, \"-\", d)\n\t\t\t\tevt.Replyf(\"Calendar notifications silenced for %s.\", d.String())\n\t\t\t}\n\t\t} else {\n\t\t\tevt.Reply(\"Invalid command. A duration is requried, e.g. !gcal silence 4h\")\n\t\t}\n\t}\n}\n\nfunc getSpamKey(scope, id string) string {\n\treturn \"gcal-\" + scope + \"-spam-\" + id\n}\n\nfunc updateCachedCalEvents(roomId string) {\n\tlog.Debugf(\"START: updateCachedCalEvents(%q)\", roomId)\n\n\tnow := time.Now()\n\n\ttopMut.Lock()\n\tc := configCache[roomId]\n\ttopMut.Unlock()\n\n\tc.LoadFromPrefs() // update the config from prefs\n\n\tevts, err := getEvents(c.CalendarId, now)\n\tif err != nil {\n\t\tlog.Printf(\"FAILED: updateCachedCalEvents(%q): %s\", roomId, err)\n\t\treturn\n\t}\n\n\tc.mut.Lock()\n\tc.calTs = now\n\tc.CalEvents = evts\n\tc.mut.Unlock()\n\n\tlog.Debugf(\"DONE: updateCachedCalEvents(%q)\", roomId)\n}\n\nfunc getCachedConfig(roomId string, now time.Time) *Config {\n\ttopMut.Lock()\n\tc := configCache[roomId]\n\ttopMut.Unlock()\n\n\tage := now.Sub(c.configTs)\n\n\tif age.Minutes() > 10 {\n\t\tc.LoadFromPrefs()\n\t}\n\n\treturn c\n}\n\n// getCachedEvents fetches the calendar data from the Google Calendar API,\nfunc (c *Config) getCachedCalEvents(now time.Time) ([]CalEvent, error) {\n\tc.mut.Lock()\n\tcalAge := now.Sub(c.calTs)\n\tc.mut.Unlock()\n\n\tif calAge.Hours() > 1.1 {\n\t\tlog.Debugf(\"%q's calendar cache appears to be expired after %f hours\", c.RoomId, calAge.Hours())\n\t\tevts, err := getEvents(c.CalendarId, now)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Error encountered while fetching calendar events: %s\", err)\n\t\t\treturn nil, err\n\t\t} else {\n\t\t\tc.mut.Lock()\n\t\t\tc.calTs = now\n\t\t\tc.CalEvents = evts\n\t\t\tc.mut.Unlock()\n\t\t}\n\t}\n\n\treturn c.CalEvents, nil\n}\n\nfunc (c *Config) LoadFromPrefs() error {\n\tc.mut.Lock()\n\tdefer c.mut.Unlock()\n\n\tcidpref := hal.GetPref(\"\", \"\", c.RoomId, \"google_calendar\", \"calendar-id\", \"\")\n\tif cidpref.Success {\n\t\tc.CalendarId = cidpref.Value\n\t} else {\n\t\treturn fmt.Errorf(\"Failed to load calendar-id preference for room %q: %s\", c.RoomId, cidpref.Error)\n\t}\n\n\tc.Autoreply = c.loadBoolPref(\"autoreply\")\n\tc.AnnounceStart = c.loadBoolPref(\"announce-start\")\n\tc.AnnounceEnd = c.loadBoolPref(\"announce-end\")\n\n\ttzpref := hal.GetPref(\"\", \"\", c.RoomId, \"google_calendar\", \"timezone\", DefaultTz)\n\ttz, err := time.LoadLocation(tzpref.Value)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Could not load timezone info for '%s': %s\\n\", tzpref.Value, err)\n\t}\n\tc.Timezone = *tz\n\n\tc.configTs = time.Now()\n\n\treturn nil\n}\n\nfunc (c *Config) expireCaches() {\n\tc.calTs = time.Time{}\n\tc.configTs = time.Time{}\n}\n\nfunc (c *Config) loadBoolPref(key string) bool {\n\tpref := hal.GetPref(\"\", \"\", c.RoomId, \"google_calendar\", key, \"false\")\n\n\tval, err := strconv.ParseBool(pref.Value)\n\tif err != nil {\n\t\tlog.Printf(\"unable to parse boolean pref value: %s\", err)\n\t\treturn false\n\t}\n\n\treturn val\n}\n"
  },
  {
    "path": "plugins/guys/plugin.go",
    "content": "package guys\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nfunc Register() {\n\tguys := hal.Plugin{\n\t\tName:  \"guys\",\n\t\tFunc:  guys,\n\t\tRegex: \"(?i:guys)\",\n\t}\n\tguys.Register()\n}\n\n// guys counts how many times you've used \"guys\" in a chat message and\n// lets you know via DM\n// !plugin attach guys\n// this gets it listening to the room but it won't notify you until a pref is set\n// !prefs set --user * --plugin guys --key enabled --value true\n// or\n// !prefs set --room * --plugin guys --key enabled --value true\nfunc guys(evt hal.Evt) {\n\tif !evt.IsChat {\n\t\treturn\n\t}\n\n\tkey := \"guys-\" + evt.UserId\n\thal.IncrementCounter(key)\n\n\t// even if this plugin is attached to a room it won't notify without\n\t// an accompanying pref to say whether it's a specific user who cares\n\t// or the whole room\n\tuserCares := hal.GetPref(evt.UserId, \"\", \"\", \"guys\", \"enabled\", \"false\")\n\troomCares := hal.GetPref(\"\", \"\", evt.RoomId, \"guys\", \"enabled\", \"false\")\n\tif userCares.Value == \"false\" && roomCares.Value == \"false\" {\n\t\treturn\n\t}\n\n\tcount, _ := hal.GetCounter(key)\n\tmsg := fmt.Sprintf(\"Yo. You have now used \\\"guys\\\" %d times.\", count)\n\n\t// only let the user know - no need to publicly shame or clutter the room\n\tevt.ReplyDM(msg)\n}\n"
  },
  {
    "path": "plugins/inspect/plugin.go",
    "content": "package inspect\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport \"github.com/netflix/hal-9001/hal\"\n\nvar log hal.Logger\n\nfunc Register() {\n\tgetid := hal.Plugin{\n\t\tName:    \"getid\",\n\t\tFunc:    getid,\n\t\tCommand: \"getid\",\n\t}\n\tgetid.Register()\n\n\tleave := hal.Plugin{\n\t\tName:  \"leave\",\n\t\tFunc:  leave,\n\t\tRegex: \"^[[:space:]]*!leave\",\n\t}\n\tleave.Register()\n}\n\n// getid resolves user & room names to ids using the broker's RoomNameToId\n// and UserNameToId methods (along with the LooksLike* variants).\nfunc getid(evt hal.Evt) {\n\targs := evt.BodyAsArgv()\n\tif len(args) != 2 {\n\t\tevt.Replyf(\"%s requires exactly 2 arguments. Only %d were provided. e.g. !getid atobey\",\n\t\t\targs[0], len(args))\n\t\treturn\n\t}\n\n\tmaybeRoomId := evt.Broker.RoomNameToId(args[1])\n\tmaybeUserId := evt.Broker.UserNameToId(args[1])\n\n\tif evt.Broker.LooksLikeRoomId(maybeRoomId) {\n\t\tevt.Replyf(\"Room: %q => %q\", args[1], maybeRoomId)\n\t} else if evt.Broker.LooksLikeUserId(maybeUserId) {\n\t\tevt.Replyf(\"User: %q => %q\", args[1], maybeUserId)\n\t} else {\n\t\tevt.Replyf(\"Could not resolve %q as a user or room.\", args[1])\n\t}\n}\n\nfunc leave(evt hal.Evt) {\n\tlog.Printf(\"Leaving room %q as requested by %s.\", evt.RoomId, evt.User)\n\tevt.Replyf(\"Leaving room %q as requested by %s.\", evt.RoomId, evt.User)\n\terr := evt.Broker.Leave(evt.RoomId)\n\tif err != nil {\n\t\tevt.Replyf(\"Error leaving room %q: %s\", evt.RoomId, err)\n\t}\n}\n"
  },
  {
    "path": "plugins/mark/plugin.go",
    "content": "package mark\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\ntype Mark struct {\n\tTimestamp time.Time `json:\"timestamp\"`\n\tUser      string    `json:\"user\"`\n\tRoom      string    `json:\"room\"`\n\tBroker    string    `json:\"broker\"`\n\tNote      string    `json:\"note\"`\n}\n\nconst MarkTable = `\nCREATE TABLE IF NOT EXISTS marks (\n  ts       TIMESTAMP,\n  user     VARCHAR(191),\n  room     VARCHAR(191),\n  broker   VARCHAR(191),\n  note     TEXT,\n  PRIMARY KEY (ts,user,room,broker)\n)`\n\nfunc Register() {\n\tmark := hal.Plugin{\n\t\tName:    \"mark\",\n\t\tCommand: \"mark\",\n\t\tFunc:    mark,\n\t}\n\tmark.Register()\n\n\thal.SqlInit(MarkTable)\n\n\thttp.HandleFunc(\"/v1/marks\", httpGetMarks)\n}\n\nfunc mark(evt hal.Evt) {\n\targs := evt.BodyAsArgv()\n\t// check for !marks list or !marks --list and do that instead\n\tif len(args) > 1 && (args[1] == \"list\" || args[1] == \"--list\") {\n\t\tlistMarks(evt)\n\t\treturn\n\t}\n\n\t// strip the leading \"!mark \"\n\tnote := strings.TrimSpace(evt.Body)\n\tnote = strings.TrimPrefix(note, \"!mark\")\n\tnote = strings.TrimSpace(note)\n\n\tsql := `INSERT INTO marks (ts, user, room, broker, note) VALUES (?, ?, ?, ?, ?)`\n\t_, err := hal.SqlDB().Exec(sql, evt.Time, evt.UserId, evt.RoomId, evt.BrokerName(), note)\n\tif err != nil {\n\t\tlog.Printf(\"Could not insert mark into database: %s\\n\", err)\n\t}\n\n\tlog.Printf(\"Mark added at %s with note %q\", evt.Time, note)\n\n\tevt.Replyf(\"Mark added at %s with note %q\", evt.Time, note)\n}\n\nfunc listMarks(evt hal.Evt) {\n\tmarks, err := FetchMarks(evt.RoomId, 50)\n\tif err != nil {\n\t\tevt.Replyf(\"could not fetch marks: '%s'\", err)\n\t\treturn\n\t}\n\n\tdata := make([][]string, len(marks))\n\tfor i, mark := range marks {\n\t\tuser := evt.Broker.UserIdToName(mark.User)\n\t\tdata[i] = []string{mark.Timestamp.String(), user, mark.Note}\n\t}\n\n\tevt.ReplyTable([]string{\"Time\", \"User\", \"Note\"}, data)\n}\n\nfunc httpGetMarks(w http.ResponseWriter, r *http.Request) {\n\tmarks, err := FetchMarks(\"\", 50)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"could not fetch marks: '%s'\", err), 500)\n\t\treturn\n\t}\n\n\tjs, err := json.Marshal(marks)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"could not marshal marks to json: '%s'\", err), 500)\n\t\treturn\n\t}\n\n\tw.Write(js)\n}\n\nfunc FetchMarks(room string, limit int) ([]Mark, error) {\n\tdb := hal.SqlDB()\n\n\tsql := `SELECT UNIX_TIMESTAMP(ts) AS ts, user, room, broker, note\n\t        FROM marks\n\t\t\tWHERE ts < ? AND ts > ?`\n\n\t// temporary - these will be parameters eventually\n\t// will probably also add query gen for filtering by user/room/broker\n\tnow := time.Now()\n\tyesterday := now.Add(-time.Hour * 24)\n\n\tparams := make([]interface{}, 2)\n\tparams[0] = &now\n\tparams[1] = &yesterday\n\n\tif room != \"\" {\n\t\tsql = sql + \" AND room=?\"\n\t\tparams = append(params, &room)\n\t}\n\tsql = sql + \" ORDER BY ts DESC\"\n\n\trows, err := db.Query(sql, params...)\n\tif err != nil {\n\t\tlog.Printf(\"marks query failed: %s\\n\", err)\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\tmarks := make([]Mark, 0)\n\n\tfor rows.Next() {\n\t\tmark := Mark{}\n\n\t\tvar ts int64\n\t\terr = rows.Scan(&ts, &mark.User, &mark.Room, &mark.Broker, &mark.Note)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Row iteration failed: %s\\n\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tmark.Timestamp = time.Unix(ts, 0)\n\n\t\tmarks = append(marks, mark)\n\t}\n\n\treturn marks, nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/helpers.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\nfunc init() {\n\tlog.SetPrefix(\"plugins/pagerduty\")\n}\n\n// AuthenticatedGet authenticates with the provided token and GETs the url.\nfunc authenticatedGet(geturl, token string) (*http.Response, error) {\n\ttokenHdr := fmt.Sprintf(\"Token token=%s\", token)\n\n\treq, err := http.NewRequest(\"GET\", geturl, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Add(\"Accept\", \"application/vnd.pagerduty+json;version=2\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"Authorization\", tokenHdr)\n\n\tclient := &http.Client{}\n\tr, err := client.Do(req)\n\n\treturn r, err\n}\n\n// AuthenticatedPost authenticates with the provided token and posts the\n// provided body.\nfunc authenticatedPost(token, postUrl string, body []byte) (*http.Response, error) {\n\ttokenHdr := fmt.Sprintf(\"Token token=%s\", token)\n\tbuf := bytes.NewBuffer(body)\n\n\treq, err := http.NewRequest(\"POST\", postUrl, buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Add(\"Accept\", \"application/vnd.pagerduty+json;version=2\")\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"Authorization\", tokenHdr)\n\n\tclient := &http.Client{}\n\treturn client.Do(req)\n}\n\nfunc pagedUrl(resource string, offset, limit int, params map[string][]string) string {\n\tout := fmt.Sprintf(\"https://api.pagerduty.com%s\", resource)\n\n\tquery := make([]string, 0)\n\n\tif limit != 0 {\n\t\tquery = append(query, fmt.Sprintf(\"limit=%d\", limit))\n\t}\n\n\tif offset != 0 {\n\t\tquery = append(query, fmt.Sprintf(\"offset=%d\", offset))\n\t}\n\n\tif params != nil {\n\t\tfor k, vlist := range params {\n\t\t\tfor _, vv := range vlist {\n\t\t\t\tquery = append(query, fmt.Sprintf(\"%s=%s\", k, url.QueryEscape(vv)))\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(query) > 0 {\n\t\treturn fmt.Sprintf(\"%s?%s\", out, strings.Join(query, \"&\"))\n\t}\n\n\treturn out\n}\n"
  },
  {
    "path": "plugins/pagerduty/oncall_plugin.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nconst OncallUsage = `!oncall <alias>\n\nFind out who is oncall. If only one argument is provided, it must match\na known alias for a Pagerduty service. Otherwise, it is expected to be\na subcommand.\n\n!oncall core\n`\n\nconst DefaultTopicInterval = \"10m\"\n\nvar onePerToken map[string]sync.Mutex\nvar mapLock sync.Mutex\n\nfunc init() {\n\tonePerToken = make(map[string]sync.Mutex)\n}\n\n// TODO: add the service key to the output such that someone trying to contact a team\n// can page them from within Slack without having to set up a page alias or go out\n// to a web page. !page should be able to take a service key so the output can include something\n// like: \"To page <team> use the command: !page <servicekey> <message>\"\nfunc oncall(msg hal.Evt) {\n\tparts := msg.BodyAsArgv()\n\n\tif len(parts) == 1 {\n\t\tmsg.Reply(OncallUsage)\n\t\treturn\n\t}\n\n\t// make sure the pagerduty token is setup in hal.Secrets\n\ttoken, err := getSecrets()\n\tif err != nil || token == \"\" {\n\t\tmsg.Replyf(\"pagerduty: %s is not set up in hal.Secrets. Cannot continue.\", PagerdutyTokenKey)\n\t\treturn\n\t}\n\n\tif parts[1] == \"cache-now\" {\n\t\tmsg.Reply(\"Updating Pagerduty policy cache now.\")\n\t\tgetOncallCache(token, true)\n\t\tmsg.Reply(\"Pagerduty policy cache update complete.\")\n\t\treturn\n\t} else if parts[1] == \"cache-status\" {\n\t\tage := int(hal.Cache().Age(CacheKey).Seconds())\n\t\tnext := time.Time{}\n\t\tstatus := \"broken\"\n\t\tpf := hal.GetPeriodicFunc(\"pagerduty-oncall-cache\")\n\t\tif pf != nil {\n\t\t\tnext = pf.Last().Add(pf.Interval)\n\t\t\tstatus = pf.Status()\n\t\t}\n\t\tmsg.Replyf(\"The cache is %d seconds old. Auto-update is %s and its next update is at %s.\",\n\t\t\tage, status, next.Format(time.UnixDate))\n\t\treturn\n\t} else if len(parts) > 2 {\n\t\t// flatten split words back into position 1, parts isn't used after the ToLower\n\t\tparts[1] = strings.Join(parts[1:], \" \")\n\t}\n\n\t// TODO: look at the aliases set up for !page and try for an exact match\n\t// before doing fuzzy search -- move fuzzy search to a \"search\" subcommand\n\t// so it's clear that it is not precise\n\twant := strings.ToLower(parts[1])\n\n\t// see if there's an exact match on an alias, e.g. \"!oncall core\" -> alias.core\n\t/*\n\t\taliasPref := msg.AsPref().SetUser(\"\").FindKey(aliasKey(want)).One()\n\t\tif aliasPref.Success {\n\t\t\tsvc, err := GetServiceByKey(token, aliasPref.Value)\n\t\t\tif err == nil {\n\t\t\t}\n\t\t\t// all through to search ...\n\t\t}\n\t*/\n\n\t// search over all policies looking for matching policy name, escalation\n\t// rule name, or service name\n\tmatches := make(map[string][]Oncall)\n\toncalls := getOncallCache(token, false)\n\tvar exactMatchFound bool\n\n\taddMatch := func(matches map[string][]Oncall, oncall Oncall) {\n\t\tkey := oncall.EscalationPolicy.Summary\n\t\tif _, exists := matches[key]; exists {\n\t\t\tmatches[key] = append(matches[key], oncall)\n\t\t} else {\n\t\t\tmatches[key] = []Oncall{oncall}\n\t\t}\n\t}\n\n\tfor _, oncall := range oncalls {\n\t\tschedSummary := clean(oncall.Schedule.Summary)\n\t\tif schedSummary == want {\n\t\t\taddMatch(matches, oncall)\n\t\t\texactMatchFound = true\n\t\t\tcontinue\n\t\t} else if !exactMatchFound && strings.Contains(schedSummary, want) {\n\t\t\taddMatch(matches, oncall)\n\t\t\tcontinue\n\t\t}\n\n\t\tepDesc := clean(oncall.EscalationPolicy.Description)\n\t\tif epDesc == want {\n\t\t\taddMatch(matches, oncall)\n\t\t\texactMatchFound = true\n\t\t\tcontinue\n\t\t} else if !exactMatchFound && strings.Contains(epDesc, want) {\n\t\t\taddMatch(matches, oncall)\n\t\t\tcontinue\n\t\t}\n\n\t\tepSummary := clean(oncall.EscalationPolicy.Summary)\n\t\tif epSummary == want {\n\t\t\taddMatch(matches, oncall)\n\t\t\texactMatchFound = true\n\t\t\tcontinue\n\t\t} else if !exactMatchFound && strings.Contains(epSummary, want) {\n\t\t\taddMatch(matches, oncall)\n\t\t\tcontinue\n\t\t}\n\t}\n\n\t// check team names if there were no matches\n\t// TODO: cache some of these results and always check team names\n\tteams, err := GetTeams(token, nil)\n\tif err != nil {\n\t\tlog.Printf(\"REST call to Pagerduty /teams failed: %s\", err)\n\t} else {\n\t\tfor _, team := range teams {\n\t\t\tltname := clean(team.Name)\n\t\t\tltdesc := clean(team.Description)\n\n\t\t\tif strings.Contains(ltname, want) || strings.Contains(ltdesc, want) {\n\t\t\t\toncalls := getTeamOncalls(token, team)\n\t\t\t\tfor _, oncall := range oncalls {\n\t\t\t\t\taddMatch(matches, oncall)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treply := formatOncallReply(want, exactMatchFound, matches)\n\tmsg.Reply(reply)\n}\n\n// getTeamOncalls fetches escalation policies for the team then the oncalls for those\n// policies and returns a list.\nfunc getTeamOncalls(token string, team Team) []Oncall {\n\tmut := getMutex(token)\n\tmut.Lock()\n\tdefer mut.Unlock()\n\n\tout := make([]Oncall, 0)\n\n\tparams := map[string][]string{\"team_ids[]\": []string{team.Id}}\n\tpolicies, err := GetEscalationPolicies(token, params)\n\tif err != nil {\n\t\tlog.Printf(\"Error while fetching escalation policies for team id %q: %s\", team.Id, err)\n\t\treturn out\n\t}\n\n\tpolicy_ids := make([]string, 0)\n\tfor _, policy := range policies {\n\t\tpolicy_ids = append(policy_ids, policy.Id)\n\t}\n\n\tparams = map[string][]string{\n\t\t\"include[]\":               []string{\"users\"},\n\t\t\"escalation_policy_ids[]\": policy_ids,\n\t}\n\n\toncalls, err := GetOncalls(token, params)\n\tif err != nil {\n\t\tlog.Printf(\"Error while fetching oncalls for team id %q's policies: %s\", team.Id, err)\n\t} else {\n\t\treturn oncalls\n\t}\n\n\treturn out\n}\n\nfunc getOncallCache(token string, forceUpdate bool) []Oncall {\n\tmut := getMutex(token)\n\tmut.Lock()\n\tdefer mut.Unlock()\n\n\toncalls := []Oncall{}\n\n\tif !forceUpdate {\n\t\t// see if there's a copy cached\n\t\tif hal.Cache().Exists(CacheKey) {\n\t\t\tttl, err := hal.Cache().Get(CacheKey, &oncalls)\n\t\t\tif err != nil {\n\t\t\t\tlog.Printf(\"Error retreiving oncalls from the Hal TTL cache: %s\", err)\n\t\t\t\toncalls = []Oncall{}\n\t\t\t} else if ttl == 0 {\n\t\t\t\toncalls = []Oncall{}\n\t\t\t}\n\t\t}\n\n\t\t// the cache exists and is still valid, return it now\n\t\tif len(oncalls) > 0 {\n\t\t\treturn oncalls\n\t\t}\n\t}\n\n\t// get all of the defined policies\n\tparams := map[string][]string{\"include[]\": []string{\"users\"}}\n\toncalls, err := GetOncalls(token, params)\n\tif err != nil {\n\t\tlog.Printf(\"Returning empty list. REST call to Pagerduty failed: %s\", err)\n\t\treturn []Oncall{}\n\t}\n\n\t// set the cache to expire 1 minute later than the polling interval\n\tcacheExpire := getCacheFreq() + time.Minute\n\thal.Cache().Set(CacheKey, &oncalls, cacheExpire)\n\n\treturn oncalls\n}\n\nfunc getCacheFreq() time.Duration {\n\tcacheFreq := hal.GetPref(\"\", \"\", \"\", \"pagerduty\", \"cache-update-frequency\", DefaultCacheInterval)\n\tcd, err := time.ParseDuration(cacheFreq.Value)\n\tif err != nil {\n\t\tlog.Panicf(\"BUG: could not parse cache update frequency preference: %q\", cacheFreq.Value)\n\t}\n\n\treturn cd\n}\n\nfunc getTopicFreq(roomId string) time.Duration {\n\ttopicFreq := hal.GetPref(\"\", \"\", roomId, \"pagerduty\", \"topic-update-frequency\", DefaultTopicInterval)\n\ttd, err := time.ParseDuration(topicFreq.Value)\n\tif err != nil {\n\t\tlog.Panicf(\"BUG: could not parse topic update frequency preference: %q\", topicFreq.Value)\n\t}\n\n\treturn td\n}\n\nfunc oncallInit(i *hal.Instance) {\n\tcacheFreq := getCacheFreq()\n\ttopicFreq := getTopicFreq(i.RoomId)\n\n\ttoken, err := getSecrets()\n\tif err != nil || token == \"\" {\n\t\treturn // getSecrets will log the error\n\t}\n\n\tpf := hal.PeriodicFunc{\n\t\tName:     \"pagerduty-oncall-cache\",\n\t\tInterval: cacheFreq,\n\t\tFunction: func() { pollOncalls(token) },\n\t}\n\n\tpf.Register()\n\tgo pf.Start()\n\n\ttpf := hal.PeriodicFunc{\n\t\tName:     topicFuncName(i.RoomId),\n\t\tInterval: topicFreq,\n\t\tFunction: func() { topicUpdater(token, i.RoomId, i.Broker.Name()) },\n\t}\n\n\ttpf.Register()\n\tgo tpf.Start()\n\n\t// TODO: add a command to stop, etc.\n}\n\nfunc pollOncalls(token string) {\n\tgetOncallCache(token, true)\n}\n\n// topicUpdater runs periodically to update the topic in the room\n// it's configured in.\n// To fully enable it, you need the oncall schedule id from the pagerduty API.\n// !prefs set --room * --broker slack --plugin pagerduty --key topic-updater-schedule-id --value <schedule id>\n// !prefs set --room * --broker slack --plugin pagerduty --key topic-prefix --value <text>\n// !prefs set --room * --broker slack --plugin pagerduty --key topic-suffix --value <text>\n// TODO: see if there's a way to also resolve integration keys instead of using the schedule id\nfunc topicUpdater(token, roomId, brokerName string) {\n\tmut := getMutex(token)\n\tmut.Lock()\n\tdefer mut.Unlock()\n\n\tlog.Debugf(\"ENTER topicUpdater(token, %q, %q)\", roomId, brokerName)\n\n\tpref := hal.GetPref(\"\", brokerName, roomId, \"pagerduty\", \"topic-updater-schedule-id\", \"-\")\n\t// probably not configured, nothing to see here...\n\tif !pref.Success || pref.Value == \"-\" {\n\t\tlog.Debugf(\"The pref ''/%q/%q/pagerduty/topic-updater-schedule-id does not seem to be set. Returning without taking action.\",\n\t\t\tbrokerName, roomId)\n\t\treturn\n\t}\n\n\tparams := map[string][]string{\n\t\t\"include[]\":      []string{\"users\", \"contact_methods\"},\n\t\t\"schedule_ids[]\": []string{pref.Value},\n\t}\n\n\toncalls, err := GetOncalls(token, params)\n\tif err != nil {\n\t\tlog.Printf(\"Failed to fetch oncalls for schedule id %q: %s\", pref.Value, err)\n\t\treturn\n\t}\n\n\tlog.Debugf(\"Got %d users for schedule id %q\", len(oncalls), pref.Value)\n\n\t// there may be more than one entry but if they're both on the same\n\t// schedule it should be the same primary oncall so ignore all but the first\n\tif len(oncalls) == 0 {\n\t\tlog.Printf(\"no oncall results for id %q\", pref.Value)\n\t\treturn\n\t}\n\n\t// TODO: yet another place some kind of templating support would be handy\n\tprefix := hal.GetPref(\"\", brokerName, roomId, \"pagerduty\", \"topic-prefix\", \"\")\n\tsuffix := hal.GetPref(\"\", brokerName, roomId, \"pagerduty\", \"topic-suffix\", \"\")\n\n\t// e.g. prefix = \"\", summary = \"Al Tobey\", suffix = \" [team-dl@company.com] !pageus\"\n\ttopic := prefix.Value + oncalls[0].User.Summary + suffix.Value\n\n\tbroker := hal.Router().GetBroker(brokerName)\n\n\toldTopic, err := broker.GetTopic(roomId)\n\tif err != nil {\n\t\tlog.Printf(\"Could not fetch current topic for room %q: %s\", roomId, err)\n\t\treturn\n\t}\n\n\t// only do the update if the topic has changed\n\tif topic != oldTopic {\n\t\tbroker.SetTopic(roomId, topic)\n\t}\n}\n\nfunc topicFuncName(roomId string) string {\n\treturn fmt.Sprintf(\"pagerduty-topic-updater-%s\", roomId)\n}\n\n// OncallsByLevel provides sorting by oncall level for []Oncall.\ntype OncallsByLevel []Oncall\n\nfunc (a OncallsByLevel) Len() int      { return len(a) }\nfunc (a OncallsByLevel) Swap(i, j int) { a[i], a[j] = a[j], a[i] }\nfunc (a OncallsByLevel) Less(i, j int) bool {\n\t// sort \"always on call\" users to the end of the list\n\tif a[j].Schedule.Summary == \"\" {\n\t\treturn true\n\t}\n\n\treturn a[i].EscalationLevel < a[j].EscalationLevel\n}\n\nfunc formatOncallReply(wanted string, exactMatchFound bool, matches map[string][]Oncall) string {\n\tbuf := bytes.NewBuffer([]byte{})\n\n\tif exactMatchFound {\n\t\tfor _, oncalls := range matches {\n\t\t\tfmt.Fprintf(buf, \"exact match found for %q\\n\", oncalls[0].EscalationPolicy.Summary)\n\t\t}\n\t} else {\n\t\tfmt.Fprintf(buf, \"%d records matched for query: %q\\n\", len(matches), wanted)\n\t}\n\n\tkeys := make([]string, 0)\n\tfor key, _ := range matches {\n\t\tkeys = append(keys, key)\n\t}\n\tsort.Strings(keys)\n\n\tfor _, key := range keys {\n\t\toncalls := matches[key]\n\t\tsort.Sort(OncallsByLevel(oncalls))\n\n\t\tfor _, oncall := range oncalls {\n\t\t\tindent := strings.Repeat(\"    \", oncall.EscalationLevel)\n\t\t\tsched := oncall.Schedule.Summary\n\t\t\tif sched == \"\" {\n\t\t\t\tsched = \"always on call\"\n\t\t\t}\n\n\t\t\tif exactMatchFound {\n\t\t\t\tfmt.Fprintf(buf, \"%s%s - %s\\n\", indent, oncall.User.Summary, sched)\n\t\t\t} else {\n\t\t\t\tfmt.Fprintf(buf, \"%s%s - %s - %s\\n\", indent,\n\t\t\t\t\toncall.EscalationPolicy.Summary, oncall.User.Summary, sched)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn buf.String()\n}\n\nfunc getMutex(token string) sync.Mutex {\n\tmapLock.Lock()\n\tdefer mapLock.Unlock()\n\n\tif _, exists := onePerToken[token]; !exists {\n\t\tvar mut sync.Mutex\n\t\tonePerToken[token] = mut\n\t}\n\n\treturn onePerToken[token]\n}\n\nfunc clean(in string) string {\n\tlower := strings.ToLower(in)\n\tclean := strings.Trim(lower, `()[]{}<>~!@#$%^&*+/=\"',.?|`)\n\treturn clean\n}\n"
  },
  {
    "path": "plugins/pagerduty/page_plugin.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nconst PageUsage = `!page <alias> [optional message]\n\nSend an alert via Pagerduty with an optional custom message.\n\nAliases that have a comma-separated list of service keys will result in one page going to each service key when the alias is paged.\n\n!page core\n!page core <message>\n!pagecore HELP ME YOU ARE MY ONLY HOPE\n\n!page add <alias> <service key>\n!page add <alias> <service key>,<service_key>,<service_key>,...\n!page rm <alias>\n!page list\n`\n\nconst PageDefaultMessage = `your presence is requested in the chat room`\n\nfunc page(msg hal.Evt) {\n\tparts := msg.BodyAsArgv()\n\n\t// detect concatenated command + team name & split them\n\t// e.g. \"!pagecore\" -> {\"!page\", \"core\"}\n\tif strings.HasPrefix(parts[0], \"!page\") && len(parts[0]) > 5 {\n\t\tteam := strings.TrimPrefix(parts[0], \"!page\")\n\t\tparts = append([]string{\"!page\", team}, parts[1:]...)\n\t}\n\n\t// should be 2 parts now, \"!page\" and the target team at a minimum\n\tif parts[0] != \"!page\" || len(parts) < 2 {\n\t\tmsg.Reply(PageUsage)\n\t\treturn\n\t}\n\n\tswitch parts[1] {\n\tcase \"h\", \"help\":\n\t\tmsg.Reply(PageUsage)\n\tcase \"add\":\n\t\taddAlias(msg, parts[2:])\n\tcase \"rm\":\n\t\trmAlias(msg, parts[2:])\n\tcase \"list\":\n\t\tlistAlias(msg)\n\tdefault:\n\t\tpageAlias(msg, parts[1:])\n\t}\n}\n\nfunc pageAlias(evt hal.Evt, parts []string) {\n\tpageMessage := PageDefaultMessage\n\tmsgPref := evt.AsPref().FindKey(\"default-message\").Room(evt.RoomId).One()\n\n\t// Caller slices off the !page. parts[0] should be the alias.\n\t// Anything after is a custom message.\n\tif len(parts) > 1 {\n\t\tpageMessage = strings.Join(parts[1:], \" \")\n\t} else if msgPref.Success {\n\t\tpageMessage = msgPref.Value\n\t}\n\n\t// map alias name to PD token via prefs\n\tkey := aliasKey(parts[0])\n\t// make sure to filter on at least room id since FindKey might find duplicate\n\t// aliases from other rooms\n\tpref := evt.AsPref().FindKey(key).Room(evt.RoomId).One()\n\n\t// make sure the query succeeded\n\tif !pref.Success {\n\t\tif pref.Error != nil {\n\t\t\tevt.Replyf(\"Unable to access preferences: %#q\", pref.Error)\n\t\t} else {\n\t\t\tevt.Replyf(\"Alias %q is not configured. Try !page add %s <pagerduty integration key>\", parts[0], parts[0])\n\t\t}\n\t\treturn\n\t}\n\n\t// if qpref.Get returned the default, the alias was not found\n\tif pref.Value == \"\" {\n\t\tevt.Replyf(\"Alias %q is not configured. Try !page add %s <pagerduty integration key>\", parts[0], parts[0])\n\t\treturn\n\t}\n\n\t// make sure the hal secrets are set up\n\ttoken, err := getSecrets()\n\tif err != nil {\n\t\tevt.Error(err)\n\t\treturn\n\t}\n\n\t// the value can be a list of tokens, separated by commas\n\tfor _, svckey := range strings.Split(pref.Value, \",\") {\n\t\t// Pagerduty has confirmed that both V1 and V2 keys are supported on the V2 endpoint.\n\t\tpde2 := NewV2Event(svckey)\n\t\tpde2.Action = \"trigger\"\n\t\tpde2.Payload.Summary = pageMessage        // required\n\t\tpde2.Payload.Source = evt.User            // required\n\t\tpde2.Payload.Severity = \"critical\"        // required\n\t\tpde2.Payload.Component = evt.BrokerName() // optional\n\t\tpde2.Payload.Group = evt.Room             // optional\n\t\tpde2.Payload.Class = \"!page\"              // optional\n\n\t\tresp, err := pde2.Send(token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Pagerduty V2 API failed: %s\", err)\n\t\t\tevt.Replyf(\"Pagerduty V2 API failed! Your alert has NOT been delivered. Error: %s\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// 200 means the alert has been sent. 202 is returned when the event is queued for delivery.\n\t\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\t\tlog.Printf(\"Pagerduty V2 response message for %q -> %s(%s): %s\\n\", pageMessage, parts[0], svckey, resp.Message)\n\t\t\tevt.Replyf(\"Message sent to %s using integration key %s via Pagerduty V2 API.\", parts[0], svckey)\n\t\t}\n\t}\n}\n\nfunc addAlias(msg hal.Evt, parts []string) {\n\tif len(parts) < 2 {\n\t\tmsg.Replyf(\"!page add requires 2 arguments, e.g. !page add sysadmins XXXXXXX\")\n\t\treturn\n\t} else if len(parts) > 2 {\n\t\tkeys := strings.Replace(strings.Join(parts[1:], \",\"), \",,\", \",\", len(parts)-2)\n\t\tparts = []string{parts[0], keys}\n\t}\n\n\tpref := msg.AsPref()\n\tpref.User = \"\" // filled in by AsPref and unwanted\n\tpref.Key = aliasKey(parts[0])\n\tpref.Value = parts[1]\n\n\terr := pref.Set()\n\tif err != nil {\n\t\tmsg.Replyf(\"Write failed: %s\", err)\n\t} else {\n\t\tmsg.Replyf(\"Added alias: %q -> %q\", parts[0], parts[1])\n\t}\n}\n\nfunc rmAlias(msg hal.Evt, parts []string) {\n\tif len(parts) != 1 {\n\t\tmsg.Replyf(\"!page rm requires 1 argument, e.g. !page rm sysadmins\")\n\t\treturn\n\t}\n\n\tpref := msg.AsPref()\n\tpref.User = \"\" // filled in by AsPref and unwanted\n\tpref.Key = aliasKey(parts[0])\n\tpref.Delete()\n\n\tmsg.Replyf(\"Removed alias %q\", parts[0])\n}\n\nfunc listAlias(msg hal.Evt) {\n\tpref := msg.AsPref()\n\tpref.User = \"\" // filled in by AsPref and unwanted\n\tprefs := pref.GetPrefs()\n\tdata := prefs.Table()\n\tmsg.ReplyTable(data[0], data[1:])\n}\n\nfunc aliasKey(alias string) string {\n\treturn fmt.Sprintf(\"alias.%s\", alias)\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_events_v1.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n)\n\n// https://developer.pagerduty.com/documentation/integration/events/trigger\nconst V1EventEndpoint = `https://events.pagerduty.com/generic/2010-04-15/create_event.json`\n\n// Context is an interface for the contexts field in V1 PD events.\ntype Context interface {\n\tGetType() string\n}\n\ntype ContextLink struct {\n\tType string `json:\"type\"`\n\tHref string `json:\"href\"`\n\tText string `json:\"text,omitempty\"`\n}\n\ntype ContextImage struct {\n\tType string `json:\"type\"`\n\tSrc  string `json:\"src\"`\n\tHref string `json:\"href,omitempty\"`\n\tAlt  string `json:\"alt,omitempty\"`\n}\n\ntype Event struct {\n\tServiceKey  string                 `json:\"service_key\"`\n\tEventType   string                 `json:\"event_type\"`\n\tDescription string                 `json:\"description\"`\n\tIncidentKey string                 `json:\"incident_key,omitempty\"`\n\tDetails     map[string]interface{} `json:\"details,omitempty\"` // arbitrary json\n\tClient      string                 `json:\"client,omitempty\"`\n\tClientUrl   string                 `json:\"client_url,omitempty\"`\n\tContexts    []Context              `json:\"contexts,omitempty\"`\n}\n\ntype Error struct {\n\tMessage string   `json:\"message\"`\n\tCode    int      `json:\"code\"`\n\tErrors  []string `json:\"errors\"`\n}\n\ntype ErrorResponse struct {\n\tError Error `json:\"error\"`\n}\n\ntype Response struct {\n\tStatus      string   `json:\"status\"`\n\tMessage     string   `json:\"message\"`\n\tIncidentKey string   `json:\"incident_key,omitempty\"`\n\tErrors      []string `json:\"errors,omitempty\"`\n\tStatusCode  int      `json:\"\"`\n}\n\n// NewEvent returns an initialized Event structure. You probably don't\n// want to use this and instead use NewTrigger/NewAck/NewResolve.\nfunc NewEvent(serviceKey, eventType, description string) *Event {\n\treturn &Event{\n\t\tServiceKey:  serviceKey,\n\t\tEventType:   eventType,\n\t\tDescription: description,\n\t\tDetails:     make(map[string]interface{}),\n\t\tContexts:    make([]Context, 0),\n\t}\n}\n\nfunc NewTrigger(serviceKey, description string) *Event {\n\treturn NewEvent(serviceKey, \"trigger\", description)\n}\n\nfunc NewAck(serviceKey, description string) *Event {\n\treturn NewEvent(serviceKey, \"acknowledge\", description)\n}\n\nfunc NewResolve(serviceKey, description string) *Event {\n\treturn NewEvent(serviceKey, \"resolve\", description)\n}\n\nfunc NewResponse(status, message, incidentKey string) *Response {\n\tout := Response{\n\t\tStatus:      status,\n\t\tMessage:     message,\n\t\tIncidentKey: incidentKey,\n\t\tErrors:      make([]string, 0),\n\t}\n\n\treturn &out\n}\n\n// Send posts the event to Pagerduty using the provided token.\nfunc (e *Event) Send(token string) (*Response, error) {\n\terr := e.checkRequired()\n\tif err != nil {\n\t\treturn e.respond(\"error\", err.Error()), err\n\t}\n\n\tjs, err := json.Marshal(e)\n\tif err != nil {\n\t\tlog.Printf(\"json.Marshal failed: %s\\n\", err)\n\t\treturn e.respond(\"error\", err.Error()), err\n\t}\n\n\tresp, err := authenticatedPost(token, V1EventEndpoint, js)\n\tif err != nil {\n\t\treturn e.respond(\"error\", err.Error()), err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif resp.StatusCode == 200 {\n\t\tout := Response{}\n\t\terr = json.Unmarshal(body, &out)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"json.Unmarshal failed: %s\\n\", err)\n\t\t\treturn nil, err\n\t\t}\n\t\tout.StatusCode = resp.StatusCode\n\t\treturn &out, nil\n\t} else {\n\t\tmsg := fmt.Sprintf(\"Server returned %d: %q\", resp, string(body))\n\t\treturn e.respond(\"error\", msg), errors.New(msg)\n\t}\n}\n\nfunc (e *Event) respond(status, message string) *Response {\n\treturn NewResponse(status, message, e.IncidentKey)\n}\n\nfunc (e *Event) checkRequired() error {\n\tet := e.EventType\n\n\tif len(et) == 0 {\n\t\treturn errors.New(\"EventType is a required field.\")\n\t}\n\n\tif et != \"trigger\" && et != \"acknowledge\" && et != \"resolve\" {\n\t\tmsg := fmt.Sprintf(\"EventType must be one of 'trigger', 'acknowledge', or 'resolve'. Got: %q\", et)\n\t\treturn errors.New(msg)\n\t}\n\n\tif len(e.ServiceKey) == 0 {\n\t\treturn errors.New(\"ServiceKey is a required field.\")\n\t}\n\n\tif len(e.Description) == 0 {\n\t\treturn errors.New(\"Description is a required field.\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *ContextLink) GetType() string {\n\treturn \"link\"\n}\n\nfunc (c *ContextImage) GetType() string {\n\treturn \"image\"\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_events_v2.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io/ioutil\"\n)\n\n// https://v2.developer.pagerduty.com/docs/events-api-v2\nconst V2EventEndpoint = `https://events.pagerduty.com/v2/enqueue`\n\n// data structures for the PagerDuty Common Event Format\n// Timestamp\ntype EventPayload struct {\n\tSummary   string            `json:\"summary\"`             // high-level text\n\tSeverity  string            `json:\"severity\"`            // enum: info, warning, error, critical\n\tSource    string            `json:\"source,omitempty\"`    // e.g. hostname, IP, ARN\n\tComponent string            `json:\"component,omitempty\"` // e.g. \"mysql\", \"keepalive\"\n\tGroup     string            `json:\"group,omitempty\"`     // e.g. \"www\", \"prod-data\"\n\tClass     string            `json:\"class,omitempty\"`     // e.g. \"High CPU\", \"Latency\"\n\tCustom    map[string]string `json:\"custom_details\"`\n}\n\ntype EventImage struct {\n\tSrc  string `json:\"src\"`\n\tHref string `json:\"href\"`\n\tAlt  string `json:\"alt\"`\n}\n\ntype EventBody struct {\n\tRoutingKey string       `json:\"routing_key\"`\n\tAction     string       `json:\"event_action\"`        // e.g. \"trigger\"\n\tDedupKey   string       `json:\"dedup_key,omitempty\"` // arbitrary key for server-side dedup\n\tPayload    EventPayload `json:\"payload\"`\n\tImages     []EventImage `json:\"images\"`\n\tClient     string       `json:\"client\"`     // e.g. \"Scorebot/#core\"\n\tClientUrl  string       `json:\"client_url\"` // e.g. \"https://scorebot.prod.netflix.net\"\n}\n\ntype EventResult struct {\n\tStatus     string `json:\"status\"`    // e.g. \"success\"\n\tMessage    string `json:\"message\"`   // e.g. \"Event processed\"\n\tDedupKey   string `json:\"dedup_key\"` // a uuid-ish key\n\tStatusCode int    `json:\"-\"`\n}\n\nfunc NewV2Event(routingKey string) *EventBody {\n\tdetails := make(map[string]string)\n\tout := EventBody{\n\t\tRoutingKey: routingKey,\n\t\tAction:     \"trigger\",\n\t\tPayload: EventPayload{\n\t\t\t// provide defaults for required fields\n\t\t\tSummary:  \"Something happened! This is the default summary.\",\n\t\t\tSource:   \"unspecified\",\n\t\t\tSeverity: \"error\",\n\t\t\tCustom:   details,\n\t\t},\n\t\tImages: []EventImage{},\n\t}\n\n\treturn &out\n}\n\nfunc (eb *EventBody) Send(token string) (EventResult, error) {\n\tout := EventResult{Status: \"failed\"}\n\n\terr := eb.checkFields()\n\tif err != nil {\n\t\treturn out, err\n\t}\n\n\tjs, err := json.Marshal(eb)\n\tif err != nil {\n\t\tmsg := fmt.Sprintf(\"json.Marshal failed: %s\", err)\n\t\tout.Message = msg\n\t\tlog.Println(msg)\n\t\treturn out, err\n\t}\n\n\tresp, err := authenticatedPost(token, V2EventEndpoint, js)\n\tif err != nil {\n\t\tmsg := fmt.Sprintf(\"POST failed: %s\", err)\n\t\tout.Message = msg\n\t\tlog.Println(msg)\n\t\treturn out, err\n\t}\n\tdefer resp.Body.Close()\n\n\tout.StatusCode = resp.StatusCode\n\n\tbody, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn out, err\n\t}\n\n\t// 200 means the event has been received and is on its way to a device.\n\t// 202 means they received the event and will send asynchronously.\n\t// Return success for all 2xx results.\n\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\terr = json.Unmarshal(body, &out)\n\t\tif err != nil {\n\t\t\tmsg := fmt.Sprintf(\"json.Unmarshal failed: %s\", err)\n\t\t\tout.Status = \"failed\"\n\t\t\tout.Message = msg\n\t\t\tlog.Println(msg)\n\t\t\treturn out, err\n\t\t}\n\t\treturn out, nil\n\t} else {\n\t\tmsg := fmt.Sprintf(\"Server returned %d: %q\", resp, string(body))\n\t\tout.Message = msg\n\t\treturn out, errors.New(msg)\n\t}\n}\n\nfunc (eb *EventBody) checkFields() error {\n\t// TODO: check some fields\n\treturn nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_oncall.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n)\n\n// https://v2.developer.pagerduty.com/v2/page/api-reference#!/On-Calls/get_oncalls\n// TODO: figure out if query should be a typed struct or validated\nfunc GetOncalls(token string, query map[string][]string) ([]Oncall, error) {\n\toncalls := make([]Oncall, 0)\n\toffset := 0\n\tlimit := 100\n\n\tfor {\n\t\toncallsResp := OncallsResponse{}\n\n\t\turl := pagedUrl(\"/oncalls\", offset, limit, query)\n\n\t\tresp, err := authenticatedGet(url, token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GET %s failed: %s\", url, err)\n\t\t\treturn oncalls, err\n\t\t}\n\n\t\tdata, err := ioutil.ReadAll(resp.Body)\n\n\t\terr = json.Unmarshal(data, &oncallsResp)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\n\\n%s\\n\\n\", data)\n\t\t\tlog.Printf(\"json.Unmarshal of data from %q failed: %s\", url, err)\n\t\t\treturn oncalls, err\n\t\t}\n\n\t\toncalls = append(oncalls, oncallsResp.Oncalls...)\n\n\t\tif oncallsResp.More {\n\t\t\toffset = offset + limit\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn oncalls, nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_policy.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n)\n\n// https://v2.developer.pagerduty.com/v2/page/api-reference#!/Escalation_Policies/get_escalation_policies\n\nfunc GetEscalationPolicies(token string, params map[string][]string) ([]EscalationPolicy, error) {\n\tpolicies := make([]EscalationPolicy, 0)\n\toffset := 0\n\tlimit := 100\n\n\tfor {\n\t\tepresp := EscalationPolicyResponse{}\n\n\t\turl := pagedUrl(\"/escalation_policies\", offset, limit, params)\n\n\t\tresp, err := authenticatedGet(url, token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GET %s failed: %s\", url, err)\n\t\t\treturn policies, err\n\t\t}\n\n\t\tdata, err := ioutil.ReadAll(resp.Body)\n\n\t\terr = json.Unmarshal(data, &epresp)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"json.Unmarshal failed: %s\", err)\n\t\t\treturn policies, err\n\t\t}\n\n\t\tpolicies = append(policies, epresp.EscalationPolicies...)\n\n\t\tif epresp.More {\n\t\t\toffset = offset + limit\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn policies, nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_schedule.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io/ioutil\"\n)\n\ntype scheduleOncallUsersResponse struct {\n\tUsers []User `json:\"users\"`\n}\n\n// https://v2.developer.pagerduty.com/v2/page/api-reference#!/Schedules/get_schedules_id\nfunc GetScheduleOncalls(token, id string) ([]User, error) {\n\tout := scheduleOncallUsersResponse{}\n\n\turl := pagedUrl(\"/schedules/\"+id+\"/users\", 0, 0, nil)\n\n\tresp, err := authenticatedGet(url, token)\n\tif err != nil {\n\t\tlog.Printf(\"GET %s failed: %s\", url, err)\n\t\treturn []User{}, err\n\t}\n\n\tdata, err := ioutil.ReadAll(resp.Body)\n\n\terr = json.Unmarshal(data, &out)\n\tif err != nil {\n\t\tlog.Printf(\"json.Unmarshal failed: %s\", err)\n\t\treturn []User{}, err\n\t}\n\n\treturn out.Users, nil\n}\n\nfunc GetSchedules(token string, params map[string][]string) ([]Schedule, error) {\n\tschedules := make([]Schedule, 0)\n\toffset := 0\n\tlimit := 100\n\n\tfor {\n\t\tschedulesResp := SchedulesResponse{}\n\n\t\turl := pagedUrl(\"/schedules\", offset, limit, params)\n\n\t\tresp, err := authenticatedGet(url, token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GET %s failed: %s\", url, err)\n\t\t\treturn schedules, err\n\t\t}\n\n\t\tdata, err := ioutil.ReadAll(resp.Body)\n\n\t\terr = json.Unmarshal(data, &schedulesResp)\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"\\n\\n%s\\n\\n\", data)\n\t\t\tlog.Printf(\"json.Unmarshal of data from %q failed: %s\", url, err)\n\t\t\treturn schedules, err\n\t\t}\n\n\t\tschedules = append(schedules, schedulesResp.Schedules...)\n\n\t\tif schedulesResp.More {\n\t\t\toffset = offset + limit\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn schedules, nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_service.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n// API docs: https://developer.pagerduty.com/documentation/rest/escalation_policies/on_call\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n)\n\nfunc GetServices(token string, params map[string][]string) ([]Service, error) {\n\tservices := make([]Service, 0)\n\toffset := 0\n\tlimit := 100\n\n\tfor {\n\t\tsvcResp := ServicesResponse{}\n\n\t\tsvcsUrl := pagedUrl(\"/services\", offset, limit, params)\n\n\t\tresp, err := authenticatedGet(svcsUrl, token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GET %s failed: %s\", svcsUrl, err)\n\t\t\treturn services, err\n\t\t}\n\n\t\tdata, err := ioutil.ReadAll(resp.Body)\n\n\t\terr = json.Unmarshal(data, &svcResp)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"json.Unmarshal failed: %s\", err)\n\t\t\treturn []Service{}, err\n\t\t}\n\n\t\tservices = append(services, svcResp.Services...)\n\n\t\tif svcResp.More {\n\t\t\toffset = offset + limit\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn services, nil\n}\n\nfunc GetService(token, id string) (Service, error) {\n\tout := Service{\n\t\tIncidentCounts: IncidentCounts{},\n\t}\n\n\tsvcsUrl := pagedUrl(\"/services/\"+id, 0, 0, nil)\n\n\tresp, err := authenticatedGet(svcsUrl, token)\n\tif err != nil {\n\t\tlog.Printf(\"GET %s failed: %s\", svcsUrl, err)\n\t\treturn out, err\n\t}\n\n\tdata, err := ioutil.ReadAll(resp.Body)\n\n\terr = json.Unmarshal(data, &out)\n\tif err != nil {\n\t\tlog.Printf(\"json.Unmarshal failed: %s\", err)\n\t\treturn out, err\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_team.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n)\n\n// https://v2.developer.pagerduty.com/v2/page/api-reference#!/Teams/get_teams\n\nfunc GetTeams(token string, params map[string][]string) ([]Team, error) {\n\tout := make([]Team, 0)\n\toffset := 0\n\tlimit := 100\n\n\tfor {\n\t\turl := pagedUrl(\"/teams\", offset, limit, params)\n\n\t\tresp, err := authenticatedGet(url, token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GET %s failed: %s\", url, err)\n\t\t\treturn out, err\n\t\t}\n\n\t\tdata, err := ioutil.ReadAll(resp.Body)\n\n\t\toresp := TeamsResponse{}\n\t\terr = json.Unmarshal(data, &oresp)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"json.Unmarshal failed: %s\", err)\n\t\t\treturn out, err\n\t\t}\n\n\t\tout = append(out, oresp.Teams...)\n\n\t\tif oresp.More {\n\t\t\toffset = offset + limit\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_types.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"time\"\n)\n\ntype ContactMethod struct {\n\tId             string `json:\"id\"`\n\tType           string `json:\"type\"`\n\tLabel          string `json:\"label\"`\n\tAddress        string `json:\"address\"`\n\tSendShortEmail bool   `json:\"send_short_email\"`\n}\n\ntype NotificationRule struct {\n\tId                  string        `json:\"id\"`\n\tType                string        `json:\"type\"`\n\tStartDelayInMinutes int           `json:\"start_delay_in_minutes\"`\n\tCreatedAt           string        `json:\"created_at\"`\n\tUrgency             string        `json:\"urgency\"`\n\tContactMethod       ContactMethod `json:\"contact_method\"`\n}\n\ntype Team struct {\n\tId          string `json:\"id\"`\n\tType        string `json:\"type\"`\n\tSummary     string `json:\"summary\"`\n\tSelf        string `json:\"self\"`\n\tHtmlUrl     string `json:\"html_url\"`\n\tName        string `json:\"name\"`\n\tDescription string `json:\"description\"`\n}\n\ntype TeamRef struct {\n\tId      string `json:\"id\"`\n\tType    string `json:\"type\"`\n\tSummary string `json:\"summary\"`\n\tSelf    string `json:\"self\"`\n\tHtmlUrl string `json:\"html_url\"`\n}\n\ntype TeamsResponse struct {\n\tTeams  []Team `json:\"teams\"`\n\tOffset int    `json:\"offset\"`\n\tLimit  int    `json:\"limit\"`\n\tMore   bool   `json:\"more\"`\n\tTotal  int    `json:\"total,omitempty\"`\n}\n\ntype Schedule struct {\n\tId                   string             `json:\"id\"`\n\tSummary              string             `json:\"summary\"`\n\tType                 string             `json:\"type\"`\n\tSelf                 string             `json:\"self\"`\n\tHtmlUrl              string             `json:\"html_url\"`\n\tScheduleLayers       []ScheduleLayer    `json:\"schedule_layers\"`\n\tTimezone             string             `json:\"time_zone\"`\n\tName                 string             `json:\"name\"`\n\tDescription          string             `json:\"description\"`\n\tFinalSchedule        SubSchedule        `json:\"final_schedule,omitempty\"`\n\tOverridesSubSchedule SubSchedule        `json:\"overrides_subschedule,omitempty\"`\n\tEscalationPolicies   []EscalationPolicy `json:\"escalation_policies\"`\n\tUsers                []UserRef          `json:\"users\"`\n}\n\ntype ScheduleRef struct {\n\tId      string `json:\"id\"`\n\tType    string `json:\"type\"`\n\tSummary string `json:\"summary\"`\n\tSelf    string `json:\"self\"`\n\tHtmlUrl string `json:\"html_url\"`\n}\n\ntype SchedulesResponse struct {\n\tSchedules []Schedule `json:\"schedules\"`\n\tOffset    int        `json:\"offset\"`\n\tLimit     int        `json:\"limit\"`\n\tMore      bool       `json:\"more\"`\n\tTotal     int        `json:\"total,omitempty\"`\n}\n\ntype ScheduleLayer struct {\n\tId                         string               `json:\"id,omitempty\"`\n\tStart                      *time.Time           `json:\"start\"`\n\tEnd                        *time.Time           `json:\"end,omitempty\"`\n\tType                       string               `json:\"type\"`\n\tSummary                    string               `json:\"summary\"`\n\tSelf                       string               `json:\"self\"`\n\tHtmlUrl                    string               `json:\"html_url\"`\n\tUsers                      []UserRef            `json:\"users\"`\n\tRestrictions               []Restriction        `json:\"restrictions\"`\n\tRotationVirtualStart       string               `json:\"rotation_virtual_start\"`\n\tRotationTurnLengthSeconds  int                  `json:\"rotation_turn_length_seconds\"`\n\tName                       string               `json:\"name\"`\n\tRenderedScheduleEntries    []ScheduleLayerEntry `json:\"rendered_schedule_entries\"`\n\tRenderedCoveragePercentage float64              `json:\"rendered_coverage_percentage\"`\n}\n\ntype SubSchedule struct {\n\tName                       string               `json:\"name\"`\n\tRenderedScheduleEntries    []ScheduleLayerEntry `json:\"rendered_schedule_entries\"`\n\tRenderedCoveragePercentage float64              `json:\"rendered_coverage_percentage\"`\n}\n\ntype ScheduleLayerEntry struct {\n\tUser  UserRef `json:\"user\"`\n\tStart string  `json:\"start\"`\n\tEnd   string  `json:\"end\"`\n}\n\ntype Restriction struct {\n\tType            string `json:\"type\"`\n\tDurationSeconds int    `json:\"duration_seconds\"`\n\tStartTimeOfDay  string `json:\"start_time_of_day\"`\n}\n\ntype EscalationPolicy struct {\n\tId              string           `json:\"id\"`\n\tSummary         string           `json:\"summary\"`\n\tType            string           `json:\"type\"`\n\tSelf            string           `json:\"self\"`\n\tHtmlUrl         string           `json:\"html_url\"`\n\tName            string           `json:\"name\"`\n\tDescription     string           `json:\"description\"`\n\tNumLoops        int              `json:\"num_loops\"`\n\tRepeatEnabled   bool             `json:\"repeat_enabled\"`\n\tEscalationRules []EscalationRule `json:\"escalation_rules\"`\n\tServiceRefs     []ServiceRef     `json:\"services\"`\n\tTeamRefs        []TeamRef        `json:\"teams\"`\n}\n\ntype EscalationPolicyResponse struct {\n\tEscalationPolicies []EscalationPolicy `json:\"escalation_policies\"`\n\tOffset             int                `json:\"offset\"`\n\tLimit              int                `json:\"limit\"`\n\tMore               bool               `json:\"more\"`\n\tTotal              int                `json:\"total,omitempty\"`\n}\n\ntype EscalationRule struct {\n\tId                       string             `json:\"id\"`\n\tEscalationDelayInMinutes int                `json:\"escalation_delay_in_minutes\"`\n\tTargets                  []EscalationTarget `json:\"targets\"`\n}\n\ntype EscalationPolicyRef struct {\n\tId      string `json:\"id\"`\n\tType    string `json:\"type\"`\n\tSummary string `json:\"summary\"`\n\tSelf    string `json:\"self\"`\n\tHtmlUrl string `json:\"html_url\"`\n}\n\ntype EscalationTarget struct {\n\tId   string `json:\"id\"`\n\tType string `json:\"type\"`\n}\n\ntype PolicyService struct {\n\tId                 string `json:\"id\"`\n\tName               string `json:\"name\"`\n\tIntegrationEmail   string `json:\"integration_email\"`\n\tHtmlUrl            string `json:\"html_url\"`\n\tEscalationPolicyId string `json:\"escalation_policy_id\"`\n}\n\ntype Integration struct {\n\tId               string `json:\"id\"`\n\tType             string `json:\"type\"`\n\tSummary          string `json:\"summary\"`\n\tSelf             string `json:\"self\"`\n\tHtmlUrl          string `json:\"html_url\"`\n\tName             string `json:\"name\"`\n\tCreatedAt        string `json:\"created_at\"`\n\tIntegrationKey   string `json:\"integration_key\"`\n\tIntegrationEmail string `json:\"integration_email\"`\n\t// ignore service\n\t// ignore vendor\n\t// ignore config\n}\n\ntype IncidentCounts struct {\n\tTriggered    int `json:\"triggered\"`\n\tAcknowledged int `json:\"acknowledged\"`\n\tResolved     int `json:\"resolved\"`\n\tTotal        int `json:\"total\"`\n}\n\n// Service represents a Pagerduty service object from /api/v1/services/:id\ntype Service struct {\n\tId                     string           `json:\"id\"`\n\tType                   string           `json:\"type\"`\n\tName                   string           `json:\"name\"`\n\tServiceUrl             string           `json:\"service_url\"`\n\tServiceKey             string           `json:\"service_key\"`\n\tAutoResolveTimeout     int              `json:\"auto_resolve_timeout\"`\n\tAcknowledgementTimeout int              `json:\"acknowledgement_timeout\"`\n\tCreatedAt              string           `json:\"created_at\"`\n\tStatus                 string           `json:\"status\"`\n\tLastIncidentTimestamp  string           `json:\"last_incident_timestamp\"`\n\tEmailIncidentCreation  string           `json:\"email_incident_creation\"`\n\tIncidentCounts         IncidentCounts   `json:\"incident_counts\"`\n\tEmailFilterMode        string           `json:\"email_filter_mode\"`\n\tDescription            string           `json:\"description\"`\n\tIntegrations           []Integration    `json:\"integrations\"`\n\tEscalationPolicy       EscalationPolicy `json:\"escalation_policy\"`\n\tTeams                  []Team           `json:\"teams\"`\n}\n\ntype ServiceRef struct {\n\tId      string `json:\"id\"`\n\tType    string `json:\"type\"`\n\tSummary string `json:\"summary\"`\n\tSelf    string `json:\"self\"`\n\tHtmlUrl string `json:\"html_url\"`\n}\n\ntype ServicesResponse struct {\n\tServices []Service `json:\"services\"`\n\tLimit    int       `json:\"limit\"`\n\tOffset   int       `json:\"offset\"`\n\tMore     bool      `json:\"more\"`\n\tTotal    int       `json:\"total\"`\n}\n\ntype User struct {\n\tId                string             `json:\"id\"`\n\tType              string             `json:\"type\"`\n\tSummary           string             `json:\"summary\"`\n\tSelf              string             `json:\"self\"`\n\tHtmlUrl           string             `json:\"html_url\"`\n\tName              string             `json:\"name\"`\n\tEmail             string             `json:\"email\"`\n\tJobTitle          string             `json:\"job_title\"`\n\tTimezone          string             `json:\"time_zone\"`\n\tColor             string             `json:\"color\"`\n\tRole              string             `json:\"role,omitempty\"`\n\tAvatarUrl         string             `json:\"avatar_url,omitempty\"`\n\tDescription       string             `json:\"description,omitempty\"`\n\tBilled            bool               `json:\"billed,omitempty\"`\n\tUserUrl           string             `json:\"user_url,omitempty\"`\n\tInvitationSent    bool               `json:\"invitation_sent,omitempty\"`\n\tMarketingOptOut   bool               `json:\"marketing_opt_out,omitempty\"`\n\tContactMethods    []ContactMethod    `json:\"contact_methods\"`\n\tNotificationRules []NotificationRule `json:\"notification_rules\"`\n\tTeams             []Team             `json:\"teams\"`\n}\n\ntype UserRef struct {\n\tId      string `json:\"id\"`\n\tType    string `json:\"type\"`\n\tSummary string `json:\"summary\"`\n\tSelf    string `json:\"self\"`\n\tHtmlUrl string `json:\"html_url\"`\n}\n\ntype UsersResponse struct {\n\tUsers  []User `json:\"users\"`\n\tOffset int    `json:\"offset\"`\n\tLimit  int    `json:\"limit\"`\n\tMore   bool   `json:\"more\"`\n\tTotal  int    `json:\"total\"`\n}\n\ntype Oncall struct {\n\tEscalationPolicy EscalationPolicy `json:\"escalation_policy\"`\n\tUser             User             `json:\"user\"`\n\tSchedule         Schedule         `json:\"schedule\"`\n\tEscalationLevel  int              `json:\"escalation_level\"`\n\tStart            *time.Time       `json:\"start\"`\n\tEnd              *time.Time       `json:\"end\"`\n}\n\ntype OncallsResponse struct {\n\tOncalls []Oncall `json:\"oncalls\"`\n\tOffset  int      `json:\"offset\"`\n\tLimit   int      `json:\"limit\"`\n\tMore    bool     `json:\"more\"`\n\tTotal   int      `json:\"total,omitempty\"`\n}\n\ntype Override struct {\n\tId    string  `json:\"id\"`\n\tStart string  `json:\"start\"`\n\tEnd   string  `json:\"end\"`\n\tUser  UserRef `json:\"user\"`\n}\n"
  },
  {
    "path": "plugins/pagerduty/pd_user.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"io/ioutil\"\n)\n\n// https://v2.developer.pagerduty.com/v2/page/api-reference#!/On-Calls/get_oncalls\n\nfunc GetUsersOncall(token string) ([]Oncall, error) {\n\tout := make([]Oncall, 0)\n\toffset := 0\n\tlimit := 100\n\n\tfor {\n\t\turl := pagedUrl(\"/oncalls\", offset, limit, nil)\n\n\t\tresp, err := authenticatedGet(url, token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GET %s failed: %s\", url, err)\n\t\t\treturn out, err\n\t\t}\n\n\t\tdata, err := ioutil.ReadAll(resp.Body)\n\n\t\toresp := OncallsResponse{}\n\t\terr = json.Unmarshal(data, &oresp)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"json.Unmarshal failed: %s\", err)\n\t\t\treturn out, err\n\t\t}\n\n\t\tout = append(out, oresp.Oncalls...)\n\n\t\tif oresp.More {\n\t\t\toffset = offset + limit\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn out, nil\n}\n\nfunc GetUsers(token string, params map[string][]string) ([]User, error) {\n\tout := make([]User, 0)\n\toffset := 0\n\tlimit := 100\n\n\tfor {\n\t\turl := pagedUrl(\"/users\", offset, limit, params)\n\n\t\tresp, err := authenticatedGet(url, token)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"GET %s failed: %s\", url, err)\n\t\t\treturn out, err\n\t\t}\n\n\t\tdata, err := ioutil.ReadAll(resp.Body)\n\n\t\turesp := UsersResponse{}\n\t\terr = json.Unmarshal(data, &uresp)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"json.Unmarshal failed: %s\", err)\n\t\t\treturn out, err\n\t\t}\n\n\t\tout = append(out, uresp.Users...)\n\n\t\tif uresp.More {\n\t\t\toffset = offset + limit\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn out, nil\n}\n"
  },
  {
    "path": "plugins/pagerduty/plugin.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\n// the hal.secrets key that should contain the pagerduty auth token\nconst PagerdutyTokenKey = `pagerduty.token`\n\n// the key name used for caching the full escalation policy\nconst CacheKey = `pagerduty.policy_cache`\n\nconst cacheExpire = time.Minute * 10\n\nconst DefaultCacheInterval = \"1h\"\n\nfunc Register() {\n\t// use a custom RE because !page might be \"!page foo\" or \"!pagefoo\"\n\tpg := hal.Plugin{\n\t\tName:  \"page\",\n\t\tFunc:  page,\n\t\tRegex: \"^[[:space:]]*!page\",\n\t}\n\tpg.Register()\n\n\toc := hal.Plugin{\n\t\tName:    \"oncall\",\n\t\tFunc:    oncall,\n\t\tInit:    oncallInit,\n\t\tCommand: \"oncall\",\n\t}\n\toc.Register()\n\n\tpoller := hal.Plugin{\n\t\tName:    \"pd_poller\",\n\t\tFunc:    pollerHandler,\n\t\tInit:    pollerInit,\n\t\tCommand: \"pdpoller\",\n\t}\n\tpoller.Register()\n}\n\n// TODO: consider making the token key per-room so different rooms can use different tokens\n// doing this will require a separate cache object per token...\nfunc getSecrets() (token string, err error) {\n\tsecrets := hal.Secrets()\n\ttoken = secrets.Get(PagerdutyTokenKey)\n\tif token == \"\" {\n\t\terr = fmt.Errorf(\"Your Pagerduty auth token does not seem to be configured. Please add the %q secret.\", PagerdutyTokenKey)\n\t}\n\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n\n\treturn token, err\n}\n"
  },
  {
    "path": "plugins/pagerduty/poller.go",
    "content": "package pagerduty\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\n// TODO: add a timestamp-based cleanup for old edges/attrs/etc.\n\nfunc pollerHandler(evt hal.Evt) {\n\t// nothing yet - TODO: add control code, e.g. force refresh\n}\n\nfunc pollerInit(inst *hal.Instance) {\n\tpf := hal.PeriodicFunc{\n\t\tName:     \"pagerduty-poller\",\n\t\tInterval: time.Hour,\n\t\tFunction: ingestPagerdutyAccount,\n\t}\n\n\tpf.Register()\n\tgo pf.Start()\n}\n\nfunc ingestPagerdutyAccount() {\n\ttoken, err := getSecrets()\n\tif err != nil || token == \"\" {\n\t\tlog.Printf(\"pagerduty: %s is not set up in hal.Secrets. Cannot continue.\", PagerdutyTokenKey)\n\t\treturn\n\t}\n\n\tingestPDusers(token)\n\tingestPDteams(token)\n\tingestPDservices(token)\n\tingestPDschedules(token)\n}\n\nfunc ingestPDusers(token string) {\n\tparams := map[string][]string{\"include[]\": []string{\"contact_methods\"}}\n\tusers, err := GetUsers(token, params)\n\tif err != nil {\n\t\tlog.Printf(\"Could not retreive users from the Pagerduty API: %s\", err)\n\t\treturn\n\t}\n\n\tfor _, user := range users {\n\t\tattrs := map[string]string{\n\t\t\t\"pd-user-id\": user.Id,\n\t\t\t\"name\":       user.Name,\n\t\t\t\"email\":      user.Email,\n\t\t}\n\n\t\t// plug in the contact methods\n\t\tfor _, cm := range user.ContactMethods {\n\t\t\tif strings.HasSuffix(cm.Type, \"_reference\") {\n\t\t\t\tlog.Printf(\"contact methods not included in data: try adding include[]=contact_methods to the request\")\n\t\t\t} else {\n\t\t\t\tattrs[cm.Type+\"-id\"] = cm.Id\n\t\t\t\tattrs[cm.Type] = cm.Address\n\t\t\t}\n\t\t}\n\n\t\tedges := []string{\"name\", \"email\", \"phone_contact_method\", \"sms_contact_method\"}\n\t\tlogit(hal.Directory().Put(user.Id, \"pd-user\", attrs, edges))\n\n\t\tfor _, team := range user.Teams {\n\t\t\tlogit(hal.Directory().PutNode(team.Id, \"pd-team\"))\n\t\t\tlogit(hal.Directory().PutEdge(team.Id, \"pd-team\", user.Id, \"pd-user\"))\n\t\t}\n\t}\n}\n\nfunc ingestPDteams(token string) {\n\tteams, err := GetTeams(token, nil)\n\tif err != nil {\n\t\tlog.Printf(\"Could not retreive teams from the Pagerduty API: %s\", err)\n\t\treturn\n\t}\n\n\tfor _, team := range teams {\n\t\tattrs := map[string]string{\n\t\t\t\"pd-team-id\":          team.Id,\n\t\t\t\"pd-team\":             team.Name,\n\t\t\t\"pd-team-summary\":     team.Summary,\n\t\t\t\"pd-team-description\": team.Description,\n\t\t}\n\n\t\tlogit(hal.Directory().Put(team.Id, \"pd-team\", attrs, []string{\"pd-team-id\"}))\n\t}\n}\n\nfunc ingestPDservices(token string) {\n\tparams := map[string][]string{\"include[]\": []string{\"integrations\"}}\n\tservices, err := GetServices(token, params)\n\tif err != nil {\n\t\tlog.Printf(\"Could not retreive services from the Pagerduty API: %s\", err)\n\t\treturn\n\t}\n\n\tfor _, service := range services {\n\t\tattrs := map[string]string{\n\t\t\t\"pd-service-id\":           service.Id,\n\t\t\t\"pd-service\":              service.Name,\n\t\t\t\"pd-service-description\":  service.Description,\n\t\t\t\"pd-escalation-policy-id\": service.EscalationPolicy.Id,\n\t\t}\n\n\t\tedges := []string{\"pd-service-key\", \"pd-service-id\", \"pd-escalation-policy-id\", \"pd-integration-key\"}\n\t\tlogit(hal.Directory().Put(service.Id, \"pd-service\", attrs, edges))\n\n\t\tfor _, team := range service.Teams {\n\t\t\tlogit(hal.Directory().PutNode(team.Id, \"pd-team\"))\n\t\t\tlogit(hal.Directory().PutEdge(team.Id, \"pd-team\", service.Id, \"pd-service\"))\n\t\t}\n\n\t\tfor _, igr := range service.Integrations {\n\t\t\tif igr.Type == \"generic_email_inbound_integration\" {\n\t\t\t\tlogit(hal.Directory().PutNode(igr.IntegrationEmail, \"pd-integration-email\"))\n\t\t\t\tlogit(hal.Directory().PutEdge(igr.IntegrationEmail, \"pd-integration-email\", service.Id, \"pd-service\"))\n\n\t\t\t\tfor _, team := range service.Teams {\n\t\t\t\t\tlogit(hal.Directory().PutEdge(igr.IntegrationEmail, \"pd-integration-email\", team.Id, \"pd-team\"))\n\t\t\t\t}\n\t\t\t} else if igr.Type == \"events_api_v2_inbound_integration\" || igr.Type == \"generic_events_api_inbound_integration\" {\n\t\t\t\tlogit(hal.Directory().PutNode(igr.IntegrationKey, \"pd-integration-key\"))\n\t\t\t\tlogit(hal.Directory().PutEdge(igr.IntegrationKey, \"pd-integration-key\", service.Id, \"pd-service\"))\n\n\t\t\t\tfor _, team := range service.Teams {\n\t\t\t\t\tlogit(hal.Directory().PutEdge(igr.IntegrationKey, \"pd-integration-key\", team.Id, \"pd-team\"))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc ingestPDschedules(token string) {\n\tschedules, err := GetSchedules(token, nil)\n\tif err != nil {\n\t\tlog.Printf(\"Could not retreive schedules from the Pagerduty API: %s\", err)\n\t\treturn\n\t}\n\n\tfor _, schedule := range schedules {\n\t\tattrs := map[string]string{\n\t\t\t\"pd-schedule-id\":      schedule.Id,\n\t\t\t\"pd-schedule\":         schedule.Name,\n\t\t\t\"pd-schedule-summary\": schedule.Summary,\n\t\t}\n\n\t\tlogit(hal.Directory().Put(schedule.Id, \"pd-schedule\", attrs, []string{\"pd-schedule-id\"}))\n\n\t\tfor _, ep := range schedule.EscalationPolicies {\n\t\t\tlogit(hal.Directory().PutNode(ep.Id, \"pd-escalation-policy\"))\n\t\t\tlogit(hal.Directory().PutEdge(ep.Id, \"pd-escalation-policy\", schedule.Id, \"pd-schedule\"))\n\t\t}\n\n\t\tfor _, user := range schedule.Users {\n\t\t\tlogit(hal.Directory().PutNode(user.Id, \"pd-user\"))\n\t\t\tlogit(hal.Directory().PutEdge(user.Id, \"pd-user\", schedule.Id, \"pd-schedule\"))\n\t\t}\n\t}\n}\n\nfunc logit(err error) {\n\tif err != nil {\n\t\tlog.Println(\"pagerduty/hal_directory error: %s\", err)\n\t}\n}\n"
  },
  {
    "path": "plugins/pluginmgr/plugin.go",
    "content": "// Package pluginmgr is a plugin manager for hal that allows users to\n// manage plugins from inside chat or over REST.\npackage pluginmgr\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\n// NAME of the plugin\nconst NAME = \"pluginmgr\"\n\n// HELP text\nconst HELP = `\nExamples:\n!plugin list\n!plugin instances\n!plugin save\n!plugin attach <plugin> --room <room>\n!plugin attach --regex ^!foo <plugin> <room>\n!plugin detach <plugin> <room>\n!plugin group list\n!plugin group add <group_name> <plugin_name>\n!plugin group del <group_name> <plugin_name>\n\ne.g.\n!plugin attach uptime --room CORE\n!plugin detach uptime --room CORE\n!plugin save\n`\n\nconst PluginGroupTable = `\nCREATE TABLE IF NOT EXISTS plugin_groups (\n    group_name  VARCHAR(191),\n    plugin_name VARCHAR(191),\n\tts          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n    PRIMARY KEY(group_name, plugin_name)\n)`\n\ntype PluginGroupRow struct {\n\tGroup     string    `json:\"group\"`\n\tPlugin    string    `json:\"plugin\"`\n\tTimestamp time.Time `json:\"timestamp\"`\n}\n\ntype PluginGroup []*PluginGroupRow\n\nvar cli *hal.Cmd\n\n// Register makes this plugin available to the system.\nfunc Register() {\n\tplugin := hal.Plugin{\n\t\tName:    NAME,\n\t\tFunc:    pluginmgr,\n\t\tCommand: \"plugin\",\n\t}\n\n\tplugin.Register()\n\n\thal.SqlInit(PluginGroupTable)\n\n\tcli = hal.NewCmd(\"!plugin\", true).SetUsage(\"Manage bot plugins.\")\n\n\tcli.AddSubCmd(\"attach\").\n\t\tSetUsage(\"attach a plugin to the current or specified room with an optional command regex\").\n\t\tSubCmd().AddIdxParam(0, \"plugin\", true).\n\t\tSubCmd().AddIdxParam(1, \"room\", false).\n\t\tSubCmd().AddIdxParam(2, \"regex\", false)\n\n\tcli.AddSubCmd(\"detach\").\n\t\tSetUsage(\"detach a plugin from a room\").\n\t\tSubCmd().AddIdxParam(0, \"plugin\", true).\n\t\tSubCmd().AddIdxParam(1, \"room\", false).\n\t\tSubCmd().AddIdxParam(2, \"regex\", false)\n\n\tcli.AddSubCmd(\"save\").\n\t\tSetUsage(\"persist the configured plugins to the database\")\n\n\tcli.AddSubCmd(\"list\").\n\t\tSetUsage(\"list the attached plugins\")\n\n\tcli.AddSubCmd(\"instances\").\n\t\tSetUsage(\"list the available plugin instances\").\n\t\tSubCmd().AddIdxParam(0, \"room\", false)\n\n\tgrp := cli.AddSubCmd(\"group\")\n\tgrp.SetUsage(\"Plugin groups.\")\n\n\tgrp.AddSubCmd(\"list\").\n\t\tAddIdxParam(0, \"group\", false)\n\n\tgrp.AddSubCmd(\"add\").\n\t\tSubCmd().AddIdxParam(0, \"group\", true).\n\t\tSubCmd().AddIdxParam(1, \"plugin\", true)\n\n\tgrp.AddSubCmd(\"del\").\n\t\tSubCmd().AddIdxParam(0, \"group\", true).\n\t\tSubCmd().AddIdxParam(1, \"plugin\", true)\n}\n\nfunc pluginmgr(evt hal.Evt) {\n\treq, err := cli.Process(evt.BodyAsArgv())\n\tif err != nil {\n\t\tevt.Replyf(\"%s\\n%s\", err, cli.Usage())\n\t\treturn\n\t}\n\n\tsub := req.SubCmdInst()\n\tpr := hal.PluginRegistry()\n\n\t// read the param, check validity, return string\n\tplugin := func() string {\n\t\tname := sub.GetIdxParamInstByName(\"plugin\").MustString()\n\t\tp, err := pr.GetPlugin(name)\n\t\tif err != nil {\n\t\t\tevt.Replyf(\"No such plugin: %q\", name)\n\t\t\treturn \"\"\n\t\t}\n\n\t\treturn p.Name\n\t}\n\n\t// read the param, resolve name -> id as needed, return string\n\troom := func() string {\n\t\t// automatically defaults to the current room with or without the *\n\t\tr := evt.RoomId\n\t\trp := sub.GetIdxParamInstByName(\"room\")\n\t\tif rp.Found() {\n\t\t\tr = rp.MustString()\n\t\t}\n\n\t\t// the user may have provided --room with a room name\n\t\t// try to resolve a roomId with the broker, falling back to the name\n\t\tif evt.Broker != nil {\n\t\t\troomId := evt.Broker.RoomNameToId(r)\n\t\t\tif roomId != \"\" {\n\t\t\t\treturn roomId\n\t\t\t}\n\t\t}\n\n\t\treturn r\n\t}\n\n\t// read the param, grab the plugin, return string w/ default from\n\t// the plugin metadata\n\tregex := func() string {\n\t\t// only needs to work with commands that require the plugin arg\n\t\tpn := sub.GetIdxParamInstByName(\"plugin\").MustString()\n\t\tp, err := pr.GetPlugin(pn)\n\t\tif err != nil {\n\t\t\treturn \"\" // doesn't matter, nothing works without a good plugin\n\t\t}\n\t\treturn sub.GetIdxParamInstByName(\"regex\").DefString(p.Regex)\n\t}\n\n\tswitch req.SubCmdToken() {\n\tcase \"\", \"help\":\n\t\tevt.Reply(cli.Usage())\n\tcase \"attach\":\n\t\tattachPlugin(evt, plugin(), room(), regex())\n\tcase \"detach\":\n\t\tdetachPlugin(evt, plugin(), room())\n\tcase \"save\":\n\t\tsavePlugins(evt)\n\tcase \"list\":\n\t\tlistPlugins(evt)\n\tcase \"instances\":\n\t\tlistInstances(evt, room())\n\tcase \"group\":\n\t\tgsub := sub.SubCmdInst()\n\t\tg := gsub.GetIdxParamInstByName(\"group\").MustString()\n\t\tswitch sub.SubCmdToken() {\n\t\tcase \"add\":\n\t\t\tp := gsub.GetIdxParamInstByName(\"plugin\").MustString()\n\t\t\taddGroupPlugin(evt, g, p)\n\t\tcase \"del\":\n\t\t\tp := gsub.GetIdxParamInstByName(\"plugin\").MustString()\n\t\t\tdelGroupPlugin(evt, g, p)\n\t\tcase \"list\":\n\t\t\tlistGroupPlugin(evt, g)\n\t\t}\n\t}\n}\n\nfunc listPlugins(evt hal.Evt) {\n\thdr := []string{\"Plugin Name\", \"Default RE\", \"Status\"}\n\trows := [][]string{}\n\tpr := hal.PluginRegistry()\n\n\tfor _, p := range pr.ActivePluginList() {\n\t\trow := []string{p.Name, p.Regex, \"active\"}\n\t\trows = append(rows, row)\n\t}\n\n\tfor _, p := range pr.InactivePluginList() {\n\t\trow := []string{p.Name, p.Regex, \"inactive\"}\n\t\trows = append(rows, row)\n\t}\n\n\tevt.ReplyTable(hdr, rows)\n}\n\nfunc listInstances(evt hal.Evt, roomId string) {\n\thdr := []string{\"Plugin Name\", \"Broker\", \"Room\", \"RE\"}\n\trows := [][]string{}\n\tpr := hal.PluginRegistry()\n\n\tif roomId == \"*\" {\n\t\troomId = evt.RoomId\n\t}\n\n\tfor _, inst := range pr.InstanceList() {\n\t\tif roomId != \"\" && inst.RoomId != roomId {\n\t\t\tcontinue\n\t\t}\n\n\t\trow := []string{\n\t\t\tinst.Plugin.Name,\n\t\t\tinst.Broker.Name(),\n\t\t\tinst.RoomId,\n\t\t\tinst.Regex,\n\t\t}\n\t\trows = append(rows, row)\n\t}\n\n\tevt.ReplyTable(hdr, rows)\n}\n\nfunc savePlugins(evt hal.Evt) {\n\tpr := hal.PluginRegistry()\n\n\terr := pr.SaveInstances()\n\tif err != nil {\n\t\tevt.Replyf(\"Error while saving plugin config: %s\", err)\n\t} else {\n\t\tevt.Reply(\"Plugin configuration saved.\")\n\t}\n}\n\nfunc attachPlugin(evt hal.Evt, pluginName, roomId, regex string) {\n\tpr := hal.PluginRegistry()\n\tplugin, err := pr.GetPlugin(pluginName)\n\tif err != nil {\n\t\tevt.Replyf(\"No such plugin: '%s'\", plugin)\n\t\treturn\n\t}\n\n\tinst := plugin.Instance(roomId, evt.Broker)\n\tinst.RoomId = roomId\n\tinst.Regex = regex\n\terr = inst.Register()\n\tif err != nil {\n\t\tevt.Replyf(\"Failed to launch plugin '%s' in room id '%s': %s\", plugin, roomId, err)\n\n\t} else {\n\t\tevt.Replyf(\"Launched an instance of plugin: '%s' in room id '%s'\", plugin, roomId)\n\t}\n}\n\nfunc detachPlugin(evt hal.Evt, plugin, roomId string) {\n\tpr := hal.PluginRegistry()\n\tinstances := pr.FindInstances(roomId, evt.BrokerName(), plugin)\n\n\t// there should be only one, for now just log if that is not the case\n\tif len(instances) > 1 {\n\t\tlog.Printf(\"FindInstances(%q, %q) returned %d instances. Expected 0 or 1.\",\n\t\t\troomId, plugin, len(instances))\n\t} else if len(instances) == 0 {\n\t\tevt.Replyf(\"No plugin named %q is attached to room %q.\", plugin, roomId)\n\t}\n\n\tfor _, inst := range instances {\n\t\tinst.Unregister()\n\t\tevt.Replyf(\"%q/%q unregistered\", roomId, plugin)\n\t}\n}\n\nfunc GetPluginGroup(group string) (PluginGroup, error) {\n\tout := make(PluginGroup, 0)\n\tsql := `SELECT group_name, plugin_name FROM plugin_groups`\n\tparams := []interface{}{}\n\n\tif group != \"\" {\n\t\tsql = sql + \" WHERE group_name=?\"\n\t\tparams = []interface{}{&group}\n\t}\n\n\tdb := hal.SqlDB()\n\trows, err := db.Query(sql, params...)\n\tif err != nil {\n\t\treturn out, err\n\t}\n\tdefer rows.Close()\n\n\tfor rows.Next() {\n\t\tpgr := PluginGroupRow{}\n\n\t\t// TODO: add timestamps back after making some helpers for time conversion\n\t\t// (code that was here didn't handle NULL)\n\t\terr := rows.Scan(&pgr.Group, &pgr.Plugin)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"PluginGroup row iteration failed: %s\\n\", err)\n\t\t\tbreak\n\t\t}\n\n\t\tout = append(out, &pgr)\n\t}\n\n\treturn out, nil\n}\n\nfunc (pgr *PluginGroupRow) Save() error {\n\tsql := `INSERT INTO plugin_groups\n\t        (group_name, plugin_name, ts) VALUES (?, ?, ?)`\n\n\tdb := hal.SqlDB()\n\t_, err := db.Exec(sql, &pgr.Group, &pgr.Plugin, &pgr.Timestamp)\n\treturn err\n}\n\nfunc (pgr *PluginGroupRow) Delete() error {\n\tsql := `DELETE FROM plugin_groups WHERE group_name=? AND plugin_name=?`\n\n\tdb := hal.SqlDB()\n\t_, err := db.Exec(sql, &pgr.Group, &pgr.Plugin)\n\treturn err\n}\n\nfunc listGroupPlugin(evt hal.Evt, group string) {\n\tpgs, err := GetPluginGroup(\"\")\n\tif err != nil {\n\t\tevt.Replyf(\"Could not fetch plugin group list: %s\", err)\n\t\treturn\n\t}\n\n\ttbl := make([][]string, len(pgs))\n\tfor i, pgr := range pgs {\n\t\ttbl[i] = []string{pgr.Group, pgr.Plugin}\n\t}\n\n\tevt.ReplyTable([]string{\"Group Name\", \"Plugin Name\"}, tbl)\n}\n\nfunc addGroupPlugin(evt hal.Evt, group, pluginName string) {\n\tpr := hal.PluginRegistry()\n\t// make sure the plugin name is valid\n\tplugin, err := pr.GetPlugin(pluginName)\n\tif err != nil {\n\t\tevt.Error(err)\n\t\treturn\n\t}\n\n\t// no checking for group other than \"can it be inserted as a string\"\n\tpgr := PluginGroupRow{\n\t\tGroup:     group,\n\t\tPlugin:    plugin.Name,\n\t\tTimestamp: time.Now(),\n\t}\n\n\terr = pgr.Save()\n\tif err != nil {\n\t\tevt.Replyf(\"failed to add %q to group %q: %s\", pgr.Plugin, pgr.Group, err)\n\t} else {\n\t\tevt.Replyf(\"added %q to group %q\", pgr.Plugin, pgr.Group)\n\t}\n}\n\nfunc delGroupPlugin(evt hal.Evt, group, plugin string) {\n\tpgr := PluginGroupRow{Group: group, Plugin: plugin}\n\terr := pgr.Delete()\n\tif err != nil {\n\t\tevt.Replyf(\"failed to delete %q from group %q: %s\", pgr.Plugin, pgr.Group, err)\n\t} else {\n\t\tevt.Replyf(\"deleted %q from group %q\", pgr.Plugin, pgr.Group)\n\t}\n}\n"
  },
  {
    "path": "plugins/prefmgr/http.go",
    "content": "package prefmgr\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nfunc prefHandler(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Content-Type\", \"application/json\")\n\n\tswitch r.Method {\n\tcase http.MethodGet:\n\t\tgetPrefHandler(w, r)\n\tcase http.MethodPut:\n\t\tputPrefHandler(w, r)\n\tcase http.MethodPatch:\n\t\tpatchPrefHandler(w, r)\n\tcase http.MethodDelete:\n\t\tdeletePrefHandler(w, r)\n\t}\n}\n\n// getPrefHandler returns all prefs as a JSON document.\n// There are currently no parameters for server-side filtering, that will\n// be done client-side.\nfunc getPrefHandler(w http.ResponseWriter, r *http.Request) {\n\tprefs := hal.FindPrefs(\"\", \"\", \"\", \"\", \"\")\n\n\tbytes, err := json.Marshal(&prefs)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error while encoding prefs as JSON: %s\", err)\n\t}\n\n\t_, err = w.Write(bytes)\n\tif err != nil {\n\t\tlog.Fatalf(\"Error while sending JSON response: %s\", err)\n\t}\n}\n\nfunc putPrefHandler(w http.ResponseWriter, r *http.Request) {\n}\n\nfunc patchPrefHandler(w http.ResponseWriter, r *http.Request) {\n}\n\nfunc deletePrefHandler(w http.ResponseWriter, r *http.Request) {\n}\n"
  },
  {
    "path": "plugins/prefmgr/plugin.go",
    "content": "// prefmgr exposes hal's preferences as a bot command and over REST\npackage prefmgr\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nconst NAME = \"prefmgr\"\n\nconst HELP = `Listing keys with no filter will list all keys visible to the active user and room.\n\n!prefs list --key KEY\n!prefs list --user USER --room CHANNEL --plugin PLUGIN --key KEY --def DEFAULT\n`\n\nvar cli *hal.Cmd\nvar slackLinkRE *regexp.Regexp\nvar log hal.Logger\n\nfunc init() {\n\tlog.SetPrefix(\"plugins/prefmgr\")\n}\n\nfunc init() {\n\tcli = hal.NewCmd(\"!pref\", true).SetUsage(\"Manage hal preferences over chat.\")\n\n\tkeyUsage := \"the key name, up to 190 utf8 characters\"\n\tvalueUsage := \"the value, arbitrary utf8\"\n\troomUsage := \"the chat room id (usually auto-resolved, '*' for 'this room')\"\n\tuserUsage := \"the user id (usually auto-resolved, '*' for 'executing user')\"\n\tbrokerUsage := \"the broker name. e.g. 'slack' ('*' for 'this broker')\"\n\tpluginUsage := \"the plugin name. e.g. 'archive' ('*' for 'this plugin')\"\n\n\tcli.AddSubCmd(\"set\").\n\t\tSetUsage(\"set a preference key/value\").\n\t\tSubCmd().AddKVParam(\"key\", true).AddAlias(\"k\").SetUsage(keyUsage).\n\t\tSubCmd().AddKVParam(\"value\", true).AddAlias(\"v\").SetUsage(valueUsage).\n\t\tSubCmd().AddKVParam(\"room\", false).AddAlias(\"r\").SetUsage(roomUsage).\n\t\tSubCmd().AddKVParam(\"user\", false).AddAlias(\"u\").SetUsage(userUsage).\n\t\tSubCmd().AddKVParam(\"broker\", false).AddAlias(\"b\").SetUsage(brokerUsage).\n\t\tSubCmd().AddKVParam(\"plugin\", false).AddAlias(\"p\").SetUsage(pluginUsage)\n\n\tcli.AddSubCmd(\"list\").\n\t\tAddAlias(\"get\").\n\t\tSetUsage(\"retreive preferences, optionally filtered by the provided attributes\").\n\t\tSubCmd().\n\t\tAddKVParam(\"key\", false).AddAlias(\"k\").SetUsage(keyUsage).\n\t\tSubCmd().AddKVParam(\"value\", false).AddAlias(\"v\").SetUsage(valueUsage).\n\t\tSubCmd().AddKVParam(\"room\", false).AddAlias(\"r\").SetUsage(roomUsage).\n\t\tSubCmd().AddKVParam(\"user\", false).AddAlias(\"u\").SetUsage(userUsage).\n\t\tSubCmd().AddKVParam(\"broker\", false).AddAlias(\"b\").SetUsage(brokerUsage).\n\t\tSubCmd().AddKVParam(\"plugin\", false).AddAlias(\"p\").SetUsage(pluginUsage)\n\n\tcli.AddSubCmd(\"find\").\n\t\tSetUsage(\"retreive preferences following precedence rules\").\n\t\tSubCmd().AddKVParam(\"key\", false).AddAlias(\"k\").SetUsage(keyUsage).\n\t\tSubCmd().AddKVParam(\"value\", false).AddAlias(\"v\").SetUsage(valueUsage).\n\t\tSubCmd().AddKVParam(\"room\", false).AddAlias(\"r\").SetUsage(roomUsage).\n\t\tSubCmd().AddKVParam(\"user\", false).AddAlias(\"u\").SetUsage(userUsage).\n\t\tSubCmd().AddKVParam(\"broker\", false).AddAlias(\"b\").SetUsage(brokerUsage).\n\t\tSubCmd().AddKVParam(\"plugin\", false).AddAlias(\"p\").SetUsage(pluginUsage)\n\n\tcli.AddSubCmd(\"rm\").\n\t\tSetUsage(\"delete a preference by id\").\n\t\tAddIdxParam(0, \"id\", true).\n\t\tSetUsage(\"the preference id to delete\")\n\n\tslackLinkRE = regexp.MustCompile(\"^<(?:http|mailto):.*|.*>$\")\n}\n\nfunc Register() {\n\tplugin := hal.Plugin{\n\t\tName:    NAME,\n\t\tFunc:    prefmgr,\n\t\tCommand: \"pref\",\n\t}\n\tplugin.Register()\n\n\thttp.HandleFunc(\"/api/pref\", prefHandler)\n}\n\n// prefmgr is called when someone executes !pref in the chat system\nfunc prefmgr(evt hal.Evt) {\n\treq, err := cli.Process(evt.BodyAsArgv())\n\tif err != nil {\n\t\t// eww...\n\t\tswitch err.(type) {\n\t\tcase hal.SubCmdNotFound:\n\t\t\tevt.Reply(cli.Usage())\n\t\tdefault:\n\t\t\tevt.Reply(err.Error())\n\t\t}\n\t\treturn\n\t}\n\n\tswitch req.SubCmdToken() {\n\tcase \"\", \"help\":\n\t\tevt.Reply(cli.Usage())\n\tcase \"set\":\n\t\tcliSet(req.SubCmdInst(), &evt)\n\tcase \"list\":\n\t\tcliList(req.SubCmdInst(), &evt)\n\tcase \"find\":\n\t\tcliFind(req.SubCmdInst(), &evt)\n\tcase \"rm\":\n\t\tcliRm(req.SubCmdInst(), &evt)\n\tdefault:\n\t\tevt.Reply(req.Usage())\n\t}\n}\n\n// cmd2pref copies data from the hal.Cmd and hal.Evt into a hal.Pref, resolving\n// *'s on the way.\nfunc cmd2pref(req *hal.SubCmdInst, evt *hal.Evt) (*hal.Pref, error) {\n\tvar out hal.Pref\n\n\tfor _, pi := range req.ListKVParamInsts() {\n\t\tvar err error\n\t\tvar key, value string\n\n\t\tswitch pi.Key() {\n\t\tcase \"key\":\n\t\t\tkey, err = pi.String()\n\t\t\tout.Key = stripAutoLinks(key)\n\t\tcase \"value\":\n\t\t\tvalue, err = pi.String()\n\t\t\tout.Value = stripAutoLinks(value)\n\t\tcase \"room\":\n\t\t\tout.Room = pi.DefString(evt.RoomId)\n\t\tcase \"user\":\n\t\t\tout.User = pi.DefString(evt.UserId)\n\t\tcase \"broker\":\n\t\t\tout.Broker = pi.DefString(evt.BrokerName())\n\t\tcase \"plugin\":\n\t\t\tout.Plugin, _ = pi.String()\n\t\t}\n\n\t\t// return on the first error\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &out, nil\n}\n\n// cliList implements !pref list\nfunc cliList(req *hal.SubCmdInst, evt *hal.Evt) {\n\topts := hal.Pref{}\n\tprefs := opts.Find()\n\n\tfor _, pi := range req.ListKVParamInsts() {\n\t\tvar err error\n\t\tvar key, value string\n\n\t\tswitch pi.Key() {\n\t\tcase \"key\":\n\t\t\tkey, err = pi.String()\n\t\t\tprefs = prefs.Key(stripAutoLinks(key))\n\t\tcase \"value\":\n\t\t\tvalue, err = pi.String()\n\t\t\tprefs = prefs.Value(stripAutoLinks(value))\n\t\tcase \"room\":\n\t\t\tprefs = prefs.Room(pi.DefString(evt.RoomId))\n\t\tcase \"user\":\n\t\t\tprefs = prefs.User(pi.DefString(evt.UserId))\n\t\tcase \"broker\":\n\t\t\tprefs = prefs.Broker(pi.DefString(evt.BrokerName()))\n\t\tcase \"plugin\":\n\t\t\tprefs = prefs.Plugin(pi.DefString(NAME))\n\t\t}\n\n\t\tif err != nil {\n\t\t\tevt.Error(err)\n\t\t\treturn\n\t\t}\n\t}\n\n\tdata := prefs.Table()\n\tevt.ReplyTable(data[0], data[1:])\n}\n\n// cliFind implements !pref find\nfunc cliFind(req *hal.SubCmdInst, evt *hal.Evt) {\n\topts, err := cmd2pref(req, evt)\n\tif err != nil {\n\t\tpanic(err) // TODO: placeholder\n\t}\n\n\tprefs := opts.Find()\n\tdata := prefs.Table()\n\tevt.ReplyTable(data[0], data[1:])\n}\n\n// cliSet implements !pref set\nfunc cliSet(req *hal.SubCmdInst, evt *hal.Evt) {\n\topts, err := cmd2pref(req, evt)\n\tif err != nil {\n\t\tpanic(err) // TODO: placeholder\n\t}\n\n\tif opts.Room != \"\" && !evt.Broker.LooksLikeRoomId(opts.Room) {\n\t\topts.Room = evt.Broker.RoomNameToId(opts.Room)\n\t}\n\n\tif opts.User != \"\" && !evt.Broker.LooksLikeUserId(opts.User) {\n\t\topts.User = evt.Broker.UserNameToId(opts.User)\n\t}\n\n\t// TODO: check plugin name validity\n\t// TODO: check broker name validity\n\n\tfmt.Printf(\"Setting pref: %q\\n\", opts.String())\n\terr = opts.Set()\n\tif err != nil {\n\t\tevt.Replyf(\"Failed to set pref: %q\", err)\n\t} else {\n\t\tdata := opts.GetPrefs().Table()\n\t\tevt.ReplyTable(data[0], data[1:])\n\t}\n}\n\n// cliRm implements !pref rm <id>\nfunc cliRm(req *hal.SubCmdInst, evt *hal.Evt) {\n\tid, err := req.GetIdxParamInst(0).Int()\n\tif err != nil {\n\t\tpanic(err) // TODO: placeholder\n\t}\n\n\terr = hal.RmPrefId(id)\n\tif err != nil {\n\t\tevt.Replyf(\"Failed to delete pref with id %d: %s\", id, err)\n\t} else {\n\t\tevt.Replyf(\"Deleted pref id %d.\", id)\n\t}\n}\n\nfunc stripAutoLinks(in string) string {\n\tif slackLinkRE.MatchString(in) {\n\t\tparts := strings.Split(strings.TrimSuffix(in, \">\"), \"|\")\n\t\tif len(parts) == 2 {\n\t\t\treturn parts[1]\n\t\t}\n\t}\n\n\treturn in\n}\n"
  },
  {
    "path": "plugins/roster/plugin.go",
    "content": "package roster\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\ntype RosterUser struct {\n\tBroker    string    `json: broker` // broker name e.g. slack, hipchat\n\tUser      string    `json: user`\n\tRoom      string    `json: room`\n\tTimestamp time.Time `json: timestamp`\n}\n\nconst ROSTER_TABLE = `\nCREATE TABLE IF NOT EXISTS roster (\n\tbroker VARCHAR(191) NOT NULL,\n\tuser   VARCHAR(191) NOT NULL,\n\troom   VARCHAR(191) DEFAULT NULL,\n\tts     TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n\tPRIMARY KEY (broker, user, room)\n)`\n\nfunc Register() {\n\t// rostertracker gets all messages and keeps a database of when users\n\t// were last seen to support !last, and the web roster.\n\troster := hal.Plugin{\n\t\tName: \"roster_tracker\",\n\t\tFunc: rostertracker,\n\t}\n\troster.Register()\n\n\trostercmd := hal.Plugin{\n\t\tName:  \"roster_command\",\n\t\tFunc:  rosterlast,\n\t\tRegex: \"!last\",\n\t}\n\trostercmd.Register()\n\n\thal.SqlInit(ROSTER_TABLE)\n\n\thttp.HandleFunc(\"/v1/roster\", webroster)\n}\n\n// rostertracker is called for every message. It grabs the user and current\n// time and throws it into the db for later use.\nfunc rostertracker(msg hal.Evt) {\n\tdb := hal.SqlDB()\n\n\tsql := `INSERT INTO roster\n\t          (broker, user, room, ts)\n\t        VALUES (?,?,?,?)\n\t        ON DUPLICATE KEY\n\t        UPDATE broker=?, user=?, room=?, ts=?`\n\n\tparams := []interface{}{\n\t\tmsg.BrokerName(), msg.User, msg.Room, msg.Time,\n\t\tmsg.BrokerName(), msg.User, msg.Room, msg.Time,\n\t}\n\n\t_, err := db.Exec(sql, params...)\n\tif err != nil {\n\t\tlog.Printf(\"roster_tracker write failed: %s\", err)\n\t}\n}\n\n// rosterlast is the response to !last that causes the bot to reply via DM\n// to the user with a table of when users last posted a message to slack\n// rather than relying on status, which is usually useless.\nfunc rosterlast(msg hal.Evt) {\n\trus, err := GetRoster()\n\tif err != nil {\n\t\tlog.Printf(\"Error while retreiving roster: %s\\n\", err)\n\t\treturn\n\t}\n\n\t// TODO: ASCII art instead of JSON\n\tjs, err := json.MarshalIndent(rus, \"\", \"    \")\n\tif err != nil {\n\t\tlog.Printf(\"JSON marshaling failed: %s\\n\", err)\n\t\treturn\n\t}\n\n\tmsg.Replyf(\"```%s```\", string(js))\n}\n\nfunc webroster(w http.ResponseWriter, r *http.Request) {\n\trus, err := GetRoster()\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"could not fetch roster: '%s'\", err), 500)\n\t\treturn\n\t}\n\n\tjs, err := json.Marshal(rus)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"could not marshal roster to json: '%s'\", err), 500)\n\t\treturn\n\t}\n\n\tw.Write(js)\n}\n\nfunc GetRoster() ([]*RosterUser, error) {\n\tdb := hal.SqlDB()\n\n\tsql := `SELECT broker, user, room,\n\t               UNIX_TIMESTAMP(ts) AS ts\n\t               FROM roster\n\t               ORDER BY ts DESC`\n\n\trows, err := db.Query(sql)\n\tif err != nil {\n\t\tlog.Printf(\"Roster query failed: %s\\n\", err)\n\t\treturn nil, err\n\t}\n\tdefer rows.Close()\n\n\trus := []*RosterUser{}\n\n\tfor rows.Next() {\n\t\tru := RosterUser{}\n\n\t\tvar ts int64\n\t\terr = rows.Scan(&ru.Broker, &ru.User, &ru.Room, &ts)\n\t\tif err != nil {\n\t\t\tlog.Printf(\"Row iteration failed: %s\\n\", err)\n\t\t\treturn nil, err\n\t\t}\n\n\t\tru.Timestamp = time.Unix(ts, 0)\n\n\t\trus = append(rus, &ru)\n\t}\n\n\treturn rus, nil\n}\n"
  },
  {
    "path": "plugins/seppuku/plugin.go",
    "content": "package seppuku\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\nfunc Register() {\n\tp := hal.Plugin{\n\t\tName:  \"seppuku\",\n\t\tFunc:  seppuku,\n\t\tRegex: \"^[[:space:]]*!(seppuku|切腹)\",\n\t}\n\tp.Register()\n\n\tz := hal.Plugin{\n\t\tName:  \"zombie\",\n\t\tFunc:  zombie,\n\t\tRegex: \"^[[:space:]]*!(zombie|ゾンビ)\",\n\t}\n\tz.Register()\n}\n\n// seppuku instructs the bot to die.\n// you probably don't want this on in production - if you do, a supervisor\n// is highly recommended\nfunc seppuku(evt hal.Evt) {\n\tevt.Reply(\"さようなら\")\n\ttime.Sleep(2 * time.Second)\n\tlog.Printf(\"exiting due to %q command from %s in %s/%s\", evt.Body, evt.User, evt.BrokerName(), evt.Room)\n\tos.Exit(1337)\n}\n\n// zombie disables all plugins but seppuku and stays running.\n// useful for putting a bot deployed under a supervisor out of comission\n// so a local copy can be tested without interference - put the bot into zombie\n// mode then when you're ready for it to die, instruct it to seppuku\nfunc zombie(evt hal.Evt) {\n\tpr := hal.PluginRegistry()\n\n\tfor _, inst := range pr.InstanceList() {\n\t\tif inst.Plugin.Name == \"zombie\" {\n\t\t\t// this makes the hal router think zombie has executed for every\n\t\t\t// incoming event so it doesn't fall through and say \"invalid command\"\n\t\t\tinst.Regex = \"\"\n\t\t\tinst.Func = func(evt hal.Evt) { return }\n\t\t} else if inst.Plugin.Name != \"seppuku\" {\n\t\t\tinst.Unregister()\n\t\t}\n\t}\n\n\tevt.Reply(\"まったネクストライフ\")\n}\n"
  },
  {
    "path": "plugins/spam/plugin.go",
    "content": "package spam\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar log hal.Logger\n\nfunc Register() {\n\tp := hal.Plugin{\n\t\tName: \"spam\",\n\t\tFunc: spam,\n\t}\n\tp.Register()\n}\n\nfunc spam(evt hal.Evt) {\n\tresponse := evt.AsPref().SetUser(\"\").FindKey(\"spam-response\").One()\n\tif response.Success {\n\t\tevt.Reply(response.Value)\n\t} else {\n\t\tlog.Printf(\"spam is configured in room %q but could not find the 'spam-response' pref\", evt.RoomId)\n\t}\n}\n"
  },
  {
    "path": "plugins/uptime/plugin.go",
    "content": "// uptime: the simplest useful plugin possible\npackage uptime\n\n/*\n * Copyright 2016-2017 Netflix, Inc.\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n *     http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/netflix/hal-9001/hal\"\n)\n\nvar booted time.Time\n\nfunc init() {\n\tbooted = time.Now()\n}\n\nfunc Register() {\n\tp := hal.Plugin{\n\t\tName:    \"uptime\",\n\t\tFunc:    uptime,\n\t\tCommand: \"uptime\",\n\t}\n\tp.Register()\n}\n\nfunc uptime(evt hal.Evt) {\n\tut := time.Since(booted)\n\tevt.Reply(fmt.Sprintf(\"uptime: %s\", ut.String()))\n}\n"
  }
]