[
  {
    "path": ".dockerignore",
    "content": "Dockerfile\n.travis\n.dockerignore\ncoverage.txt\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where the package manifests are located.\n# Please see the documentation for all configuration options:\n# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file\n\nversion: 2\nupdates:\n  - package-ecosystem: \"gomod\"\n    directory: \"/\" # Location of package manifests\n    schedule:\n      interval: \"weekly\"\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Docker push\n\non:\n  push:\n    branches:\n      - master\n\n\njobs:\n  docker_publish:\n    runs-on: 1.25\n    env:\n      DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }}\n      DOCKER_PASS: ${{ secrets.DOCKERHUB_PASSWORD }}\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          fetch-depth: 2\n      - name: Login to Docker hub\n        run: docker login -u $DOCKER_USER -p $DOCKER_PASS\n      - name: Build image\n        run: |\n          pwd\n          ls -l\n          docker build -t ${{ github.repository }} -f Dockerfile .\n      - name: Publish image\n        run: |\n          docker tag ${{ github.repository }} ${{ github.repository }}:latest\n          docker push ${{ github.repository }}\n"
  },
  {
    "path": ".github/workflows/tests.yml",
    "content": "name: Tests\n\non: [push, pull_request]\n\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go: ['1.25']\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          fetch-depth: 2\n          # clone in the gopath\n          path: src/github.com/${{ github.repository }}\n      - uses: actions/setup-go@v2\n        with:\n          stable: false\n          go-version: ${{ matrix.go }}\n      - run: |\n          echo \"GOPATH=$GITHUB_WORKSPACE\" >> $GITHUB_ENV\n      - run: |\n          cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }}/cmd/irc-slack\n          make\n          ./irc-slack --version\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go: ['1.25']\n    steps:\n      - uses: actions/checkout@v2\n        with:\n          fetch-depth: 2\n          # clone in the gopath\n          path: src/github.com/${{ github.repository }}\n      - uses: actions/setup-go@v2\n        with:\n          stable: false\n          go-version: ${{ matrix.go }}\n      - run: |\n          echo \"GOPATH=$GITHUB_WORKSPACE\" >> $GITHUB_ENV\n      - run: |\n          cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }}\n          go get -v -t ./...\n          echo \"\" > coverage.txt\n          for d in $(go list ./...); do\n              go test -v -race -coverprofile=profile.out -covermode=atomic \"${d}\"\n              if [ -f profile.out ]; then\n                cat profile.out >> coverage.txt\n                rm profile.out\n              fi\n          done\n          bash <(curl -s https://codecov.io/bash) -c -f coverage.txt -F unittest\n"
  },
  {
    "path": ".gitignore",
    "content": "coverage.txt\ncmd/irc-slack/irc-slack\n"
  },
  {
    "path": ".stickler.yml",
    "content": "linters:\n  golint:\n    fixer: true\nfixers:\n  enable: true\n"
  },
  {
    "path": "Dockerfile",
    "content": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.23-alpine AS builder\n\nLABEL BUILD=\"docker build -t insomniacslk/irc-slack -f Dockerfile .\"\nLABEL RUN=\"docker run --rm -p 6666:6666 -it insomniacslk/irc-slack\"\n\n# Install git.\n# Git is required for fetching the dependencies.\nRUN apk update && apk add --no-cache git bash make\nCOPY . $GOPATH/src/github.com/insomniacslk/irc-slack\nENV GO111MODULE=on\nWORKDIR $GOPATH/src/github.com/insomniacslk/irc-slack/cmd/irc-slack\n# Build the binary.\nRUN make\nRUN cp irc-slack /go/bin\n\n############################\n# STEP 2 build a small image\n############################\nFROM scratch\n# Copy the ssl certs so we can talk to slack\nCOPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt\n# Copy our static executable.\nCOPY --from=builder /go/bin/irc-slack /go/bin/irc-slack\nENV PATH=\"/go/bin:$PATH\"\n# Run the irc-slack binary.\nCMD [\"/go/bin/irc-slack\", \"-H\", \"0.0.0.0\"]\n"
  },
  {
    "path": "Dockerfile.autotoken",
    "content": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.16-alpine AS builder\n\nLABEL BUILD=\"docker build -t insomniacslk/irc-slack/tools-autotoken -f Dockerfile.autotoken .\"\nLABEL RUN=\"docker run --rm -it insomniacslk/irc-slack/tools-autotoken\"\n\n# Install git.\n# Git is required for fetching the dependencies.\nRUN apk update && apk add --no-cache --purge git bash chromium\nCOPY . $GOPATH/src/github.com/insomniacslk/irc-slack/\nENV GO111MODULE=on\nWORKDIR $GOPATH/src/github.com/insomniacslk/irc-slack/tools/autotoken\n# Build the binary.\nRUN CGO_ENABLED=0 go build -ldflags=\"-w -s\" -o /go/bin/autotoken\nENV PATH=\"/go/bin:$PATH\"\nWORKDIR /tmp\nUSER guest\n# Run the autotoken binary.\nCMD [\"/go/bin/autotoken\", \"-h\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "BSD 3-Clause License\n\nCopyright (c) 2018, Andrea Barberio\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are met:\n\n* Redistributions of source code must retain the above copyright notice, this\n  list of conditions and the following disclaimer.\n\n* Redistributions in binary form must reproduce the above copyright notice,\n  this list of conditions and the following disclaimer in the documentation\n  and/or other materials provided with the distribution.\n\n* Neither the name of the copyright holder nor the names of its\n  contributors may be used to endorse or promote products derived from\n  this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\nDISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE\nFOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL\nDAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR\nSERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER\nCAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,\nOR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE\nOF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "README.md",
    "content": "# IRC-to-Slack gateway\n\n`irc-slack` is an IRC-to-Slack gateway. It is an IRC server that lets you\nconnect to your Slack teams with your IRC client.\n\n[![](images/team_chat_2x.png)](https://xkcd.com/1782/)\n\n(That guy is me)\n\nSlack has ended support for IRC and XMPP gateway on the 15th of May 2018. So\nwhat's left to do for people like me, who want to still be able to log in via\nIRC? Either you use [wee-slack](https://github.com/wee-slack/wee-slack) (~~but I\ndon't use WeeChat~~), or you implement your own stuff.\n\nNOTE: after Slack turned down their IRC gateway I got a lot of contacts from users of irc-slack asking me to fix and improve it. I didn't expect people to actually use it, but thanks to your feedback I'm now actively developing it again :-)\nPlease keep reporting bugs and sending PRs!\n\n## How to use it\n\n```\ncd cmd/irc-slack\nmake # use `make` instead of `go build` to include build information when running with `-v`\n./irc-slack # by default on port 6666\n```\n\nThen configure your IRC client to connect to localhost:6666 and use one of the methods in the Tokens section to set the connection password.\n\nYou can also [run it with Docker](#run-it-with-docker).\n\n## Feature matrix\n\n|     | public channel | private channel | multiparty IM | IM |\n| --- | --- | --- | --- | --- |\n| from me | works | works | doesn't work ([#168](https://github.com/insomniacslk/irc-slack/issues/168)) | works |\n| to me | works | works | works | works |\n| thread from me | doesn't work ([#168](https://github.com/insomniacslk/irc-slack/issues/168)) | doesn't work ([#168](https://github.com/insomniacslk/irc-slack/issues/168)) | untested | doesn't work ([#166](https://github.com/insomniacslk/irc-slack/issues/166)) |\n| thread to me | works | works | untested | works but sends in the IM chat ([#167](https://github.com/insomniacslk/irc-slack/issues/167)) |\n\n## Encryption\n\n`irc-slack` by default does not use encryption when communicating with your IRC\nclient (but the communication between `irc-slack` and the Slack servers is\nencrypted).\nIf you want to use TLS, you can use the `-key` and `-cert` command line\nparameters, and point them to a TLS certificate that you own.\nThis is useful if you plan to connect to to `irc-slack` over the internet.\n\nFor example, you can generate a valid certificate with LetsEncrypt (adjust the relevant\nfields of course):\n```\nsudo certbot certonly \\\n    -n \\\n    -d your.domain.example.com \\\n    --test-cert \\\n    --standalone \\\n    -m your@email.example.com \\\n    --agree-tos\n```\n\nThen your key and certificate will be generated under\n`/etc/letsencrypt/live/your.domain.example.com`\nwith the names `privkey.pem` and `cert.pem` respectively.\n\n## Authentication\n\nTo connect to Slack via `irc-slack` you need an authentication string. There are\nthree possible methods:\n* User tokens with auth cookies (recommended)\n* Slack app tokens (if you can install apps on your slack team)\n* legacy tokens (soon to be deprecated)\n\nThese options are discussed in more detail below.\nThen just add `-key <path/to/privkey.pem> -cert <path/to/cert.pem>` to enable\nTLS on `irc-slack`, and enable TLS on your IRC client.\n\n\n### User tokens with auth cookie\n\nThis approach does not require legacy tokens nor installing any app, but in order to\nget the token there are a few manual steps to execute.\n\nThis type of token starts with `xoxc-`, and requires an auth cookie to be paired\nto it in order to work.\n\nThere are two possible procedures, an entirely manual one, using the browser\nconsole, and a semi-automated one, which requires Chrome or Chromium in headless\nmode.\n\n**manual procedure via browser**\n\nThis is the same procedure as described in two similar projects, see:\n* https://github.com/adsr/irslackd/wiki/IRC-Client-Config#xoxc-tokens\n* https://github.com/ltworf/localslackirc/#obtain-a-token\n\nBut in short, log via browser on the Slack team, open the browser's network tab\nin the developer tools, and look for an XHR transaction. Then look for\n* the token (it starts with `xoxc-`) in the request data\n* the auth cookie, contained in the `d` key-value in the request cookies (it looks like `d=XXXX;`)\n\nThen concatenate the token and the auth cookie using a `|` character, like this:\n```\nxoxc-XXXX|d=XXXX;\n```\n\nand use the above as your IRC password.\n\n**semi-automated procedure using Chrome/Chromium in headless mode**\n\nSee [autotoken](tools/autotoken). Just build it with `go build` and run with\n`./autotoken -h` to see the usage help.\n\nIf you prefer to run `autotoken` via Docker, you can test your luck with:\n```\ndocker build -t insomniacslk/irc-slack/tools-autotoken -f Dockerfile.autotoken .\ndocker run --rm -it -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix insomniacslk/irc-slack/tools-autotoken autotoken -h\n```\n\n### Slack App tokens\n\nAs an alternative, you can install the irc-slack app on your workspace, and use the token that it returns after you authorize it.\n\nIn order to run the application, you need to do the following steps:\n* create a Slack app using their v1 OauthV2 API (note: not their v2 version) at https://api.slack.com/apps\n* configure the redirect URL to your endpoint (in this case\n  https://my-server/irc-slack/auth/)\n* run the web app under [slackapp](tools/slackapp/) passing your app client ID and client secret, you can find them in the Basic Information tab at the link at the previous step\n\nThe token starts with `xoxp-`, and you can use it as your IRC password when\nconnecting to `irc-slack`.\n\nThis is a Slack app with full user permissions, that is used to generate a Slack user token.\nNote that you need to install this app on every workspace you want to use it\nfor, and the workspace owners may reject it.\n\nThis app exchanges your temporary authentication code with a permanent token.\n\n### Legacy tokens\n\nThis is the easiest method, but it's deprecated and Slack will soon disable it.\nSlack has announced that they will stop issuing legacy tokens starting the 4th\nof May 2020, so this section will stay here for historical reasons.\n\nGet you Slack legacy token at https://api.slack.com/custom-integrations/legacy-tokens ,\nand set it as your IRC password when connecting to `irc-slack`.\n\n\n## Run it with Docker\n\nThanks to [halkeye](https://github.com/halkeye) you can run `irc-slack` via\nDocker. The `Dockerfile` is published on\nhttps://hub.docker.com/r/insomniacslk/irc-slack and will by default listen on\n`0.0.0.0:6666`. You can pull and run it with:\n\n```\ndocker run --rm -p 6666:6666 insomniacslk/irc-slack\n```\n\nIf you want to build it locally, just run:\n```\ndocker build -f Dockerfile . -t insomniacslk/irc-slack\n```\n\n\n### Connecting with irssi\n```\n/network add yourteam.slack.com\n/server add -auto -network yourteam.slack.com localhost 6666 xoxp-<your-slack-token>\n/connect yourteam.slack.com\n```\n\nRemember to add `-tls` to the `/connect` command if you're running `irc-slack`\nwith TLS.\nAlso remember to replace `localhost` with the name of the host you're connecting to,\nif different.\n\n### Connecting with WeeChat\n\n```\n/server add yourteam.slack.com localhost/6666\n/set irc.server.yourteam.slack.com.password xoxp-<your-slack-token>\n/connect yourteam.slack.com\n```\n\nTo enable TLS, also run the following before the `/connect` command:\n```\n/set irc.server.yourteam.slack.com.ssl on\n/set irc.server.yourteam.slack.com.ssl_verify on\n```\n\nAlso remember to replace `localhost` with the name of the host you're connecting to,\nif different.\n\n## Gateway usage\n\nThere are a few options that you can pass to the server, e.g. to change the listener port, or the server name:\n\n```\n$ ./irc-slack -h\nUsage of ./irc-slack:\n  -c, --cert string         TLS certificate for HTTPS server. Requires -key\n  -C, --chunk int           Maximum size of a line to send to the client. Only works for certain reply types (default 512)\n  -D, --debug               Enable debug logging of the Slack API\n  -d, --download string     If set will download attachments to this location\n  -l, --fileprefix string   If set will overwrite urls to attachments with this prefix and local file name inside the path set with -d\n  -H, --host string         IP address to listen on (default \"127.0.0.1\")\n  -k, --key string          TLS key for HTTPS server. Requires -cert\n  -L, --loglevel string     Log level. One of [none debug info warning error fatal] (default \"info\")\n  -P, --pagination int      Pagination value for API calls. If 0 or unspecified, use the recommended default (currently 200). Larger values can help on large Slack teams\n  -p, --port int            Local port to listen on (default 6666)\n  -s, --server string       IRC server name (i.e. the host name to send to clients)\npflag: help requested\nexit status 2\n```\n\n## Deploying with Puppet\n\nYou can use the [irc-slack module for Puppet](https://github.com/b4ldr/puppet-irc_slack) by [John Bond](https://github.com/b4ldr).\n\n## TODO\n\nA lot of things. Want to help? Grep \"TODO\", \"FIXME\" and \"XXX\" in the code and send me a PR :)\n\nThis currently \"works for me\", but I published it in the hope that someone would use it so we can find and fix bugs.\n\n## BUGS\n\nPlenty of them. I wrote this project while on a plane (like many other projects of mine) so this is hack-level quality - no proper design, no RFC compliance, no testing. I just fired up an IRC client until I could reasonably chat on a few Slack teams. Please report all the bugs you find on the Github issue tracker, or privately to me.\n\n## Authors\n\n* [Andrea Barberio](https://insomniac.slackware.it)\n* [Josip Janzic](https://github.com/janza)\n\n## Thanks\n\nSpecial thanks to\n* Stefan Stasik for helping me find, fix and troubleshoot a zillion of bugs :)\n* [Mauro Codella](https://github.com/codella) for patiently reading and replying for two hours in a private conversation that I used to test the fix at [pull/23](https://github.com/insomniacslk/irc-slack/pull/23) :D\n"
  },
  {
    "path": "cmd/irc-slack/Makefile",
    "content": "CMD=irc-slack\n\nREVISION := $(shell git rev-parse --short HEAD)\nBRANCH := $(shell git rev-parse --abbrev-ref HEAD)\n\nall: build\n\nbuild: $(wildcard *.go)\n\tCGO_ENABLED=0 go build -ldflags \"-X main.Version=git-$(REVISION)_$(BRANCH)\" -o $(CMD)\n"
  },
  {
    "path": "cmd/irc-slack/main.go",
    "content": "package main\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net\"\n\t\"os\"\n\n\t\"github.com/insomniacslk/irc-slack/pkg/ircslack\"\n\n\t\"github.com/coredhcp/coredhcp/logger\"\n\t\"github.com/sirupsen/logrus\"\n\tflag \"github.com/spf13/pflag\"\n)\n\n// Version information. Will be populated with the git revision and branch\n// information when running `make`.\nvar (\n\tProgramName        = \"irc-slack\"\n\tVersion     string = \"unknown (please build with `make`)\"\n)\n\n// To authenticate, the IRC client has to send a PASS command with a Slack\n// legacy token for the desired team. See README.md for details.\nvar (\n\tport                 = flag.IntP(\"port\", \"p\", 6666, \"Local port to listen on\")\n\thost                 = flag.StringP(\"host\", \"H\", \"127.0.0.1\", \"IP address to listen on\")\n\tserverName           = flag.StringP(\"server\", \"s\", \"\", \"IRC server name (i.e. the host name to send to clients)\")\n\tchunkSize            = flag.IntP(\"chunk\", \"C\", 512, \"Maximum size of a line to send to the client. Only works for certain reply types\")\n\tfileDownloadLocation = flag.StringP(\"download\", \"d\", \"\", \"If set will download attachments to this location\")\n\tfileProxyPrefix      = flag.StringP(\"fileprefix\", \"l\", \"\", \"If set will overwrite urls to attachments with this prefix and local file name inside the path set with -d\")\n\tlogLevel             = flag.StringP(\"loglevel\", \"L\", \"info\", fmt.Sprintf(\"Log level. One of %v\", getLogLevels()))\n\tflagSlackDebug       = flag.BoolP(\"debug\", \"D\", false, \"Enable debug logging of the Slack API\")\n\tflagPagination       = flag.IntP(\"pagination\", \"P\", 0, \"Pagination value for API calls. If 0 or unspecified, use the recommended default (currently 200). Larger values can help on large Slack teams\")\n\tflagKey              = flag.StringP(\"key\", \"k\", \"\", \"TLS key for HTTPS server. Requires -cert\")\n\tflagCert             = flag.StringP(\"cert\", \"c\", \"\", \"TLS certificate for HTTPS server. Requires -key\")\n\tflagVersion          = flag.BoolP(\"version\", \"v\", false, \"Print version and exit\")\n)\n\nvar log = logger.GetLogger(\"main\")\n\nvar logLevels = map[string]func(*logrus.Logger){\n\t\"none\":    func(l *logrus.Logger) { l.SetOutput(ioutil.Discard) },\n\t\"debug\":   func(l *logrus.Logger) { l.SetLevel(logrus.DebugLevel) },\n\t\"info\":    func(l *logrus.Logger) { l.SetLevel(logrus.InfoLevel) },\n\t\"warning\": func(l *logrus.Logger) { l.SetLevel(logrus.WarnLevel) },\n\t\"error\":   func(l *logrus.Logger) { l.SetLevel(logrus.ErrorLevel) },\n\t\"fatal\":   func(l *logrus.Logger) { l.SetLevel(logrus.FatalLevel) },\n}\n\nfunc getLogLevels() []string {\n\tvar levels []string\n\tfor k := range logLevels {\n\t\tlevels = append(levels, k)\n\t}\n\treturn levels\n}\n\nfunc main() {\n\tflag.CommandLine.SortFlags = false\n\tflag.Parse()\n\tif *flagVersion {\n\t\tfmt.Printf(\"%s version %s\\n\", ProgramName, Version)\n\t\tos.Exit(0)\n\t}\n\n\tfn, ok := logLevels[*logLevel]\n\tif !ok {\n\t\tlog.Fatalf(\"Invalid log level '%s'. Valid log levels are %v\", *logLevel, getLogLevels())\n\t}\n\tfn(log.Logger)\n\tlog.Infof(\"Setting log level to '%s'\", *logLevel)\n\tvar sName string\n\tif *serverName == \"\" {\n\t\tsName = \"localhost\"\n\t} else {\n\t\tsName = *serverName\n\t}\n\tlocalAddr := net.TCPAddr{Port: *port}\n\tip := net.ParseIP(*host)\n\tif ip == nil {\n\t\tlog.Fatalf(\"Invalid IP address to listen on: '%s'\", *host)\n\t}\n\tlocalAddr.IP = ip\n\tlog.Printf(\"Starting server on %v\", localAddr.String())\n\tif *fileDownloadLocation != \"\" {\n\t\tdInfo, err := os.Stat(*fileDownloadLocation)\n\t\tif err != nil || !dInfo.IsDir() {\n\t\t\tlog.Fatalf(\"Missing or invalid download directory: %s\", *fileDownloadLocation)\n\t\t}\n\t}\n\tdoTLS := false\n\tif *flagKey != \"\" && *flagCert != \"\" {\n\t\tdoTLS = true\n\t}\n\tvar tlsConfig *tls.Config\n\tif doTLS {\n\t\tif *flagKey == \"\" || *flagCert == \"\" {\n\t\t\tlog.Fatalf(\"-key and -cert must be specified together\")\n\t\t}\n\t\tcert, err := tls.LoadX509KeyPair(*flagCert, *flagKey)\n\t\tif err != nil {\n\t\t\tlog.Fatalf(\"Failed to load TLS key/cert: %v\", err)\n\t\t}\n\t\ttlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}\n\t}\n\tserver := ircslack.Server{\n\t\tLocalAddr:            &localAddr,\n\t\tName:                 sName,\n\t\tChunkSize:            *chunkSize,\n\t\tFileDownloadLocation: *fileDownloadLocation,\n\t\tFileProxyPrefix:      *fileProxyPrefix,\n\t\tSlackDebug:           *flagSlackDebug,\n\t\tPagination:           *flagPagination,\n\t\tTLSConfig:            tlsConfig,\n\t}\n\tif err := server.Start(); err != nil {\n\t\tlog.Fatal(err)\n\t}\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n  irc-slack:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - 6666:6666\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/insomniacslk/irc-slack\n\ngo 1.25\n\nrequire (\n\tgithub.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d\n\tgithub.com/chromedp/chromedp v0.14.2\n\tgithub.com/coredhcp/coredhcp v0.0.0-20250806070228-f7e98e4e350b\n\tgithub.com/sirupsen/logrus v1.9.4\n\tgithub.com/slack-go/slack v0.19.0\n\tgithub.com/spf13/pflag v1.0.10\n\tgithub.com/stretchr/testify v1.11.1\n)\n\nrequire (\n\tgithub.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect\n\tgithub.com/chromedp/sysutil v1.1.0 // indirect\n\tgithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect\n\tgithub.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // indirect\n\tgithub.com/gobwas/httphead v0.1.0 // indirect\n\tgithub.com/gobwas/pool v0.2.1 // indirect\n\tgithub.com/gobwas/ws v1.4.0 // indirect\n\tgithub.com/google/go-cmp v0.6.0 // indirect\n\tgithub.com/gorilla/websocket v1.5.3 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect\n\tgithub.com/onsi/ginkgo v1.16.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect\n\tgithub.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect\n\tgithub.com/rogpeppe/go-internal v1.10.0 // indirect\n\tgolang.org/x/crypto v0.45.0 // indirect\n\tgolang.org/x/sys v0.38.0 // indirect\n\tgolang.org/x/term v0.37.0 // indirect\n\tgopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg=\ngithub.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb/go.mod h1:xzXc1S/L+64uglB3pw54o8kqyM6KFYpTeC9Q6+qZIu8=\ngithub.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=\ngithub.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=\ngithub.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=\ngithub.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=\ngithub.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=\ngithub.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=\ngithub.com/coredhcp/coredhcp v0.0.0-20250806070228-f7e98e4e350b h1:v+UeSM6ffD0hXGpaggA86L7rzysxJSProX2YZKWufvA=\ngithub.com/coredhcp/coredhcp v0.0.0-20250806070228-f7e98e4e350b/go.mod h1:A2iJXPupXJVeJZFSrP1Vx/tK6cZ7oci0V/8egMjp2LI=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=\ngithub.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=\ngithub.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=\ngithub.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=\ngithub.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=\ngithub.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=\ngithub.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=\ngithub.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=\ngithub.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=\ngithub.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=\ngithub.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=\ngithub.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=\ngithub.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=\ngithub.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=\ngithub.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=\ngithub.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=\ngithub.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=\ngithub.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=\ngithub.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=\ngithub.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=\ngithub.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=\ngithub.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=\ngithub.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=\ngithub.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=\ngithub.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=\ngithub.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=\ngithub.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=\ngithub.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=\ngithub.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=\ngithub.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=\ngithub.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=\ngithub.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=\ngithub.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=\ngithub.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=\ngithub.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=\ngithub.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=\ngithub.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=\ngithub.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=\ngithub.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=\ngithub.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=\ngithub.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=\ngithub.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=\ngithub.com/slack-go/slack v0.19.0 h1:J8lL/nGTsIUX53HU8YxZeI3PDkA+sxZsFrI2Dew7h44=\ngithub.com/slack-go/slack v0.19.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE=\ngithub.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=\ngithub.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=\ngithub.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=\ngithub.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=\ngolang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=\ngolang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=\ngolang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=\ngolang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=\ngolang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=\ngolang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=\ngolang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=\ngolang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=\ngolang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=\ngolang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=\ngolang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngolang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=\ngoogle.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=\ngoogle.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=\ngoogle.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=\ngoogle.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=\ngoogle.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=\ngopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "pkg/ircslack/channel.go",
    "content": "package ircslack\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/slack-go/slack\"\n)\n\n// Constants for public, private, and multi-party conversation prefixes.\n// Channel threads are prefixed with \"+\" but they are not conversation types\n// so they do not belong here. A thread is just a message whose destination\n// is within another message in a public, private, or multi-party conversation.\nconst (\n\tChannelPrefixPublicChannel  = \"#\"\n\tChannelPrefixPrivateChannel = \"@\"\n\tChannelPrefixMpIM           = \"&\"\n\t// NOTE: a thread is not a channel type\n\tChannelPrefixThread = \"+\"\n)\n\n// HasChannelPrefix returns true if the channel name starts with one of the\n// supproted channel prefixes.\nfunc HasChannelPrefix(name string) bool {\n\tif len(name) == 0 {\n\t\treturn false\n\t}\n\tswitch string(name[0]) {\n\tcase ChannelPrefixPublicChannel, ChannelPrefixPrivateChannel, ChannelPrefixMpIM, ChannelPrefixThread:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\n// StripChannelPrefix returns a channel name without its channel prefix. If no\n// channel prefix is present, the string is returned unchanged.\nfunc StripChannelPrefix(name string) string {\n\tif HasChannelPrefix(name) {\n\t\treturn name[1:]\n\t}\n\treturn name\n}\n\n// ChannelMembers returns a list of users in the given conversation.\nfunc ChannelMembers(ctx *IrcContext, channelID string) ([]slack.User, error) {\n\tvar (\n\t\tmembers, m []string\n\t\tnextCursor string\n\t\terr        error\n\t\tpage       int\n\t)\n\tfor {\n\t\tattempt := 0\n\t\tfor {\n\t\t\t// retry if rate-limited, no more than MaxSlackAPIAttempts times\n\t\t\tif attempt >= MaxSlackAPIAttempts {\n\t\t\t\treturn nil, fmt.Errorf(\"ChannelMembers: exceeded the maximum number of attempts (%d) with the Slack API\", MaxSlackAPIAttempts)\n\t\t\t}\n\t\t\tlog.Debugf(\"ChannelMembers: page %d attempt #%d nextCursor=%s\", page, attempt, nextCursor)\n\t\t\tm, nextCursor, err = ctx.SlackClient.GetUsersInConversation(&slack.GetUsersInConversationParameters{ChannelID: channelID, Cursor: nextCursor, Limit: 1000})\n\t\t\tif err != nil {\n\t\t\t\tlog.Errorf(\"Failed to get users in conversation '%s': %v\", channelID, err)\n\t\t\t\tif rlErr, ok := err.(*slack.RateLimitedError); ok {\n\t\t\t\t\t// we were rate-limited. Let's wait as much as Slack\n\t\t\t\t\t// instructs us to do\n\t\t\t\t\tlog.Warningf(\"Hit Slack API rate limiter. Waiting %v\", rlErr.RetryAfter)\n\t\t\t\t\ttime.Sleep(rlErr.RetryAfter)\n\t\t\t\t\tattempt++\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn nil, fmt.Errorf(\"Cannot get member list for conversation %s: %v\", channelID, err)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t\tmembers = append(members, m...)\n\t\tlog.Debugf(\"Fetched %d user IDs for channel %s (fetched so far: %d)\", len(m), channelID, len(members))\n\t\t// TODO call ctx.Users.FetchByID here in a goroutine to see if this\n\t\t// speeds up\n\t\tif nextCursor == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tpage++\n\t}\n\tlog.Debugf(\"Retrieving user information for %d users\", len(members))\n\tusers, err := ctx.Users.FetchByIDs(ctx.SlackClient, false, members...)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"Failed to fetch users by their IDs: %v\", err)\n\t}\n\treturn users, nil\n}\n\n// Channel wraps a Slack conversation with a few utility functions.\ntype Channel slack.Channel\n\n// IsPublicChannel returns true if the channel is public.\nfunc (c *Channel) IsPublicChannel() bool {\n\treturn c.IsChannel && !c.IsPrivate\n}\n\n// IsPrivateChannel returns true if the channel is private.\nfunc (c *Channel) IsPrivateChannel() bool {\n\treturn c.IsGroup && c.IsPrivate\n}\n\n// IsMP returns true if it is a multi-party conversation.\nfunc (c *Channel) IsMP() bool {\n\treturn c.IsMpIM\n}\n\n// IRCName returns the channel name as it would appear on IRC.\n// Examples:\n// * #channel for public groups\n// * @channel for private groups\n// * &Gxxxx|nick1-nick2-nick3 for multi-party IMs\nfunc (c *Channel) IRCName() string {\n\tswitch {\n\tcase c.IsPublicChannel():\n\t\treturn ChannelPrefixPublicChannel + c.Name\n\tcase c.IsPrivateChannel():\n\t\treturn ChannelPrefixPrivateChannel + c.Name\n\tcase c.IsMP():\n\t\tname := ChannelPrefixMpIM + c.ID + \"|\" + c.Name\n\t\tname = strings.Replace(name, \"mpdm-\", \"\", -1)\n\t\tname = strings.Replace(name, \"--\", \"-\", -1)\n\t\tif len(name) >= 30 {\n\t\t\treturn name[:29] + \"…\"\n\t\t}\n\t\treturn name\n\tdefault:\n\t\tlog.Warningf(\"Unknown channel type for channel %+v\", c)\n\t\treturn \"<unknow-channel-type>\"\n\t}\n}\n\n// SlackName returns the slack.Channel.Name field.\nfunc (c *Channel) SlackName() string {\n\treturn c.Name\n}\n"
  },
  {
    "path": "pkg/ircslack/channels.go",
    "content": "package ircslack\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/slack-go/slack\"\n)\n\n// Channels wraps the channel list with convenient operations and cache.\ntype Channels struct {\n\tchannels   map[string]Channel\n\tPagination int\n\tmu         sync.Mutex\n}\n\n// NewChannels creates a new Channels object.\nfunc NewChannels(pagination int) *Channels {\n\treturn &Channels{\n\t\tchannels:   make(map[string]Channel),\n\t\tPagination: pagination,\n\t}\n}\n\n// SupportedChannelPrefixes returns a list of supported channel prefixes.\nfunc SupportedChannelPrefixes() []string {\n\treturn []string{\n\t\tChannelPrefixPublicChannel,\n\t\tChannelPrefixPrivateChannel,\n\t\tChannelPrefixMpIM,\n\t\tChannelPrefixThread,\n\t}\n\n}\n\n// AsMap returns the channels as a map of name -> channel. The map is copied to\n// avoid data races\nfunc (c *Channels) AsMap() map[string]Channel {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tret := make(map[string]Channel, len(c.channels))\n\tfor k, v := range c.channels {\n\t\tret[k] = v\n\t}\n\treturn ret\n}\n\n// FetchByIDs fetches the channels with the specified IDs and updates the\n// internal channel mapping.\nfunc (c *Channels) FetchByIDs(client *slack.Client, skipCache bool, channelIDs ...string) ([]Channel, error) {\n\tvar (\n\t\ttoRetrieve       []string\n\t\talreadyRetrieved []Channel\n\t)\n\n\tif !skipCache {\n\t\tc.mu.Lock()\n\t\tfor _, cid := range channelIDs {\n\t\t\tif ch, ok := c.channels[cid]; !ok {\n\t\t\t\ttoRetrieve = append(toRetrieve, cid)\n\t\t\t} else {\n\t\t\t\talreadyRetrieved = append(alreadyRetrieved, ch)\n\t\t\t}\n\t\t}\n\t\tc.mu.Unlock()\n\t\tlog.Debugf(\"Fetching information for %d channels out of %d (%d already in cache)\", len(toRetrieve), len(channelIDs), len(channelIDs)-len(toRetrieve))\n\t} else {\n\t\ttoRetrieve = channelIDs\n\t}\n\tallFetchedChannels := make([]Channel, 0, len(channelIDs))\n\tfor i := 0; i < len(toRetrieve); i++ {\n\t\tfor {\n\t\t\tattempt := 0\n\t\t\tif attempt >= MaxSlackAPIAttempts {\n\t\t\t\treturn nil, fmt.Errorf(\"Channels.FetchByIDs: exceeded the maximum number of attempts (%d) with the Slack API\", MaxSlackAPIAttempts)\n\t\t\t}\n\t\t\tlog.Debugf(\"Fetching %d channels of %d, attempt %d of %d\", len(toRetrieve), len(channelIDs), attempt+1, MaxSlackAPIAttempts)\n\t\t\tslackChannel, err := client.GetConversationInfo(&slack.GetConversationInfoInput{ChannelID: toRetrieve[i], IncludeLocale: true, IncludeNumMembers: true})\n\t\t\tif err != nil {\n\t\t\t\tif rlErr, ok := err.(*slack.RateLimitedError); ok {\n\t\t\t\t\t// we were rate-limited. Let's wait the recommended delay\n\t\t\t\t\tlog.Warningf(\"Hit Slack API rate limiter. Waiting %v\", rlErr.RetryAfter)\n\t\t\t\t\ttime.Sleep(rlErr.RetryAfter)\n\t\t\t\t\tattempt++\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tch := Channel(*slackChannel)\n\t\t\tallFetchedChannels = append(allFetchedChannels, ch)\n\t\t\t// also update the local users map\n\t\t\tc.mu.Lock()\n\t\t\tc.channels[ch.ID] = ch\n\t\t\tc.mu.Unlock()\n\t\t\tbreak\n\t\t}\n\t}\n\tallChannels := append(alreadyRetrieved, allFetchedChannels...)\n\tif len(channelIDs) != len(allChannels) {\n\t\treturn allFetchedChannels, fmt.Errorf(\"Found %d users but %d were requested\", len(allChannels), len(channelIDs))\n\t}\n\treturn allChannels, nil\n}\n\n// Fetch retrieves all the channels on a given Slack team. The Slack client has\n// to be valid and connected.\nfunc (c *Channels) Fetch(client *slack.Client) error {\n\tlog.Infof(\"Fetching all channels, might take a while on large Slack teams\")\n\t// currently slack-go does not expose a way to change channel pagination as\n\t// it does for the users API.\n\tvar (\n\t\terr      error\n\t\tctx      = context.Background()\n\t\tchannels = make(map[string]Channel)\n\t)\n\tstart := time.Now()\n\tparams := slack.GetConversationsParameters{\n\t\tTypes: []string{\"public_channel\", \"private_channel\"},\n\t\tLimit: c.Pagination,\n\t}\n\tfor err == nil {\n\t\tchans, nextCursor, err := client.GetConversationsContext(ctx, &params)\n\t\tif err == nil {\n\t\t\tlog.Debugf(\"Retrieved %d channels (current total is %d)\", len(chans), len(channels))\n\t\t\tfor _, sch := range chans {\n\t\t\t\t// WARNING WARNING WARNING: channels are internally mapped by\n\t\t\t\t// the Slack name, while users are mapped by Slack ID.\n\t\t\t\tch := Channel(sch)\n\t\t\t\tchannels[ch.SlackName()] = ch\n\t\t\t}\n\t\t} else if rateLimitedError, ok := err.(*slack.RateLimitedError); ok {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\terr = ctx.Err()\n\t\t\tcase <-time.After(rateLimitedError.RetryAfter):\n\t\t\t\terr = nil\n\t\t\t}\n\t\t}\n\t\tif nextCursor == \"\" {\n\t\t\tbreak\n\t\t}\n\t\tparams.Cursor = nextCursor\n\t}\n\tlog.Infof(\"Retrieved %d channels in %s\", len(channels), time.Since(start))\n\tc.mu.Lock()\n\tc.channels = channels\n\tfor name, ch := range channels {\n\t\tlog.Debugf(\"Retrieved channel: %s -> %+v\", name, ch)\n\t}\n\tc.mu.Unlock()\n\treturn nil\n}\n\n// Count returns the number of channels. This method must be called after\n// `Fetch`.\nfunc (c *Channels) Count() int {\n\treturn len(c.channels)\n}\n\n// ByID retrieves a channel by its Slack ID.\nfunc (c *Channels) ByID(id string) *Channel {\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tfor _, c := range c.channels {\n\t\tif c.ID == id {\n\t\t\treturn &c\n\t\t}\n\t}\n\treturn nil\n}\n\n// ByName retrieves a channel by its Slack or IRC name.\nfunc (c *Channels) ByName(name string) *Channel {\n\tif HasChannelPrefix(name) {\n\t\t// without prefix, the channel now has the form of a Slack name\n\t\tname = name[1:]\n\t}\n\tc.mu.Lock()\n\tdefer c.mu.Unlock()\n\tif ch, ok := c.channels[name]; ok {\n\t\treturn &ch\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/ircslack/channels_test.go",
    "content": "package ircslack\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/slack-go/slack\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestChannelsNewChannels(t *testing.T) {\n\tu := NewChannels(100)\n\trequire.NotNil(t, u)\n\tassert.NotNil(t, u.channels)\n}\n\ntype fakeErrorChannelsPaginationComplete struct{}\n\ntype fakeChannelsResponse struct {\n\tMembers []slack.Channel\n\tChannel slack.Channel\n}\n\nfunc (f fakeErrorChannelsPaginationComplete) Error() string {\n\treturn \"pagination complete\"\n}\n\ntype fakeSlackHTTPClientChannels struct{}\n\nfunc (c fakeSlackHTTPClientChannels) Do(req *http.Request) (*http.Response, error) {\n\tswitch req.URL.Path {\n\tcase \"/api/conversations.list\":\n\t\t// reply as per https://api.slack.com/methods/channels.list\n\t\tdata := []byte(`{\"channels\": [{\"id\": \"1234\", \"name\": \"general\", \"is_channel\": true}], \"response_metadata\": {\"next_cursor\": \"\"}}`)\n\t\treturn &http.Response{\n\t\t\tStatus:     \"200 OK\",\n\t\t\tStatusCode: 200,\n\t\t\tProto:      \"HTTP/1.1\",\n\t\t\tProtoMajor: 1,\n\t\t\tProtoMinor: 1,\n\t\t\tBody:       ioutil.NopCloser(bytes.NewBuffer(data)),\n\t\t}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"testing: http client URL not supported: %s\", req.URL)\n\t}\n}\n\nfunc TestChannelsFetch(t *testing.T) {\n\tclient := slack.New(\"test-token\", slack.OptionHTTPClient(fakeSlackHTTPClientChannels{}))\n\tchannels := NewChannels(100)\n\terr := channels.Fetch(client)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 1, channels.Count())\n}\n\nfunc TestChannelsById(t *testing.T) {\n\tclient := slack.New(\"test-token\", slack.OptionHTTPClient(fakeSlackHTTPClientChannels{}))\n\tchannels := NewChannels(100)\n\terr := channels.Fetch(client)\n\trequire.NoError(t, err)\n\tu := channels.ByID(\"1234\")\n\trequire.NotNil(t, u)\n\tassert.Equal(t, \"1234\", u.ID)\n\tassert.Equal(t, \"general\", u.Name)\n}\n\nfunc TestChannelsByName(t *testing.T) {\n\tclient := slack.New(\"test-token\", slack.OptionHTTPClient(fakeSlackHTTPClientChannels{}))\n\tchannels := NewChannels(100)\n\terr := channels.Fetch(client)\n\trequire.NoError(t, err)\n\tu := channels.ByName(\"general\")\n\trequire.NotNil(t, u)\n\tassert.Equal(t, \"1234\", u.ID)\n\tassert.Equal(t, \"general\", u.Name)\n}\n"
  },
  {
    "path": "pkg/ircslack/event_handler.go",
    "content": "package ircslack\n\nimport (\n\t\"fmt\"\n\t\"math\"\n\t\"strings\"\n\n\t\"github.com/slack-go/slack\"\n)\n\nfunc joinText(first string, second string, separator string) string {\n\tif first == \"\" {\n\t\treturn second\n\t}\n\tif second == \"\" {\n\t\treturn first\n\t}\n\treturn first + separator + second\n}\n\nfunc formatThreadChannelName(threadTimestamp string, channel *Channel) string {\n\treturn ChannelPrefixThread + channel.Name + \"-\" + threadTimestamp\n}\n\nfunc resolveChannelName(ctx *IrcContext, msgChannel, threadTimestamp string) string {\n\tif strings.HasPrefix(msgChannel, \"C\") || strings.HasPrefix(msgChannel, \"G\") {\n\t\t// Channel message\n\t\tchannel := ctx.Channels.ByID(msgChannel)\n\t\tif channel == nil {\n\t\t\t// try fetching it, in case it's a new channel\n\t\t\tchannels, err := ctx.Channels.FetchByIDs(ctx.SlackClient, false, msgChannel)\n\t\t\tif err != nil || len(channels) == 0 {\n\t\t\t\tctx.SendUnknownError(\"Failed to fetch channel with ID `%s`: %v\", msgChannel, err)\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\tchannel = &channels[0]\n\t\t}\n\n\t\tif channel == nil {\n\t\t\tctx.SendUnknownError(\"Unknown channel ID `%s` when resolving channel name\", msgChannel)\n\t\t\treturn \"\"\n\t\t} else if threadTimestamp != \"\" {\n\t\t\tchanname := formatThreadChannelName(threadTimestamp, channel)\n\t\t\topeningText, err := ctx.GetThreadOpener(msgChannel, threadTimestamp)\n\t\t\tif err != nil {\n\t\t\t\tctx.SendUnknownError(\"Failed to get thread opener for `%s`: %v\", msgChannel, err)\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\tIrcSendChanInfoAfterJoinCustom(\n\t\t\t\tctx,\n\t\t\t\tchanname,\n\t\t\t\tmsgChannel,\n\t\t\t\topeningText.Text,\n\t\t\t\t[]slack.User{},\n\t\t\t)\n\n\t\t\tprivmsg := fmt.Sprintf(\":%v!%v@%v PRIVMSG %v :%s%s%s\\r\\n\",\n\t\t\t\tchanname, openingText.User, ctx.ServerName,\n\t\t\t\tchanname, \"\", openingText.Text, \"\",\n\t\t\t)\n\t\t\tif _, err := ctx.Conn.Write([]byte(privmsg)); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\t\treturn channame\n\t\t} else if channel.IsMpIM {\n\t\t\tif ctx.Channels.ByName(channel.IRCName()) == nil {\n\t\t\t\tmembers, err := ChannelMembers(ctx, channel.ID)\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Warningf(\"Failed to fetch channel members for `%s`: %v\", channel.Name, err)\n\t\t\t\t} else {\n\t\t\t\t\tIrcSendChanInfoAfterJoin(ctx, channel, members)\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn channel.IRCName()\n\t\t}\n\n\t\treturn channel.IRCName()\n\t} else if strings.HasPrefix(msgChannel, \"D\") {\n\t\t// Direct message to me\n\t\tchannel := ctx.Channels.ByID(msgChannel)\n\t\tif channel == nil {\n\t\t\t// not found locally, try to get it via Slack API\n\t\t\tchannels, err := ctx.Channels.FetchByIDs(ctx.SlackClient, false, msgChannel)\n\t\t\tif err != nil || len(channels) == 0 {\n\t\t\t\tctx.SendUnknownError(\"Failed to fetch IM chat with ID `%s`: %v\", msgChannel, err)\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\tchannel = &channels[0]\n\t\t}\n\t\tmembers, err := ChannelMembers(ctx, channel.ID)\n\t\tif err != nil {\n\t\t\tctx.SendUnknownError(\"Failed to fetch channel members for `%s`: %v\", channel.Name, err)\n\t\t\treturn \"\"\n\t\t}\n\t\t// we expect only two members in a direct message. Raise an\n\t\t// error if not.\n\t\tif len(members) == 0 || len(members) > 2 {\n\t\t\tctx.SendUnknownError(\"Want 1 or 2 users in conversation, got %d (conversation ID: %s)\", len(members), msgChannel)\n\t\t\treturn \"\"\n\t\t}\n\t\t// of the two users, one is me. Otherwise fail\n\t\tif ctx.UserID() == \"\" {\n\t\t\tctx.SendUnknownError(\"Cannot get my own user ID\")\n\t\t\treturn \"\"\n\t\t}\n\t\tuser1 := members[0]\n\t\tvar user2 slack.User\n\t\tif len(members) == 2 {\n\t\t\tuser2 = members[1]\n\t\t} else {\n\t\t\t// len is 1. Sending a message to myself\n\t\t\tuser2 = user1\n\t\t}\n\t\tif user1.ID != ctx.UserID() && user2.ID != ctx.UserID() {\n\t\t\tctx.SendUnknownError(\"Got a direct message where I am not part of the members list (conversation: %s)\", msgChannel)\n\t\t\treturn \"\"\n\t\t}\n\t\tvar recipientID string\n\t\tif user1.ID == ctx.UserID() {\n\t\t\t// then it's the other user\n\t\t\trecipientID = user2.ID\n\t\t} else {\n\t\t\trecipientID = user1.ID\n\t\t}\n\t\t// now resolve the ID to the user's nickname\n\t\tnickname := ctx.GetUserInfo(recipientID)\n\t\tif nickname == nil {\n\t\t\t// ERR_UNKNOWNERROR\n\t\t\tctx.SendUnknownError(\"Unknown destination user ID %s for direct message %s\", recipientID, msgChannel)\n\t\t\treturn \"\"\n\t\t}\n\t\treturn nickname.Name\n\t}\n\tlog.Warningf(\"Unknown recipient ID: %s\", msgChannel)\n\treturn \"\"\n}\n\nfunc appendIfNotMoreThan(slice []slack.Msg, msg slack.Msg) []slack.Msg {\n\tif len(slice) == 100 {\n\t\treturn append(slice[1:], msg)\n\t}\n\treturn append(slice, msg)\n}\n\nfunc getConversationDetails(\n\tctx *IrcContext,\n\tchannelID string,\n\ttimestamp string,\n) (slack.Message, error) {\n\tmessage, err := ctx.SlackClient.GetConversationHistory(&slack.GetConversationHistoryParameters{\n\t\tChannelID: channelID,\n\t\tLatest:    timestamp,\n\t\tLimit:     1,\n\t\tInclusive: true,\n\t})\n\tif err != nil {\n\t\treturn slack.Message{}, err\n\t}\n\tif len(message.Messages) > 0 {\n\t\treturn message.Messages[0], nil\n\t}\n\treturn slack.Message{}, fmt.Errorf(\"No such message found\")\n}\n\nfunc replacePermalinkWithText(ctx *IrcContext, text string) string {\n\tmatches := rxSlackArchiveURL.FindStringSubmatch(text)\n\tif len(matches) != 4 {\n\t\treturn text\n\t}\n\tchannel := matches[1]\n\ttimestamp := matches[2] + \".\" + matches[3]\n\tmessage, err := getConversationDetails(ctx, channel, timestamp)\n\tif err != nil {\n\t\tlog.Printf(\"could not get message details from permalink %s %s %s %v\", matches[0], channel, timestamp, err)\n\t\treturn text\n\t}\n\treturn text + \"\\n> \" + message.Text\n}\n\nfunc printMessage(ctx *IrcContext, message slack.Msg, prefix string) {\n\tuser := ctx.GetUserInfo(message.User)\n\tname := \"\"\n\tif user == nil {\n\t\tif message.User != \"\" {\n\t\t\tlog.Warningf(\"Failed to get user info for %v %s\", message.User, message.Username)\n\t\t\tname = message.User\n\t\t} else {\n\t\t\tname = strings.ReplaceAll(message.Username, \" \", \"_\")\n\t\t}\n\t} else {\n\t\tname = user.Name\n\t}\n\t// get channel or other recipient (e.g. recipient of a direct message)\n\tchanname := resolveChannelName(ctx, message.Channel, message.ThreadTimestamp)\n\n\ttext := message.Text\n\tfor _, attachment := range message.Attachments {\n\t\ttext = joinText(text, attachment.Pretext, \"\\n\")\n\t\ttext = joinText(text, attachment.Title, \"\\n\")\n\t\tif attachment.Text != \"\" {\n\t\t\ttext = joinText(text, attachment.Text, \"\\n\")\n\t\t} else {\n\t\t\ttext = joinText(text, attachment.Fallback, \"\\n\")\n\t\t}\n\t\ttext = joinText(text, attachment.ImageURL, \"\\n\")\n\t}\n\tfor _, file := range message.Files {\n\t\ttext = joinText(text, ctx.FileHandler.Download(file), \" \")\n\t}\n\n\tlog.Debugf(\"SLACK msg from %v (%v) on %v: %v\",\n\t\tmessage.User,\n\t\tname,\n\t\tmessage.Channel,\n\t\ttext,\n\t)\n\tif name == \"\" && text == \"\" {\n\t\tlog.Warningf(\"Empty username and message: %+v\", message)\n\t\treturn\n\t}\n\ttext = replacePermalinkWithText(ctx, text)\n\ttext = ctx.ExpandUserIds(text)\n\ttext = ExpandText(text)\n\ttext = joinText(prefix, text, \" \")\n\n\tif name == ctx.Nick() {\n\t\tbotID := message.BotID\n\t\tif (ctx.usingLegacyToken && user != nil && botID != user.Profile.BotID) ||\n\t\t\t(!ctx.usingLegacyToken && message.ClientMsgID == \"\") {\n\t\t\t// Don't print my own messages.\n\t\t\t// When using legacy tokens, we distinguish our own messages sent\n\t\t\t// from other clients by checking the bot ID.\n\t\t\t// With new style tokens, we check the client message ID.\n\t\t\tlog.Debugf(\"Skipping message sent by me\")\n\t\t\treturn\n\t\t}\n\t}\n\t// handle multi-line messages\n\tvar linePrefix, lineSuffix string\n\tif message.SubType == \"me_message\" {\n\t\t// handle /me messages\n\t\tlinePrefix = \"\\x01ACTION \"\n\t\tlineSuffix = \"\\x01\"\n\t}\n\tfor _, line := range strings.Split(text, \"\\n\") {\n\t\tprivmsg := fmt.Sprintf(\":%v!%v@%v PRIVMSG %v :%s%s%s\\r\\n\",\n\t\t\tname, message.User, ctx.ServerName,\n\t\t\tchanname, linePrefix, line, lineSuffix,\n\t\t)\n\t\tlog.Debug(privmsg)\n\t\tif _, err := ctx.Conn.Write([]byte(privmsg)); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t}\n}\n\nfunc eventHandler(ctx *IrcContext, rtm *slack.RTM) {\n\tlog.Info(\"Started Slack event listener\")\n\tfor msg := range rtm.IncomingEvents {\n\t\tswitch ev := msg.Data.(type) {\n\t\tcase *slack.MessageEvent:\n\t\t\t// https://api.slack.com/events/message\n\t\t\tmessage := ev.Msg\n\t\t\tif message.Hidden {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tswitch message.SubType {\n\t\t\tcase \"message_changed\":\n\t\t\t\t// https://api.slack.com/events/message/message_changed\n\t\t\t\teditedMessage, err := getConversationDetails(ctx, message.Channel, message.Timestamp)\n\t\t\t\tif err != nil {\n\t\t\t\t\tfmt.Printf(\"could not get changed conversation details %s\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tlog.Printf(\"edited msg chan %v\", editedMessage.Msg.Channel)\n\t\t\t\teditedMessage.Msg.Channel = message.Channel\n\t\t\t\tprintMessage(ctx, editedMessage.Msg, \"(edited)\")\n\t\t\t\tcontinue\n\t\t\tcase \"channel_topic\":\n\t\t\t\t// https://api.slack.com/events/message/channel_topic\n\t\t\t\t// Send out new topic\n\t\t\t\tchannel := ctx.Channels.ByID(message.Channel)\n\t\t\t\tif channel == nil {\n\t\t\t\t\tlog.Warningf(\"Cannot get channel name for %v\", message.Channel)\n\t\t\t\t} else {\n\t\t\t\t\tnewTopic := fmt.Sprintf(\":%v TOPIC %s :%v\\r\\n\", ctx.Mask(), channel.IRCName(), message.Topic)\n\t\t\t\t\tlog.Infof(\"Got new topic: %v\", newTopic)\n\t\t\t\t\tif _, err := ctx.Conn.Write([]byte(newTopic)); err != nil {\n\t\t\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase \"channel_join\", \"channel_leave\":\n\t\t\t\t// https://api.slack.com/events/message/channel_join\n\t\t\t\t// https://api.slack.com/events/message/channel_leave\n\t\t\t\t// Note: this is handled by slack.MemberJoinedChannelEvent\n\t\t\t\t// and slack.MemberLeftChannelEvent.\n\t\t\tdefault:\n\t\t\t\tprintMessage(ctx, message, \"\")\n\t\t\t}\n\t\tcase *slack.ConnectedEvent:\n\t\t\tlog.Info(\"Connected to Slack\")\n\t\t\tctx.SlackConnected = true\n\t\tcase *slack.DisconnectedEvent:\n\t\t\tde := msg.Data.(*slack.DisconnectedEvent)\n\t\t\tlog.Warningf(\"Disconnected from Slack (intentional: %v, cause: %v)\", de.Intentional, de.Cause)\n\t\t\tctx.SlackConnected = false\n\t\t\tctx.Conn.Close()\n\t\t\tctx.Users, ctx.Channels = nil, nil\n\t\t\treturn\n\t\tcase *slack.MemberJoinedChannelEvent:\n\t\t\t// This is the currently preferred way to notify when a user joins a\n\t\t\t// channel, see https://api.slack.com/changelog/2017-05-rethinking-channel-entrance-and-exit-events-and-messages\n\t\t\t// https://api.slack.com/events/member_joined_channel\n\t\t\tlog.Infof(\"Event: Member Joined Channel: %+v\", ev)\n\t\t\tch := ctx.Channels.ByID(ev.Channel)\n\t\t\tif ch == nil {\n\t\t\t\tlog.Warningf(\"Unknown channel: %s\", ev.Channel)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, err := ctx.Conn.Write([]byte(fmt.Sprintf(\":%s JOIN %s\\r\\n\", ctx.Mask(), ch.IRCName()))); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC JOIN message for `%s`: %v\", ch.IRCName(), err)\n\t\t\t}\n\t\tcase *slack.MemberLeftChannelEvent:\n\t\t\t// This is the currently preferred way to notify when a user leaves a\n\t\t\t// channel, see https://api.slack.com/changelog/2017-05-rethinking-channel-entrance-and-exit-events-and-messages\n\t\t\t// https://api.slack.com/events/member_left_channel\n\t\t\tlog.Infof(\"Event: Member Left Channel: %+v\", ev)\n\t\t\tch := ctx.Channels.ByID(ev.Channel)\n\t\t\tif ch == nil {\n\t\t\t\tlog.Warningf(\"Unknown channel: %s\", ev.Channel)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif _, err := ctx.Conn.Write([]byte(fmt.Sprintf(\":%v PART %s\\r\\n\", ctx.Mask(), ch.IRCName()))); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\tcase *slack.TeamJoinEvent:\n\t\t\t// https://api.slack.com/events/team_join\n\t\t\t// update the users list\n\t\t\tif _, err := ctx.Users.FetchByIDs(ctx.SlackClient, false, ev.User.ID); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to fetch users: %v\", err)\n\t\t\t}\n\t\tcase *slack.UserChangeEvent:\n\t\t\t// https://api.slack.com/events/user_change\n\t\t\t// update the user list\n\t\t\tif _, err := ctx.Users.FetchByIDs(ctx.SlackClient, false, ev.User.ID); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to fetch users: %v\", err)\n\t\t\t}\n\t\tcase *slack.ChannelJoinedEvent, *slack.ChannelLeftEvent:\n\t\t\t// https://api.slack.com/events/channel_joined\n\t\t\t// Note: this is handled by slack.MemberJoinedChannelEvent\n\t\t\t// and slack.MemberLeftChannelEvent.\n\t\tcase *slack.ReactionAddedEvent:\n\t\t\t// https://api.slack.com/events/reaction_added\n\t\t\tchanname := resolveChannelName(ctx, ev.Item.Channel, \"\")\n\t\t\tuser := ctx.GetUserInfo(ev.User)\n\t\t\tname := \"\"\n\t\t\tif user == nil {\n\t\t\t\tlog.Warningf(\"Error getting user info for %v\", ev.User)\n\t\t\t\tname = ev.User\n\t\t\t} else {\n\t\t\t\tname = user.Name\n\t\t\t}\n\t\t\tmsg, err := getConversationDetails(ctx, ev.Item.Channel, ev.Item.Timestamp)\n\t\t\tif err != nil {\n\t\t\t\tfmt.Printf(\"could not get Conversation details %s\", err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tmsgText := msg.Text\n\n\t\t\tmsgText = ctx.ExpandUserIds(msgText)\n\t\t\tmsgText = ExpandText(msgText)\n\t\t\tmsgText = strings.Split(msgText, \"\\n\")[0]\n\n\t\t\tmsgText = msgText[:int(math.Min(float64(len(msgText)), 100))]\n\n\t\t\tprivmsg := fmt.Sprintf(\":%v!%v@%v PRIVMSG %v :\\x01ACTION reacted with %s to: \\x0315%s\\x03\\x01\\r\\n\",\n\t\t\t\tname, ev.User, ctx.ServerName,\n\t\t\t\tchanname, ev.Reaction, msgText,\n\t\t\t)\n\t\t\tlog.Debug(privmsg)\n\t\t\tif _, err := ctx.Conn.Write([]byte(privmsg)); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\tcase *slack.UserTypingEvent:\n\t\t\t// https://api.slack.com/events/user_typing\n\t\t\tu := ctx.GetUserInfo(ev.User)\n\t\t\tusername := \"<unknown>\"\n\t\t\tif u != nil {\n\t\t\t\tusername = u.Name\n\t\t\t}\n\t\t\tc, err := ctx.GetConversationInfo(ev.Channel)\n\t\t\tchanname := \"<unknown or IM chat>\"\n\t\t\tif err == nil {\n\t\t\t\tchanname = c.Name\n\t\t\t}\n\t\t\tlog.Infof(\"User %s (%s) is typing on channel %s (%s)\", ev.User, username, ev.Channel, channame)\n\t\tcase *slack.DesktopNotificationEvent:\n\t\t\t// TODO implement actions on notifications\n\t\t\tlog.Infof(\"Event: Desktop notification: %+v\", ev)\n\t\tcase *slack.LatencyReport:\n\t\t\tlog.Infof(\"Current Slack latency: %v\", ev.Value)\n\t\tcase *slack.RTMError:\n\t\t\tlog.Warningf(\"Slack RTM error: %v\", ev.Error())\n\t\tcase *slack.InvalidAuthEvent:\n\t\t\tlog.Warningf(\"Invalid slack credentials\")\n\t\tdefault:\n\t\t\tlog.Debugf(\"SLACK event: %v: %+v\", msg.Type, msg.Data)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/ircslack/file_handler.go",
    "content": "package ircslack\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/slack-go/slack\"\n)\n\nconst (\n\tmaxHTTPAttempts = 3\n\tretryInterval   = time.Second\n)\n\n// FileHandler downloads files from slack\ntype FileHandler struct {\n\tSlackAPIKey          string\n\tFileDownloadLocation string\n\tProxyPrefix          string\n}\n\nfunc retryableNetError(err error) bool {\n\tif err == nil {\n\t\treturn false\n\t}\n\tswitch err := err.(type) {\n\tcase net.Error:\n\t\tif err.Timeout() {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc retryableHTTPError(resp *http.Response) bool {\n\tif resp == nil {\n\t\treturn false\n\t}\n\tif resp.StatusCode == 500 || resp.StatusCode == 502 {\n\t\treturn true\n\t}\n\treturn false\n}\n\n// Download downloads url contents to a local file and returns a url to either\n// the file on slack's server or a downloaded file\nfunc (handler *FileHandler) Download(file slack.File) string {\n\tfileURL := file.URLPrivate\n\tif handler.FileDownloadLocation == \"\" || file.IsExternal || handler.SlackAPIKey == \"\" {\n\t\treturn fileURL\n\t}\n\tlocalFileName := fmt.Sprintf(\"%s_%s\", file.ID, file.Title)\n\tif !strings.HasSuffix(localFileName, file.Filetype) {\n\t\tlocalFileName += \".\" + file.Filetype\n\t}\n\tlocalFilePath := filepath.Join(handler.FileDownloadLocation, localFileName)\n\tgo func() {\n\t\tout, err := os.Create(localFilePath)\n\t\tif err != nil {\n\t\t\tlog.Warningf(\"Could not create file for download %s: %v\", localFilePath, err)\n\t\t\treturn\n\t\t}\n\n\t\tdefer out.Close()\n\t\trequest, _ := http.NewRequest(\"GET\", fileURL, nil)\n\t\trequest.Header.Add(\"Authorization\", \"Bearer \"+handler.SlackAPIKey)\n\t\tvar client = &http.Client{}\n\t\tvar resp *http.Response\n\t\tfor attempt := 0; attempt < maxHTTPAttempts; attempt++ {\n\t\t\tresp, err = client.Do(request)\n\t\t\tif err != nil && retryableNetError(err) || retryableHTTPError(resp) {\n\t\t\t\ttime.Sleep(retryInterval * time.Duration(math.Pow(float64(attempt), 2)))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif err == nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlog.Warningf(\"Error downloading %s: %v\", fileURL, err)\n\t\t\treturn\n\t\t}\n\t\tif resp.StatusCode != http.StatusOK {\n\t\t\tlog.Debugf(\"Got %d while downloading %s\", resp.StatusCode, fileURL)\n\t\t\treturn\n\t\t}\n\t\tdefer resp.Body.Close()\n\t\t_, err = io.Copy(out, resp.Body)\n\t\tif err != nil {\n\t\t\tlog.Warningf(\"Error writing %s: %v\", fileURL, err)\n\t\t}\n\t}()\n\tif handler.ProxyPrefix != \"\" {\n\t\treturn handler.ProxyPrefix + url.PathEscape(localFileName)\n\t}\n\treturn fileURL\n}\n"
  },
  {
    "path": "pkg/ircslack/irc_context.go",
    "content": "package ircslack\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/slack-go/slack\"\n)\n\n// SlackPostMessage represents a message sent to slack api\ntype SlackPostMessage struct {\n\tTarget   string\n\tTargetTs string\n\tText     string\n}\n\n// IrcContext holds the client context information\ntype IrcContext struct {\n\tConn net.Conn\n\tUser *slack.User\n\t// TODO make RealName a function\n\tRealName          string\n\tOrigName          string\n\tSlackClient       *slack.Client\n\tSlackRTM          *slack.RTM\n\tSlackAPIKey       string\n\tSlackDebug        bool\n\tSlackConnected    bool\n\tServerName        string\n\tChannels          *Channels\n\tUsers             *Users\n\tChunkSize         int\n\tpostMessage       chan SlackPostMessage\n\tconversationCache map[string]*slack.Channel\n\tFileHandler       *FileHandler\n\t// set to `true` if we are using a deprecated legacy token, false otherwise\n\tusingLegacyToken bool\n}\n\n// Nick returns the nickname of the user, if known\nfunc (ic *IrcContext) Nick() string {\n\tif ic.User == nil {\n\t\treturn \"<unknown>\"\n\t}\n\treturn ic.User.Name\n}\n\n// UserName returns the user's name. Currently this is equivalent to the user's\n// Slack ID\nfunc (ic *IrcContext) UserName() string {\n\tif ic.User == nil {\n\t\treturn \"<unknown>\"\n\t}\n\treturn ic.User.ID\n}\n\n// GetThreadOpener returns text of the first message in a thread that provided message belongs to\nfunc (ic *IrcContext) GetThreadOpener(channel string, threadTimestamp string) (slack.Message, error) {\n\tmsgs, _, _, err := ic.SlackClient.GetConversationReplies(&slack.GetConversationRepliesParameters{\n\t\tChannelID: channel,\n\t\tTimestamp: threadTimestamp,\n\t})\n\tif err != nil || len(msgs) == 0 {\n\t\treturn slack.Message{}, err\n\t}\n\treturn msgs[0], nil\n}\n\n// ExpandUserIds will convert slack user tags with user's nicknames\nfunc (ic *IrcContext) ExpandUserIds(text string) string {\n\treturn rxSlackUser.ReplaceAllStringFunc(text, func(subs string) string {\n\t\tuid := subs[2 : len(subs)-1]\n\t\tuser := ic.GetUserInfo(uid)\n\t\tif user == nil {\n\t\t\treturn subs\n\t\t}\n\t\treturn fmt.Sprintf(\"@%s\", user.Name)\n\t})\n}\n\n// Start handles batching of messages to slack\nfunc (ic *IrcContext) Start() {\n\ttextBuffer := make(map[string]string)\n\ttimer := time.NewTimer(time.Second)\n\tvar message SlackPostMessage\n\tfor {\n\t\tselect {\n\t\tcase message = <-ic.postMessage:\n\t\t\tlog.Debugf(\"Got new message %v\", message)\n\t\t\ttextBuffer[message.Target] += message.Text + \"\\n\"\n\t\t\ttimer.Reset(time.Second)\n\t\tcase <-timer.C:\n\t\t\tfor target, text := range textBuffer {\n\t\t\t\topts := []slack.MsgOption{}\n\t\t\t\topts = append(opts, slack.MsgOptionAsUser(true))\n\t\t\t\topts = append(opts, slack.MsgOptionText(strings.TrimSpace(text), false))\n\t\t\t\tif message.TargetTs != \"\" {\n\t\t\t\t\topts = append(opts, slack.MsgOptionTS(message.TargetTs))\n\t\t\t\t}\n\t\t\t\tif _, _, err := ic.SlackClient.PostMessage(target, opts...); err != nil {\n\t\t\t\t\tlog.Warningf(\"Failed to post message to Slack to target %s: %v\", target, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\ttextBuffer = make(map[string]string)\n\t\t}\n\t}\n}\n\n// PostTextMessage batches all messages that should be posted to slack\nfunc (ic *IrcContext) PostTextMessage(target, text, targetTs string) {\n\tic.postMessage <- SlackPostMessage{\n\t\tTarget:   target,\n\t\tTargetTs: targetTs,\n\t\tText:     text,\n\t}\n}\n\n// GetUserInfo returns a slack.User instance from a given user ID, or nil if\n// no user with that ID was found\nfunc (ic *IrcContext) GetUserInfo(userID string) *slack.User {\n\tu := ic.Users.ByID(userID)\n\tif u == nil {\n\t\tlog.Warningf(\"GetUserInfo: unknown user ID '%s'\", userID)\n\t}\n\treturn u\n}\n\n// GetUserInfoByName returns a slack.User instance from a given user name, or\n// nil if no user with that name was found\nfunc (ic *IrcContext) GetUserInfoByName(username string) *slack.User {\n\tu := ic.Users.ByName(username)\n\tif u == nil {\n\t\tlog.Warningf(\"GetUserInfoByName: unknown user name '%s'\", username)\n\t}\n\treturn u\n}\n\n// UserID returns the user's Slack ID\nfunc (ic IrcContext) UserID() string {\n\tif ic.User == nil {\n\t\treturn \"<unknown>\"\n\t}\n\treturn ic.User.ID\n}\n\n// Mask returns the IRC mask for the current user\nfunc (ic IrcContext) Mask() string {\n\treturn fmt.Sprintf(\"%v!%v@%v\", ic.Nick(), ic.UserName(), ic.Conn.RemoteAddr().(*net.TCPAddr).IP)\n}\n\n// GetConversationInfo is cached version of slack.GetConversationInfo\nfunc (ic IrcContext) GetConversationInfo(conversation string) (*slack.Channel, error) {\n\tc, ok := ic.conversationCache[conversation]\n\tif ok {\n\t\treturn c, nil\n\t}\n\tc, err := ic.SlackClient.GetConversationInfo(&slack.GetConversationInfoInput{ChannelID: conversation, IncludeLocale: true, IncludeNumMembers: true})\n\tif err != nil {\n\t\treturn c, err\n\t}\n\tic.conversationCache[conversation] = c\n\treturn c, nil\n}\n\n// Maps of user contexts and nicknames\nvar (\n\tUserContexts = map[net.Addr]*IrcContext{}\n)\n\n// SendUnknownError sends an IRC 400 (ERR_UNKNOWNERROR) message to the client\n// and prints a warning about it.\nfunc (ic *IrcContext) SendUnknownError(fmtstr string, args ...interface{}) {\n\tmsg := fmt.Sprintf(fmtstr, args...)\n\tlog.Warningf(\"Sending ERR_UNKNOWNERROR (400) to client with message: %s\", msg)\n\tif err := SendIrcNumeric(ic, 400, ic.Nick(), msg); err != nil {\n\t\tlog.Warningf(\"Failed to send ERR_UNKNOWNERROR (400) to client: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "pkg/ircslack/irc_server.go",
    "content": "package ircslack\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"html\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/coredhcp/coredhcp/logger\"\n\t\"github.com/sirupsen/logrus\"\n\t\"github.com/slack-go/slack\"\n)\n\n// Project constants\nconst (\n\tProjectAuthor       = \"Andrea Barberio\"\n\tProjectAuthorEmail  = \"insomniac@slackware.it\"\n\tProjectURL          = \"https://github.com/insomniacslk/irc-slack\"\n\tMaxSlackAPIAttempts = 3\n)\n\n// IrcCommandHandler is the prototype that every IRC command handler has to implement\ntype IrcCommandHandler func(*IrcContext, string, string, []string, string)\n\n// IrcCommandHandlers maps each IRC command to its handler function\nvar IrcCommandHandlers = map[string]IrcCommandHandler{\n\t\"CAP\":     IrcCapHandler,\n\t\"NICK\":    IrcNickHandler,\n\t\"USER\":    IrcUserHandler,\n\t\"PING\":    IrcPingHandler,\n\t\"PRIVMSG\": IrcPrivMsgHandler,\n\t\"QUIT\":    IrcQuitHandler,\n\t\"MODE\":    IrcModeHandler,\n\t\"PASS\":    IrcPassHandler,\n\t\"WHOIS\":   IrcWhoisHandler,\n\t\"WHO\":     IrcWhoHandler,\n\t\"JOIN\":    IrcJoinHandler,\n\t\"PART\":    IrcPartHandler,\n\t\"TOPIC\":   IrcTopicHandler,\n\t\"NAMES\":   IrcNamesHandler,\n}\n\n// IrcNumericsSafeToChunk is a list of IRC numeric replies that are safe\n// to chunk. As per RFC2182, the maximum message size is 512, including\n// newlines. Sending longer lines breaks some clients like ZNC. See\n// https://github.com/insomniacslk/irc-slack/issues/38 for background.\n// This list is meant to grow if we find more IRC numerics that are safe\n// to split.\n// Being safe to split doesn't mean that it *will* be split. The actual\n// behaviour depends on the IrcContext.ChunkSize value.\nvar IrcNumericsSafeToChunk = []int{\n\t// RPL_WHOREPLY\n\t352,\n\t// RPL_NAMREPLY\n\t353,\n}\n\n// SplitReply will split a reply message if necessary. See\n// IrcNumericSafeToChunk for background on why splitting.\n// The function will return a list of chunks to be sent\n// separately.\n// The first argument is the entire message to be split.\n// The second argument is the chunk size to use to determine\n// whether the message should be split. Any value equal or above\n// 512 will cause splitting. Any other value will return the\n// unmodified string as only item of the list.\nfunc SplitReply(preamble, msg string, chunksize int) []string {\n\tif chunksize < 512 || chunksize >= len(preamble)+len(msg)+2 {\n\t\t// return the whole string as one chunk\n\t\treturn []string{preamble + msg + \"\\r\\n\"}\n\t}\n\tlog.Debugf(\"Splitting reply in %d-byte chunks\", chunksize)\n\t// Split and build a string until it's long enough to fit the\n\t// chunk. Splitting ignores multiple contiguous white-spaces.\n\t// We assume this is safe (unless we find out it's not).\n\t// Additionally, squeezing multiple contiguous spaces could\n\t// render the final reply shorter than the chunk size, but we\n\t// don't care here.\n\tmaxLen := chunksize - len(preamble) - 2\n\tlines := WordWrap(strings.Fields(msg), maxLen)\n\treply := make([]string, len(lines))\n\tfor idx, line := range lines {\n\t\treply[idx] = preamble + line + \"\\r\\n\"\n\t}\n\treturn reply\n}\n\nvar (\n\trxSlackUrls       = regexp.MustCompile(`<[^>]+>?`)\n\trxSlackUser       = regexp.MustCompile(`<@[UW][A-Z0-9]+>`)\n\trxSlackArchiveURL = regexp.MustCompile(`https?:\\\\/\\\\/[a-z0-9\\\\-]+\\\\.slack\\\\.com\\\\/archives\\\\/([a-zA-Z0-9]+)\\\\/p([0-9]{10})([0-9]{6})`)\n)\n\n// ExpandText expands and unquotes text and URLs from Slack's messages. Slack\n// quotes the text and URLS, and the latter are enclosed in < and >. It also\n// translates potential URLs into actual URLs (e.g. when you type \"example.com\"),\n// so you will get something like <http://example.com|example.com>. This\n// function tries to detect them and unquote and expand them for a better\n// visualization on IRC.\nfunc ExpandText(text string) string {\n\n\ttext = rxSlackUrls.ReplaceAllStringFunc(text, func(subs string) string {\n\t\tif !strings.HasPrefix(subs, \"<\") && !strings.HasSuffix(subs, \">\") {\n\t\t\treturn subs\n\t\t}\n\n\t\t// Slack URLs may contain an URL followed by a \"|\", followed by the\n\t\t// original message. Detect the pipe and only parse the URL.\n\t\tvar (\n\t\t\tslackURL = subs[1 : len(subs)-1]\n\t\t\tslackMsg string\n\t\t)\n\t\tidx := strings.LastIndex(slackURL, \"|\")\n\t\tif idx >= 0 {\n\t\t\tslackMsg = slackURL[idx+1:]\n\t\t\tslackURL = slackURL[:idx]\n\t\t}\n\n\t\tu, err := url.Parse(slackURL)\n\t\tif err != nil {\n\t\t\treturn subs\n\t\t}\n\t\t// Slack escapes the URLs passed by the users, let's undo that\n\t\t//u.RawQuery = html.UnescapeString(u.RawQuery)\n\t\tif slackMsg == \"\" {\n\t\t\treturn u.String()\n\t\t}\n\t\treturn fmt.Sprintf(\"%s (%s)\", slackMsg, u.String())\n\t})\n\ttext = html.UnescapeString(text)\n\treturn text\n}\n\n// SendIrcNumeric sends a numeric code message to the recipient\nfunc SendIrcNumeric(ctx *IrcContext, code int, args, desc string) error {\n\tpreamble := fmt.Sprintf(\":%s %03d %s :\", ctx.ServerName, code, args)\n\t//reply := fmt.Sprintf(\":%s %03d %s :%s\\r\\n\", ctx.ServerName, code, args, desc)\n\tchunks := SplitReply(preamble, desc, ctx.ChunkSize)\n\tfor _, chunk := range chunks {\n\t\tlog.Debugf(\"Sending numeric reply: %s\", chunk)\n\t\t_, err := ctx.Conn.Write([]byte(chunk))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// IrcSendChanInfoAfterJoin sends channel information to the user about a joined\n// channel.\nfunc IrcSendChanInfoAfterJoin(ctx *IrcContext, ch *Channel, members []slack.User) {\n\tIrcSendChanInfoAfterJoinCustom(ctx, ch.IRCName(), ch.ID, ch.Purpose.Value, members)\n}\n\n// IrcSendChanInfoAfterJoinCustom sends channel information to the user about a joined\n// channel. It can be used as an alternative to IrcSendChanInfoAfterJoin when\n// you need to specify custom chan name, id, and topic.\nfunc IrcSendChanInfoAfterJoinCustom(ctx *IrcContext, chanName, chanID, topic string, members []slack.User) {\n\tmemberNames := make([]string, 0, len(members))\n\tfor _, m := range members {\n\t\tmemberNames = append(memberNames, m.Name)\n\t}\n\t// TODO wrap all these Conn.Write into a function\n\tif _, err := ctx.Conn.Write([]byte(fmt.Sprintf(\":%s JOIN %s\\r\\n\", ctx.Mask(), chanName))); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC JOIN message: %v\", err)\n\t}\n\t// RPL_TOPIC\n\tif err := SendIrcNumeric(ctx, 332, fmt.Sprintf(\"%s %s\", ctx.Nick(), chanName), topic); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC TOPIC message: %v\", err)\n\t}\n\t// RPL_NAMREPLY\n\tif len(members) > 0 {\n\t\tif err := SendIrcNumeric(ctx, 353, fmt.Sprintf(\"%s = %s\", ctx.Nick(), chanName), strings.Join(memberNames, \" \")); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC NAMREPLY message: %v\", err)\n\t\t}\n\t}\n\t// RPL_ENDOFNAMES\n\tif err := SendIrcNumeric(ctx, 366, fmt.Sprintf(\"%s %s\", ctx.Nick(), chanName), \"End of NAMES list\"); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC ENDOFNAMES message: %v\", err)\n\t}\n\tlog.Infof(\"Joined channel %s\", chanName)\n}\n\n// joinChannel will join the channel with the given ID, name and topic, and send back a\n// response to the IRC client\nfunc joinChannel(ctx *IrcContext, ch *Channel) error {\n\tlog.Infof(\"%s topic=%s members=%d\", ch.IRCName(), ch.Purpose.Value, ch.NumMembers)\n\t// the channels are already joined, notify the IRC client of their\n\t// existence\n\tmembers, err := ChannelMembers(ctx, ch.ID)\n\tif err != nil {\n\t\tjErr := fmt.Errorf(\"Failed to fetch users in channel `%s (channel ID: %s): %v\", ch.Name, ch.ID, err)\n\t\tctx.SendUnknownError(\"%s\", jErr.Error())\n\t\treturn jErr\n\t}\n\tgo IrcSendChanInfoAfterJoin(ctx, ch, members)\n\treturn nil\n}\n\n// joinChannels gets all the available Slack channels and sends an IRC JOIN message\n// for each of the joined channels on Slack\nfunc joinChannels(ctx *IrcContext) error {\n\tfor _, sch := range ctx.Channels.AsMap() {\n\t\tch := Channel(sch)\n\t\tif !ch.IsPublicChannel() && !ch.IsPrivateChannel() {\n\t\t\tcontinue\n\t\t}\n\t\tif ch.IsMember {\n\t\t\tif err := joinChannel(ctx, &ch); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\n// IrcAfterLoggingIn is called once the user has successfully logged on IRC\nfunc IrcAfterLoggingIn(ctx *IrcContext, rtm *slack.RTM) error {\n\tif ctx.OrigName != ctx.Nick() {\n\t\t// Force the user into the Slack nick\n\t\tif _, err := ctx.Conn.Write([]byte(fmt.Sprintf(\":%s NICK %s\\r\\n\", ctx.OrigName, ctx.Nick()))); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t}\n\t// Send a welcome to the user, to let the client knows that it's connected\n\t// RPL_WELCOME\n\tif err := SendIrcNumeric(ctx, 1, ctx.Nick(), fmt.Sprintf(\"Welcome to the %s IRC chat, %s!\", ctx.ServerName, ctx.Nick())); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t}\n\t// RPL_MOTDSTART\n\tif err := SendIrcNumeric(ctx, 375, ctx.Nick(), \"\"); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t}\n\t// RPL_MOTD\n\tmotd := func(s string) {\n\t\tif err := SendIrcNumeric(ctx, 372, ctx.Nick(), s); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t}\n\t// RPL_ISUPPORT\n\tif err := SendIrcNumeric(ctx, 005, ctx.Nick(), \"CHANTYPES=\"+strings.Join(SupportedChannelPrefixes(), \"\")); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t}\n\tmotd(fmt.Sprintf(\"This is an IRC-to-Slack gateway, written by %s <%s>.\", ProjectAuthor, ProjectAuthorEmail))\n\tmotd(fmt.Sprintf(\"More information at %s.\", ProjectURL))\n\tmotd(fmt.Sprintf(\"Slack team name: %s\", ctx.SlackRTM.GetInfo().Team.Name))\n\tmotd(fmt.Sprintf(\"Your user info: \"))\n\tmotd(fmt.Sprintf(\"  Name     : %s\", ctx.User.Name))\n\tmotd(fmt.Sprintf(\"  ID       : %s\", ctx.User.ID))\n\tmotd(fmt.Sprintf(\"  RealName : %s\", ctx.User.RealName))\n\t// RPL_ENDOFMOTD\n\tif err := SendIrcNumeric(ctx, 376, ctx.Nick(), \"\"); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t}\n\n\t// get channels\n\tif err := joinChannels(ctx); err != nil {\n\t\treturn err\n\t}\n\n\tgo eventHandler(ctx, rtm)\n\treturn nil\n}\n\n// IrcCapHandler is called when a CAP command is sent\nfunc IrcCapHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tif len(args) > 1 {\n\t\tif args[0] == \"LS\" {\n\t\t\treply := fmt.Sprintf(\":%s CAP * LS :\\r\\n\", ctx.ServerName)\n\t\t\tif _, err := ctx.Conn.Write([]byte(reply)); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\t} else {\n\t\t\tlog.Debugf(\"Got CAP %v\", args)\n\t\t}\n\t}\n}\n\n// parseMentions parses mentions and converts them to the syntax that\n// Slack will parse, i.e. <@nickname>\nfunc parseMentions(text string) string {\n\ttokens := strings.Split(text, \" \")\n\tfor idx, token := range tokens {\n\t\tif token == \"@here\" {\n\t\t\ttokens[idx] = \"<!here>\"\n\t\t} else if token == \"@channel\" {\n\t\t\ttokens[idx] = \"<!channel>\"\n\t\t} else if token == \"@everyone\" {\n\t\t\ttokens[idx] = \"<!everyone>\"\n\t\t} else if strings.HasPrefix(token, \"@\") {\n\t\t\ttokens[idx] = \"<\" + token + \">\"\n\t\t}\n\t}\n\treturn strings.Join(tokens, \" \")\n}\n\nfunc getTargetTs(channelName string) string {\n\tif !strings.HasPrefix(channelName, \"+\") {\n\t\treturn \"\"\n\t}\n\tchanNameSplit := strings.Split(channelName, \"-\")\n\treturn chanNameSplit[len(chanNameSplit)-1]\n}\n\n// IrcPrivMsgHandler is called when a PRIVMSG command is sent\nfunc IrcPrivMsgHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tvar channelParameter, text string\n\tswitch len(args) {\n\tcase 1:\n\t\tchannelParameter = args[0]\n\t\ttext = trailing\n\tcase 2:\n\t\tchannelParameter = args[0]\n\t\ttext = args[1]\n\tdefault:\n\t\tlog.Warningf(\"Invalid number of parameters for PRIVMSG, want 1 or 2, got %d\", len(args))\n\t}\n\tif channelParameter == \"\" || text == \"\" {\n\t\tlog.Warningf(\"Invalid PRIVMSG command args: %v %v\", args, trailing)\n\t\treturn\n\t}\n\tchannel := ctx.Channels.ByName(channelParameter)\n\ttarget := \"\"\n\tif channel != nil {\n\t\t// known channel\n\t\ttarget = channel.SlackName()\n\t} else {\n\t\t// assume private message\n\t\ttarget = \"@\" + channelParameter\n\t}\n\n\tif strings.HasPrefix(text, \"\\x01ACTION \") && strings.HasSuffix(text, \"\\x01\") {\n\t\t// The Slack API has a bug, where a chat.meMessage is\n\t\t// documented to accept a channel name or ID, but actually\n\t\t// only the channel ID will work. So until this is fixed,\n\t\t// resolve the channel ID for chat.meMessage .\n\t\t// TODO revert this when the bug in the Slack API is fixed\n\t\tkey := target\n\t\tch := ctx.Channels.ByName(key)\n\t\tif ch == nil {\n\t\t\tlog.Warningf(\"Unknown channel ID for %s\", key)\n\t\t\treturn\n\t\t}\n\t\ttarget = ch.SlackName()\n\n\t\t// this is a MeMessage\n\t\t// strip off the ACTION and \\x01 wrapper\n\t\ttext = text[len(\"\\x01ACTION \") : len(text)-1]\n\t\t/*\n\t\t * workaround: I believe that there is an issue with the\n\t\t * slack API for the method chat.meMessage . Until this\n\t\t * is clarified, I will emulate a \"me message\" using a\n\t\t * simple italic formatting for the message.\n\t\t * See https://github.com/insomniacslk/irc-slack/pull/39\n\t\t */\n\t\t// TODO once clarified the issue, restore the\n\t\t//      MsgOptionMeMessage, remove the MsgOptionAsUser,\n\t\t//      and remove the italic text\n\t\t//opts = append(opts, slack.MsgOptionMeMessage())\n\t\ttext = \"_\" + text + \"_\"\n\t}\n\tctx.PostTextMessage(\n\t\ttarget,\n\t\tparseMentions(text),\n\t\tgetTargetTs(channelParameter),\n\t)\n}\n\n// wrapped logger that satisfies the slack.logger interface\ntype loggerWrapper struct {\n\t*logrus.Entry\n}\n\nfunc (l *loggerWrapper) Output(calldepth int, s string) error {\n\tl.Print(s)\n\treturn nil\n}\n\n// custom HTTP client used to set the auth cookie if requested, and only over\n// TLS.\ntype httpClient struct {\n\tc      http.Client\n\tcookie string\n}\n\nfunc (hc httpClient) Do(req *http.Request) (*http.Response, error) {\n\tif hc.cookie != \"\" {\n\t\tlog.Debugf(\"Setting auth cookie\")\n\t\tif strings.ToLower(req.URL.Scheme) == \"https\" {\n\t\t\treq.Header.Add(\"Cookie\", hc.cookie)\n\t\t} else {\n\t\t\tlog.Warning(\"Cookie is set but connection is not HTTPS, skipping\")\n\t\t}\n\t}\n\treturn hc.c.Do(req)\n}\n\n// passwordToTokenAndCookie parses the password specified by the user into a\n// Slack token and optionally a cookie Auth cookies can be specified by\n// appending a \"|\" symbol and the base64-encoded auth cookie to the Slack token.\nfunc passwordToTokenAndCookie(p string) (string, string, error) {\n\tparts := strings.Split(p, \"|\")\n\n\tswitch len(parts) {\n\tcase 1:\n\t\t// XXX should check that the token starts with xoxp- ?\n\t\treturn parts[0], \"\", nil\n\tcase 2:\n\t\tif !strings.HasPrefix(parts[0], \"xoxc-\") {\n\t\t\treturn \"\", \"\", errors.New(\"auth cookie is set, but token does not start with xoxc-\")\n\t\t}\n\t\tif parts[1] == \"\" {\n\t\t\treturn \"\", \"\", errors.New(\"auth cookie is empty\")\n\t\t}\n\t\tif !strings.HasPrefix(parts[1], \"d=\") || !strings.HasSuffix(parts[1], \";\") {\n\t\t\treturn \"\", \"\", errors.New(\"auth cookie must have the format 'd=XXX;'\")\n\t\t}\n\t\treturn parts[0], parts[1], nil\n\tdefault:\n\t\treturn \"\", \"\", fmt.Errorf(\"failed to parse password into token and cookie, got %d components, want 1 or 2\", len(parts))\n\t}\n}\n\nfunc connectToSlack(ctx *IrcContext) error {\n\ttoken, cookie, err := passwordToTokenAndCookie(ctx.SlackAPIKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\tctx.SlackClient = slack.New(\n\t\ttoken,\n\t\tslack.OptionDebug(ctx.SlackDebug),\n\t\tslack.OptionLog(&loggerWrapper{logger.GetLogger(\"slack-api\")}),\n\t\tslack.OptionHTTPClient(&httpClient{cookie: cookie}),\n\t)\n\tif cookie == \"\" {\n\t\t// legacy token\n\t\tctx.usingLegacyToken = true\n\t}\n\trtm := ctx.SlackClient.NewRTM()\n\tctx.SlackRTM = rtm\n\tgo rtm.ManageConnection()\n\tlog.Info(\"Starting Slack client\")\n\t// Wait until the websocket is connected, then print client info\n\tvar info *slack.Info\n\t// FIXME tune the timeout to a value that makes sense\n\ttimeout := 10 * time.Second\n\tstart := time.Now()\n\tfor {\n\t\tif info = rtm.GetInfo(); info != nil {\n\t\t\tbreak\n\t\t}\n\t\tif time.Now().After(start.Add(timeout)) {\n\t\t\treturn fmt.Errorf(\"Connection to Slack timed out after %v\", timeout)\n\t\t}\n\t\ttime.Sleep(100 * time.Millisecond)\n\t}\n\tlog.Info(\"CLIENT INFO:\")\n\tlog.Infof(\"  URL     : %s\", info.URL)\n\tlog.Infof(\"  User    : %+v\", *info.User)\n\tlog.Infof(\"  Team    : %+v\", *info.Team)\n\t// the users cache is not yet populated at this point, so we call the Slack\n\t// API directly.\n\tuser, err := ctx.SlackClient.GetUserInfo(info.User.ID)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"Cannot get info for user %s (ID: %s): %v\", info.User.Name, info.User.ID, err)\n\t}\n\tctx.User = user\n\tctx.RealName = user.RealName\n\t// do not fetch users here, they will be fetched later upon joining channels\n\tif err := ctx.Channels.Fetch(ctx.SlackClient); err != nil {\n\t\tctx.Conn.Close()\n\t\treturn fmt.Errorf(\"Failed to fetch channels: %v\", err)\n\t}\n\treturn IrcAfterLoggingIn(ctx, rtm)\n}\n\n// IrcNickHandler is called when a NICK command is sent\nfunc IrcNickHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tnick := trailing\n\tif len(args) == 1 {\n\t\tnick = args[0]\n\t}\n\tif nick == \"\" {\n\t\tlog.Warningf(\"Invalid NICK command args: %v %v\", args, trailing)\n\t\treturn\n\t}\n\n\tif ctx.SlackClient != nil {\n\t\tif nick != ctx.Nick() {\n\t\t\t// You cannot change nick, so force it back\n\t\t\tif _, err := ctx.Conn.Write([]byte(fmt.Sprintf(\":%s NICK %s\\r\\n\", nick, ctx.Nick()))); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n\n\t// We need the original nick later to change it\n\tctx.OrigName = nick\n\n\t// If we're ready, connect\n\tif ctx.RealName != \"\" && ctx.SlackAPIKey != \"\" {\n\t\tif err := connectToSlack(ctx); err != nil {\n\t\t\tlog.Warningf(\"Cannot connect to Slack: %v\", err)\n\t\t\t// close the IRC connection to the client\n\t\t\tctx.Conn.Close()\n\t\t}\n\t}\n}\n\n// IrcUserHandler is called when a USER command is sent\nfunc IrcUserHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\t// ignore the user-specified username. Will use the Slack ID instead\n\t// TODO get user info and set the real name with that info\n\tctx.RealName = trailing\n\n\t// If we're ready, connect\n\tif ctx.SlackClient == nil && ctx.SlackAPIKey != \"\" && ctx.OrigName != \"\" {\n\t\tif err := connectToSlack(ctx); err != nil {\n\t\t\tlog.Warningf(\"Cannot connect to Slack: %v\", err)\n\t\t\t// close the IRC connection to the client\n\t\t\tctx.Conn.Close()\n\t\t}\n\t}\n}\n\n// IrcPingHandler is called when a PING command is sent\nfunc IrcPingHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tmsg := fmt.Sprintf(\"PONG %s\", strings.Join(args, \" \"))\n\tif trailing != \"\" {\n\t\tmsg += \" :\" + trailing\n\t}\n\tif _, err := ctx.Conn.Write([]byte(msg + \"\\r\\n\")); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t}\n}\n\n// IrcQuitHandler is called when a QUIT command is sent\nfunc IrcQuitHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tctx.Conn.Close()\n}\n\n// IrcModeHandler is called when a MODE command is sent\nfunc IrcModeHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tswitch len(args) {\n\tcase 0:\n\t\tlog.Warningf(\"Invalid call to MODE handler: no arguments passed\")\n\tcase 1:\n\t\t// get mode request. Always no mode (for now)\n\t\tmode := \"+\"\n\t\t// RPL_CHANNELMODEIS\n\t\tif err := SendIrcNumeric(ctx, 324, fmt.Sprintf(\"%s %s %s\", ctx.Nick(), args[0], mode), \"\"); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\tdefault:\n\t\t// more than 1\n\t\t// set mode request. Not handled yet\n\t\t// TODO handle mode set\n\t\t// ERR_UMODEUNKNOWNFLAG\n\t\tif err := SendIrcNumeric(ctx, 501, args[0], fmt.Sprintf(\"Unknown MODE flags %s\", strings.Join(args[1:], \" \"))); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t}\n}\n\n// IrcPassHandler is called when a PASS command is sent\nfunc IrcPassHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tif len(args) != 1 {\n\t\tlog.Warningf(\"Invalid PASS arguments. Arguments are not shown for this method because they may contain Slack tokens or cookies\")\n\t\t// ERR_PASSWDMISMATCH\n\t\tif err := SendIrcNumeric(ctx, 464, \"\", \"Invalid password\"); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\tctx.SlackAPIKey = args[0]\n\tctx.FileHandler.SlackAPIKey = ctx.SlackAPIKey\n\n\t// If we're ready, connect\n\tif ctx.SlackClient == nil && ctx.RealName != \"\" && ctx.OrigName != \"\" {\n\t\tif err := connectToSlack(ctx); err != nil {\n\t\t\tlog.Warningf(\"Cannot connect to Slack: %v\", err)\n\t\t\t// close the IRC connection to the client\n\t\t\tctx.Conn.Close()\n\t\t}\n\t}\n}\n\n// IrcWhoHandler is called when a WHO command is sent\nfunc IrcWhoHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tsendErr := func() {\n\t\tctx.SendUnknownError(\"Invalid WHO command. Syntax: WHO <nickname|channel>\")\n\t}\n\tif len(args) != 1 && len(args) != 2 {\n\t\tsendErr()\n\t\treturn\n\t}\n\ttarget := args[0]\n\tvar rargs, desc string\n\tif HasChannelPrefix(target) {\n\t\tch := ctx.Channels.ByName(target)\n\t\tif ch == nil {\n\t\t\t// ERR_NOSUCHCHANNEL\n\t\t\tif err := SendIrcNumeric(ctx, 403, ctx.Nick(), fmt.Sprintf(\"No such channel %s\", target)); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tfor _, un := range ch.Members {\n\t\t\t// FIXME can we use the cached users?\n\t\t\tu := ctx.Users.ByID(un)\n\t\t\tif u == nil {\n\t\t\t\tlog.Warningf(\"Failed to get info for user name '%s'\", un)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tlog.Infof(\"%+v\", u.Name)\n\t\t\trargs = fmt.Sprintf(\"%s %s %s %s %s %s *\", ctx.Nick(), target, u.ID, ctx.ServerName, ctx.ServerName, u.Name)\n\t\t\tdesc = fmt.Sprintf(\"0 %s\", u.RealName)\n\t\t\t// RPL_WHOREPLY\n\t\t\t// \"<channel> <user> <host> <server> <nick> \\\n\t\t\t//  <H|G>[*][@|+] :<hopcount> <real name>\"\n\t\t\tif err := SendIrcNumeric(ctx, 352, rargs, desc); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\t}\n\t\t// RPL_ENDOFWHO\n\t\t// \"<name> :End of /WHO list\"\n\t\tif err := SendIrcNumeric(ctx, 315, fmt.Sprintf(\"%s %s\", ctx.Nick(), target), \"End of WHO list\"); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\tuser := ctx.GetUserInfoByName(target)\n\tif user == nil {\n\t\t// ERR_NOSUCHNICK\n\t\tif err := SendIrcNumeric(ctx, 401, ctx.Nick(), fmt.Sprintf(\"No such nick %s\", target)); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\t// FIXME get channel\n\trargs = fmt.Sprintf(\"#general %s %s %s %s %s *\", ctx.Nick(), user.ID, ctx.ServerName, ctx.ServerName, user.Name)\n\tdesc = fmt.Sprintf(\"0 %s\", user.RealName)\n\t// RPL_WHOREPLY\n\t// \"<channel> <user> <host> <server> <nick> \\\n\t//  <H|G>[*][@|+] :<hopcount> <real name>\"\n\tif err := SendIrcNumeric(ctx, 352, rargs, desc); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t}\n}\n\n// IrcWhoisHandler is called when a WHOIS command is sent\nfunc IrcWhoisHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tif len(args) != 1 && len(args) != 2 {\n\t\tctx.SendUnknownError(\"Invalid WHOIS command. Syntax: WHOIS <username>\")\n\t\treturn\n\t}\n\tusername := args[0]\n\t// if the second argument is the same as the first, it's a request of WHOIS\n\t// with idle time\n\twithIdleTime := false\n\tif len(args) == 2 && args[0] == args[1] {\n\t\twithIdleTime = true\n\t}\n\tuser := ctx.GetUserInfoByName(username)\n\tif user == nil {\n\t\t// ERR_NOSUCHNICK\n\t\tif err := SendIrcNumeric(ctx, 401, ctx.Nick(), fmt.Sprintf(\"No such nick %s\", username)); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t} else {\n\t\t// RPL_WHOISUSER\n\t\t// \"<nick> <user> <host> * :<real name>\"\n\t\tif err := SendIrcNumeric(ctx, 311, fmt.Sprintf(\"%s %s %s %s *\", ctx.Nick(), username, user.ID, ctx.ServerName), user.RealName); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\t// RPL_WHOISSERVER\n\t\t// \"<nick> <server> :<server info>\"\n\t\tif err := SendIrcNumeric(ctx, 312, fmt.Sprintf(\"%s %s %s\", ctx.Nick(), username, ctx.ServerName), \"irc-slack, https://github.com/insomniacslk/irc-slack\"); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\t// Send additional user status information, abusing the RPL_WHOISSERVER\n\t\t// reply. If there is a better method, please let us know!\n\t\tif user.Profile.StatusText != \"\" || user.Profile.StatusEmoji != \"\" {\n\t\t\tuserStatus := fmt.Sprintf(\"user status: '%s' %s\", user.Profile.StatusText, user.Profile.StatusEmoji)\n\t\t\tif user.Profile.StatusExpiration != 0 {\n\t\t\t\tuserStatus += \" until \" + time.Unix(int64(user.Profile.StatusExpiration), 0).String()\n\t\t\t}\n\t\t\tif err := SendIrcNumeric(ctx, 312, fmt.Sprintf(\"%s %s %s\", ctx.Nick(), username, ctx.ServerName), userStatus); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\t}\n\t\t// RPL_WHOISCHANNELS\n\t\t// \"<nick> :{[@|+]<channel><space>}\"\n\t\tvar channels []string\n\t\tfor chname, ch := range ctx.Channels.AsMap() {\n\t\t\tfor _, u := range ch.Members {\n\t\t\t\tif u == user.ID {\n\t\t\t\t\tchannels = append(channels, chname)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tif err := SendIrcNumeric(ctx, 319, fmt.Sprintf(\"%s %s\", ctx.Nick(), username), strings.Join(channels, \" \")); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\tif withIdleTime {\n\t\t\t// TODO send RPL_WHOISIDLE (317)\n\t\t\t// \"<nick> <integer> :seconds idle\"\n\t\t}\n\t\t// RPL_ENDOFWHOIS\n\t\t// \"<nick> :End of /WHOIS list\"\n\t\tif err := SendIrcNumeric(ctx, 318, fmt.Sprintf(\"%s %s\", ctx.Nick(), username), \":End of /WHOIS list\"); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t}\n}\n\n// IrcJoinHandler is called when a JOIN command is sent\nfunc IrcJoinHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tif len(args) != 1 {\n\t\tctx.SendUnknownError(\"Invalid JOIN command\")\n\t\treturn\n\t}\n\t// Because it is possible for an IRC Client to join multiple channels\n\t// via a multi join (e.g. /join #chan1,#chan2,#chan3) the argument\n\t// needs to be splitted by commas and each channel needs to be joined\n\t// separately.\n\tchannames := strings.Split(args[0], \",\")\n\tfor _, channame := range channames {\n\t\tif strings.HasPrefix(channame, ChannelPrefixMpIM) || strings.HasPrefix(channame, ChannelPrefixThread) {\n\t\t\tlog.Debugf(\"JOIN: ignoring channel `%s`, cannot join multi-party IMs or threads\", channame)\n\t\t\tcontinue\n\t\t}\n\t\tsch, _, _, err := ctx.SlackClient.JoinConversation(channame)\n\t\tif err != nil {\n\t\t\tlog.Warningf(\"Cannot join channel %s: %v\", channame, err)\n\t\t\tcontinue\n\t\t}\n\t\tlog.Infof(\"Joined channel %s\", channame)\n\t\tch := Channel(*sch)\n\t\tif err := joinChannel(ctx, &ch); err != nil {\n\t\t\tlog.Warningf(\"Failed to join channel `%s`: %v\", ch.Name, err)\n\t\t\tcontinue\n\t\t}\n\t}\n}\n\n// IrcPartHandler is called when a PART command is sent\nfunc IrcPartHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tif len(args) != 1 {\n\t\tctx.SendUnknownError(\"Invalid PART command\")\n\t\treturn\n\t}\n\tchanname := StripChannelPrefix(args[0])\n\t// Slack needs the channel ID to leave it, not the channel name. The only\n\t// way to get the channel ID from the name is retrieving the whole channel\n\t// list and finding the one whose name is the one we want to leave\n\tif err := ctx.Channels.Fetch(ctx.SlackClient); err != nil {\n\t\tlog.Warningf(\"Cannot leave channel %s: %v\", channame, err)\n\t\tctx.SendUnknownError(\"Cannot leave channel: %v\", err)\n\t\treturn\n\t}\n\tvar chanID string\n\tfor _, ch := range ctx.Channels.AsMap() {\n\t\tif ch.Name == channame {\n\t\t\tchanID = ch.ID\n\t\t\tlog.Debugf(\"Trying to leave channel: %+v\", ch)\n\t\t\tbreak\n\t\t}\n\t}\n\tif chanID == \"\" {\n\t\t// ERR_USERNOTINCHANNEL\n\t\tif err := SendIrcNumeric(ctx, 441, ctx.Nick(), fmt.Sprintf(\"User is not in channel %s\", channame)); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\treturn\n\t\t}\n\t\tnotInChan, err := ctx.SlackClient.LeaveConversation(chanID)\n\t\tif err != nil {\n\t\t\tlog.Warningf(\"Cannot leave channel %s (id: %s): %v\", channame, chanID, err)\n\t\t\treturn\n\t\t}\n\t\tif notInChan {\n\t\t\t// ERR_USERNOTINCHANNEL\n\t\t\tif err := SendIrcNumeric(ctx, 441, ctx.Nick(), fmt.Sprintf(\"User is not in channel %s\", channame)); err != nil {\n\t\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t\tlog.Debugf(\"Left channel %s\", channame)\n\t\tif _, err := ctx.Conn.Write([]byte(fmt.Sprintf(\":%v PART #%v\\r\\n\", ctx.Mask(), channame))); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t}\n}\n\n// IrcTopicHandler is called when a TOPIC command is sent\nfunc IrcTopicHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tif len(args) < 1 {\n\t\t// ERR_NEEDMOREPARAMS\n\t\tif err := SendIrcNumeric(ctx, 461, ctx.Nick(), \"TOPIC :Not enough parameters\"); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\tchanname := args[0]\n\ttopic := trailing\n\tchannel := ctx.Channels.ByName(channame)\n\tif channel == nil {\n\t\tlog.Warningf(\"IrcTopicHandler: unknown channel %s\", channame)\n\t\treturn\n\t}\n\tnewTopic, err := ctx.SlackClient.SetPurposeOfConversation(channel.ID, topic)\n\tif err != nil {\n\t\tctx.SendUnknownError(\"%s :Cannot set topic: %v\", channame, err)\n\t\treturn\n\t}\n\t// RPL_TOPIC\n\tif err := SendIrcNumeric(ctx, 332, fmt.Sprintf(\"%s :%s\", ctx.Nick(), channame), newTopic.Purpose.Value); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t}\n}\n\n// IrcNamesHandler is called when a NAMES command is sent\nfunc IrcNamesHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {\n\tif len(args) < 1 {\n\t\t// ERR_NEEDMOREPARAMS\n\t\tif err := SendIrcNumeric(ctx, 461, ctx.Nick(), \"NAMES :Not enough parameters\"); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC message: %v\", err)\n\t\t}\n\t\treturn\n\t}\n\tch := ctx.Channels.ByName(args[0])\n\tif ch == nil {\n\t\tctx.SendUnknownError(\"Channel `%s` not found\", args[0])\n\t\treturn\n\t}\n\n\tmembers, err := ChannelMembers(ctx, ch.ID)\n\tif err != nil {\n\t\tjErr := fmt.Errorf(\"Failed to fetch users in channel `%s (channel ID: %s): %v\", ch.Name, ch.ID, err)\n\t\tctx.SendUnknownError(\"%s\", jErr.Error())\n\t\treturn\n\t}\n\tmemberNames := make([]string, 0, len(members))\n\tfor _, m := range members {\n\t\tmemberNames = append(memberNames, m.Name)\n\t}\n\tlog.Printf(\"Found %d members in %s: %v\", len(memberNames), ch.IRCName(), memberNames)\n\t// RPL_NAMREPLY\n\tif len(members) > 0 {\n\t\tif err := SendIrcNumeric(ctx, 353, fmt.Sprintf(\"%s = %s\", ctx.Nick(), ch.IRCName()), strings.Join(memberNames, \" \")); err != nil {\n\t\t\tlog.Warningf(\"Failed to send IRC NAMREPLY message: %v\", err)\n\t\t}\n\t}\n\t// RPL_ENDOFNAMES\n\tif err := SendIrcNumeric(ctx, 366, fmt.Sprintf(\"%s %s\", ctx.Nick(), ch.IRCName()), \"End of NAMES list\"); err != nil {\n\t\tlog.Warningf(\"Failed to send IRC ENDOFNAMES message: %v\", err)\n\t}\n}\n"
  },
  {
    "path": "pkg/ircslack/logger.go",
    "content": "package ircslack\n\nimport \"github.com/coredhcp/coredhcp/logger\"\n\nvar log = logger.GetLogger(\"ircslack\")\n"
  },
  {
    "path": "pkg/ircslack/server.go",
    "content": "package ircslack\n\nimport (\n\t\"bufio\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/slack-go/slack\"\n)\n\n// Server is the server object that exposes the Slack API with an IRC interface.\ntype Server struct {\n\tName                 string\n\tLocalAddr            net.Addr\n\tListener             net.Listener\n\tSlackAPIKey          string\n\tSlackDebug           bool\n\tChunkSize            int\n\tFileDownloadLocation string\n\tFileProxyPrefix      string\n\tPagination           int\n\tTLSConfig            *tls.Config\n}\n\n// Start runs the IRC server\nfunc (s Server) Start() error {\n\tvar err error\n\tif s.TLSConfig != nil {\n\t\ts.Listener, err = tls.Listen(\"tcp\", s.LocalAddr.String(), s.TLSConfig)\n\t} else {\n\t\ts.Listener, err = net.Listen(\"tcp\", s.LocalAddr.String())\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer s.Listener.Close()\n\tlog.Infof(\"Listening on %v\", s.LocalAddr)\n\tfor {\n\t\tconn, err := s.Listener.Accept()\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"Error accepting: %v\", err)\n\t\t}\n\t\tgo s.HandleRequest(conn)\n\t}\n}\n\n// HandleRequest handle IRC client connections\nfunc (s Server) HandleRequest(conn net.Conn) {\n\tdefer conn.Close()\n\treader := bufio.NewReader(conn)\n\tfor {\n\t\tline, err := reader.ReadString('\\n')\n\t\tif err != nil {\n\t\t\t// clean up this client's state\n\t\t\tdelete(UserContexts, conn.RemoteAddr())\n\t\t\tif err == io.EOF {\n\t\t\t\tlog.Warningf(\"Client %v disconnected\", conn.RemoteAddr())\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tlog.Warningf(\"Error handling connection from %v: %v\", conn.RemoteAddr(), err)\n\t\t\tbreak\n\t\t}\n\t\ts.HandleMsg(conn, string(line))\n\t}\n}\n\n// HandleMsg handles raw IRC messages\nfunc (s *Server) HandleMsg(conn net.Conn, msg string) {\n\tif strings.HasPrefix(msg, \"PASS \") {\n\t\tlog.Debugf(\"%v: PASS ***** (redacted for privacy)\", conn.RemoteAddr())\n\t} else {\n\t\tlog.Debugf(\"%v: %v\", conn.RemoteAddr(), msg)\n\t}\n\tif len(msg) < 1 {\n\t\tlog.Warningf(\"Invalid message: '%v'\", msg)\n\t\treturn\n\t}\n\tvar (\n\t\tprefix, data string\n\t)\n\tif msg[0] == ':' {\n\t\tprefix = strings.SplitN(msg[1:], \" \", 1)[0]\n\t\tdata = msg[len(prefix)+1:]\n\t} else {\n\t\tprefix = \"\"\n\t\tdata = msg\n\t}\n\tif !strings.HasSuffix(data, \"\\r\\n\") {\n\t\tlog.Warning(\"Invalid data: not terminated with <CR><LF>\")\n\t\treturn\n\t}\n\tdata = data[:len(data)-2]\n\n\ttokens := strings.Split(data, \" \")\n\tcmd := tokens[0]\n\targs := tokens[1:]\n\tvar trailing string\n\tfor idx, arg := range args {\n\t\tif strings.HasPrefix(arg, \":\") {\n\t\t\ttrailing = strings.Join(args[idx:], \" \")[1:]\n\t\t\targs = args[:idx]\n\t\t\tbreak\n\t\t}\n\t}\n\thandler, ok := IrcCommandHandlers[cmd]\n\tif !ok {\n\t\tlog.Warningf(\"No handler found for %v\", cmd)\n\t\treturn\n\t}\n\tctx, ok := UserContexts[conn.RemoteAddr()]\n\tif !ok || ctx == nil {\n\t\tctx = &IrcContext{\n\t\t\tConn:              conn,\n\t\t\tServerName:        s.Name,\n\t\t\tSlackAPIKey:       s.SlackAPIKey,\n\t\t\tSlackDebug:        s.SlackDebug,\n\t\t\tChunkSize:         s.ChunkSize,\n\t\t\tpostMessage:       make(chan SlackPostMessage),\n\t\t\tconversationCache: make(map[string]*slack.Channel),\n\t\t\tFileHandler: &FileHandler{\n\t\t\t\tSlackAPIKey:          s.SlackAPIKey,\n\t\t\t\tFileDownloadLocation: s.FileDownloadLocation,\n\t\t\t\tProxyPrefix:          s.FileProxyPrefix,\n\t\t\t},\n\t\t\tUsers:    NewUsers(s.Pagination),\n\t\t\tChannels: NewChannels(s.Pagination),\n\t\t}\n\t\tgo ctx.Start()\n\t\tUserContexts[conn.RemoteAddr()] = ctx\n\t}\n\thandler(ctx, prefix, cmd, args, trailing)\n}\n"
  },
  {
    "path": "pkg/ircslack/users.go",
    "content": "package ircslack\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/slack-go/slack\"\n)\n\n// Users wraps the user list with convenient operations and cache.\ntype Users struct {\n\tusers      map[string]slack.User\n\tmu         sync.Mutex\n\tpagination int\n}\n\n// NewUsers creates a new Users object.\nfunc NewUsers(pagination int) *Users {\n\treturn &Users{\n\t\tusers:      make(map[string]slack.User),\n\t\tpagination: pagination,\n\t}\n}\n\n// FetchByIDs fetches the users with the specified IDs and updates the internal\n// user mapping.\nfunc (u *Users) FetchByIDs(client *slack.Client, skipCache bool, userIDs ...string) ([]slack.User, error) {\n\tvar (\n\t\ttoRetrieve       []string\n\t\talreadyRetrieved []slack.User\n\t)\n\n\tif !skipCache {\n\t\tu.mu.Lock()\n\t\tfor _, uid := range userIDs {\n\t\t\tif u, ok := u.users[uid]; !ok {\n\t\t\t\ttoRetrieve = append(toRetrieve, uid)\n\t\t\t} else {\n\t\t\t\talreadyRetrieved = append(alreadyRetrieved, u)\n\t\t\t}\n\t\t}\n\t\tu.mu.Unlock()\n\t\tlog.Debugf(\"Fetching information for %d users out of %d (%d already in cache)\", len(toRetrieve), len(userIDs), len(userIDs)-len(toRetrieve))\n\t} else {\n\t\ttoRetrieve = userIDs\n\t}\n\tchunkSize := 1000\n\tallFetchedUsers := make([]slack.User, 0, len(userIDs))\n\tfor i := 0; i < len(toRetrieve); i += chunkSize {\n\t\tupperLimit := i + chunkSize\n\t\tif upperLimit > len(toRetrieve) {\n\t\t\tupperLimit = len(toRetrieve)\n\t\t}\n\t\tfor {\n\t\t\tattempt := 0\n\t\t\tif attempt >= MaxSlackAPIAttempts {\n\t\t\t\treturn nil, fmt.Errorf(\"Users.FetchByIDs: exceeded the maximum number of attempts (%d) with the Slack API\", MaxSlackAPIAttempts)\n\t\t\t}\n\t\t\tlog.Debugf(\"Fetching %d users of %d, attempt %d of %d\", len(toRetrieve), len(userIDs), attempt+1, MaxSlackAPIAttempts)\n\t\t\tslackUsers, err := client.GetUsersInfo(toRetrieve[i:upperLimit]...)\n\t\t\tif err != nil {\n\t\t\t\tif rlErr, ok := err.(*slack.RateLimitedError); ok {\n\t\t\t\t\t// we were rate-limited. Let's wait the recommended delay\n\t\t\t\t\tlog.Warningf(\"Hit Slack API rate limiter. Waiting %v\", rlErr.RetryAfter)\n\t\t\t\t\ttime.Sleep(rlErr.RetryAfter)\n\t\t\t\t\tattempt++\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif len(*slackUsers) != len(toRetrieve[i:upperLimit]) {\n\t\t\t\tlog.Warningf(\"Tried to fetch %d users but only got %d\", len(toRetrieve[i:upperLimit]), len(*slackUsers))\n\t\t\t}\n\t\t\tallFetchedUsers = append(allFetchedUsers, *slackUsers...)\n\t\t\t// also update the local users map\n\t\t\tu.mu.Lock()\n\t\t\tfor _, user := range *slackUsers {\n\t\t\t\tu.users[user.ID] = user\n\t\t\t}\n\t\t\tu.mu.Unlock()\n\t\t\tbreak\n\t\t}\n\t}\n\tallUsers := append(alreadyRetrieved, allFetchedUsers...)\n\tif len(userIDs) != len(allUsers) {\n\t\treturn allFetchedUsers, fmt.Errorf(\"Found %d users but %d were requested\", len(allUsers), len(userIDs))\n\t}\n\treturn allUsers, nil\n}\n\n// Fetch retrieves all the users on a given Slack team. The Slack client has to\n// be valid and connected.\nfunc (u *Users) Fetch(client *slack.Client) ([]slack.User, error) {\n\tlog.Infof(\"Fetching all users, might take a while on large Slack teams\")\n\tvar opts []slack.GetUsersOption\n\tif u.pagination > 0 {\n\t\tlog.Debugf(\"Setting user pagination to %d\", u.pagination)\n\t\topts = append(opts, slack.GetUsersOptionLimit(u.pagination))\n\t}\n\tup := client.GetUsersPaginated(opts...)\n\tvar (\n\t\terr   error\n\t\tctx   = context.Background()\n\t\tusers = make(map[string]slack.User)\n\t)\n\tstart := time.Now()\n\tvar allFetchedUsers []slack.User\n\tfor err == nil {\n\t\tup, err = up.Next(ctx)\n\t\tif err == nil {\n\t\t\tlog.Debugf(\"Retrieved %d users (current total is %d)\", len(up.Users), len(users))\n\t\t\tfor _, u := range up.Users {\n\t\t\t\tusers[u.ID] = u\n\t\t\t}\n\t\t\tallFetchedUsers = append(allFetchedUsers, up.Users...)\n\t\t} else if rateLimitedError, ok := err.(*slack.RateLimitedError); ok {\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\terr = ctx.Err()\n\t\t\tcase <-time.After(rateLimitedError.RetryAfter):\n\t\t\t\terr = nil\n\t\t\t}\n\t\t}\n\t}\n\tlog.Infof(\"Retrieved %d users in %s\", len(users), time.Since(start))\n\terr = up.Failure(err)\n\tif err != nil {\n\t\tlog.Warningf(\"Failed to get users: %v\", err)\n\t}\n\tu.mu.Lock()\n\tu.users = users\n\tu.mu.Unlock()\n\treturn allFetchedUsers, nil\n}\n\n// Count returns the number of users. This method must be called after `Fetch`.\nfunc (u *Users) Count() int {\n\treturn len(u.users)\n}\n\n// ByID retrieves a user by its Slack ID.\nfunc (u *Users) ByID(id string) *slack.User {\n\tu.mu.Lock()\n\tdefer u.mu.Unlock()\n\tfor _, u := range u.users {\n\t\tif u.ID == id {\n\t\t\treturn &u\n\t\t}\n\t}\n\treturn nil\n}\n\n// ByName retrieves a user by its Slack name.\nfunc (u *Users) ByName(name string) *slack.User {\n\tu.mu.Lock()\n\tdefer u.mu.Unlock()\n\tfor _, u := range u.users {\n\t\tif u.Name == name {\n\t\t\treturn &u\n\t\t}\n\t}\n\treturn nil\n}\n\n// IDsToNames returns a list of user names from the given IDs. The\n// returned list could be shorter if there are invalid user IDs.\n// Warning: this method is probably only useful for NAMES commands\n// where a non-exact mapping is acceptable.\nfunc (u *Users) IDsToNames(userIDs ...string) []string {\n\tu.mu.Lock()\n\tdefer u.mu.Unlock()\n\tnames := make([]string, 0)\n\tfor _, uid := range userIDs {\n\t\tif u, ok := u.users[uid]; ok {\n\t\t\tnames = append(names, u.Name)\n\t\t} else {\n\t\t\tlog.Warningf(\"IDsToNames: unknown user ID %s\", uid)\n\t\t}\n\t}\n\treturn names\n}\n"
  },
  {
    "path": "pkg/ircslack/users_test.go",
    "content": "package ircslack\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/slack-go/slack\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUsersNewUsers(t *testing.T) {\n\tu := NewUsers(0)\n\trequire.NotNil(t, u)\n\tassert.NotNil(t, u.users)\n\tassert.Equal(t, 0, u.pagination)\n\n\tu = NewUsers(200)\n\trequire.NotNil(t, u)\n\tassert.NotNil(t, u.users)\n\tassert.Equal(t, 200, u.pagination)\n}\n\ntype fakeErrorUsersPaginationComplete struct{}\n\ntype fakeUsersResponse struct {\n\tMembers []slack.User\n\tUser    slack.User\n}\n\nfunc (f fakeErrorUsersPaginationComplete) Error() string {\n\treturn \"pagination complete\"\n}\n\ntype fakeSlackHTTPClient struct{}\n\nfunc (c fakeSlackHTTPClient) Do(req *http.Request) (*http.Response, error) {\n\tswitch req.URL.Path {\n\tcase \"/api/users.list\":\n\t\t// reply as per https://api.slack.com/methods/users.list\n\t\tdata := []byte(`{\"ok\": true, \"members\": [{\"id\": \"UABCD\", \"name\": \"insomniac\"}], \"response_metadata\": {\"next_cursor\": \"\"}}`)\n\t\treturn &http.Response{\n\t\t\tStatus:     \"200 OK\",\n\t\t\tStatusCode: 200,\n\t\t\tProto:      \"HTTP/1.1\",\n\t\t\tProtoMajor: 1,\n\t\t\tProtoMinor: 1,\n\t\t\tBody:       ioutil.NopCloser(bytes.NewBuffer(data)),\n\t\t}, nil\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"testing: http client URL not supported: %s\", req.URL)\n\t}\n}\n\nfunc TestUsersFetch(t *testing.T) {\n\tclient := slack.New(\"test-token\", slack.OptionHTTPClient(fakeSlackHTTPClient{}))\n\tusers := NewUsers(10)\n\tfetched, err := users.Fetch(client)\n\trequire.NoError(t, err)\n\tassert.Equal(t, 1, users.Count())\n\tassert.Equal(t, 1, len(fetched))\n}\n\nfunc TestUsersById(t *testing.T) {\n\tclient := slack.New(\"test-token\", slack.OptionHTTPClient(fakeSlackHTTPClient{}))\n\tusers := NewUsers(10)\n\t_, err := users.Fetch(client)\n\trequire.NoError(t, err)\n\tu := users.ByID(\"UABCD\")\n\trequire.NotNil(t, u)\n\tassert.Equal(t, \"UABCD\", u.ID)\n\tassert.Equal(t, \"insomniac\", u.Name)\n}\n\nfunc TestUsersByName(t *testing.T) {\n\tclient := slack.New(\"test-token\", slack.OptionHTTPClient(fakeSlackHTTPClient{}))\n\tusers := NewUsers(10)\n\t_, err := users.Fetch(client)\n\trequire.NoError(t, err)\n\tu := users.ByName(\"insomniac\")\n\trequire.NotNil(t, u)\n\tassert.Equal(t, \"UABCD\", u.ID)\n\tassert.Equal(t, \"insomniac\", u.Name)\n}\n\nfunc TestUsersIDsToNames(t *testing.T) {\n\tclient := slack.New(\"test-token\", slack.OptionHTTPClient(fakeSlackHTTPClient{}))\n\tusers := NewUsers(10)\n\t_, err := users.Fetch(client)\n\trequire.NoError(t, err)\n\tnames := users.IDsToNames(\"UABCD\")\n\tassert.Equal(t, []string{\"insomniac\"}, names)\n}\n"
  },
  {
    "path": "pkg/ircslack/wordwrap.go",
    "content": "package ircslack\n\nimport (\n\t\"strings\"\n)\n\n// WordWrap wraps the given words up to the maximum specified length.\n// If a single word is longer than the max length, it is truncated.\nfunc WordWrap(allWords []string, maxLen int) []string {\n\tvar (\n\t\tlines  []string\n\t\tcurLen int\n\t\twords  []string\n\t)\n\tfor _, word := range allWords {\n\t\t// curLen + len(words) + len(word) is the length of the current\n\t\t// line including spaces\n\t\tif curLen+len(words)+len(word) > maxLen {\n\t\t\t// we have our line. That does not include the current word\n\t\t\tlines = append(lines, strings.Join(words, \" \"))\n\t\t\t// reset the current line, add the current word\n\t\t\twords = []string{word}\n\t\t\tcurLen = len(word)\n\t\t} else {\n\t\t\twords = append(words, word)\n\t\t\tcurLen += len(word)\n\t\t}\n\t}\n\tif len(words) > 0 {\n\t\t// there's one last line to add\n\t\tlines = append(lines, strings.Join(words, \" \"))\n\t}\n\tfor idx, line := range lines {\n\t\tif len(line) > maxLen {\n\t\t\t// truncate\n\t\t\tlines[idx] = line[:maxLen]\n\t\t}\n\t}\n\treturn lines\n}\n"
  },
  {
    "path": "pkg/ircslack/wordwrap_test.go",
    "content": "package ircslack\n\nimport (\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar fox = \"The quick brown fox jumps over the lazy dog\"\n\nfunc TestWordWrapMultiLine(t *testing.T) {\n\twords := strings.Fields(fox)\n\twrapped := WordWrap(words, 10)\n\trequire.Equal(t, 5, len(wrapped))\n\trequire.Equal(t, \"The quick\", wrapped[0])\n\trequire.Equal(t, \"brown fox\", wrapped[1])\n\trequire.Equal(t, \"jumps over\", wrapped[2])\n\trequire.Equal(t, \"the lazy\", wrapped[3])\n\trequire.Equal(t, \"dog\", wrapped[4])\n}\n\nfunc TestWordWrapSingleLine(t *testing.T) {\n\twords := strings.Fields(fox)\n\twrapped := WordWrap(words, 100)\n\trequire.Equal(t, 1, len(wrapped))\n\trequire.Equal(t, fox, wrapped[0])\n}\n\nfunc TestWordWrapTruncate(t *testing.T) {\n\twords := strings.Fields(fox)\n\twrapped := WordWrap(words, 3)\n\trequire.Equal(t, 9, len(wrapped))\n\trequire.Equal(t, \"The\", wrapped[0])\n\trequire.Equal(t, \"qui\", wrapped[1])\n\trequire.Equal(t, \"bro\", wrapped[2])\n\trequire.Equal(t, \"fox\", wrapped[3])\n\trequire.Equal(t, \"jum\", wrapped[4])\n\trequire.Equal(t, \"ove\", wrapped[5])\n\trequire.Equal(t, \"the\", wrapped[6])\n\trequire.Equal(t, \"laz\", wrapped[7])\n\trequire.Equal(t, \"dog\", wrapped[8])\n}\n"
  },
  {
    "path": "tools/autotoken/main.go",
    "content": "// autotoken retrieves a Slack token and cookie using your Slack team\n// credentials.\npackage main\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/chromedp/cdproto/runtime\"\n\t\"github.com/chromedp/cdproto/storage\"\n\t\"github.com/chromedp/chromedp\"\n\t\"github.com/spf13/pflag\"\n)\n\nvar (\n\tflagDebug       = pflag.BoolP(\"debug\", \"d\", false, \"Enable debug log\")\n\tflagShowBrowser = pflag.BoolP(\"show-browser\", \"b\", false, \"show browser, useful for debugging\")\n\tflagChromePath  = pflag.StringP(\"chrome-path\", \"c\", \"\", \"Custom path for chrome browser\")\n\tflagTimeout     = pflag.DurationP(\"timeout\", \"t\", 5*time.Minute, \"Timeout\")\n)\n\nfunc main() {\n\tusage := func() {\n\t\tfmt.Fprintf(os.Stderr, \"autotoken: log into slack team and get token and cookie.\\n\\n\")\n\t\tfmt.Fprintf(os.Stderr, \"Usage: %s [-d|--debug] [-m|--mfa <token>] [-g|--gdpr] teamname[.slack.com]\\n\\n\", os.Args[0])\n\t\tpflag.PrintDefaults()\n\t\tos.Exit(1)\n\t}\n\tpflag.Usage = usage\n\tpflag.Parse()\n\tif len(pflag.Args()) < 1 {\n\t\tusage()\n\t}\n\tteam := pflag.Arg(0)\n\n\ttimeout := *flagTimeout\n\ttoken, cookie, err := fetchCredentials(context.TODO(), team, timeout, *flagDebug, *flagChromePath)\n\tif err != nil {\n\t\tlog.Fatalf(\"Failed to fetch credentials for team `%s`: %v\", team, err)\n\t}\n\n\tfmt.Printf(\"%s|%s\\n\", token, cookie)\n}\n\n// fetchCredentials fetches Slack token and cookie for a given team.\nfunc fetchCredentials(ctx context.Context, team string, timeout time.Duration, doDebug bool, chromePath string) (string, string, error) {\n\tif !strings.HasSuffix(team, \".slack.com\") {\n\t\tteam += \".slack.com\"\n\t}\n\n\tvar cancel func()\n\tctx, cancel = context.WithTimeout(ctx, timeout)\n\tdefer cancel()\n\n\t// show browser\n\tvar allocatorOpts []chromedp.ExecAllocatorOption\n\tif *flagShowBrowser {\n\t\tallocatorOpts = append(allocatorOpts, chromedp.NoFirstRun, chromedp.NoDefaultBrowserCheck)\n\t} else {\n\t\tallocatorOpts = append(allocatorOpts, chromedp.Headless)\n\t}\n\tif chromePath != \"\" {\n\t\tallocatorOpts = append(allocatorOpts, chromedp.ExecPath(chromePath))\n\t}\n\tctx, cancel = chromedp.NewExecAllocator(ctx, allocatorOpts...)\n\tdefer cancel()\n\n\tvar opts []chromedp.ContextOption\n\tif doDebug {\n\t\topts = append(opts, chromedp.WithDebugf(log.Printf))\n\t}\n\n\tctx, cancel = chromedp.NewContext(ctx, opts...)\n\tdefer cancel()\n\n\tfmt.Fprintf(os.Stderr, \"Fetching token and cookie for %s \\n\", team)\n\t// run chromedp tasks\n\treturn extractTokenAndCookie(ctx, team)\n}\n\n// extractTokenAndCookie extracts Slack token and cookie from an existing\n// context.\nfunc extractTokenAndCookie(ctx context.Context, team string) (string, string, error) {\n\tteamURL := \"https://\" + team\n\tvar token, cookie string\n\ttasks := chromedp.Tasks{\n\t\tchromedp.Navigate(teamURL),\n\t\tchromedp.WaitVisible(\".p-workspace__primary_view_contents\"),\n\t\tchromedp.ActionFunc(func(ctx context.Context) error {\n\t\t\tv, exp, err := runtime.Evaluate(`q=JSON.parse(localStorage.localConfig_v2)[\"teams\"]; q[Object.keys(q)[0]][\"token\"]`).Do(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif exp != nil {\n\t\t\t\treturn exp\n\t\t\t}\n\t\t\tif err := json.Unmarshal(v.Value, &token); err != nil {\n\t\t\t\treturn fmt.Errorf(\"failed to unmarshal token: %v\", err)\n\t\t\t}\n\t\t\treturn nil\n\t\t}),\n\t\tchromedp.ActionFunc(func(ctx context.Context) error {\n\t\t\tcookies, err := storage.GetCookies().Do(ctx)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, c := range cookies {\n\t\t\t\tif c.Name == \"d\" {\n\t\t\t\t\tcookie = fmt.Sprintf(\"d=%s;\", c.Value)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}),\n\t}\n\tif err := chromedp.Run(ctx, tasks); err != nil {\n\t\treturn \"\", \"\", err\n\t}\n\treturn token, cookie, nil\n}\n"
  },
  {
    "path": "tools/slackapp/main.go",
    "content": "package main\n\n/* Slack Oauth app built according to\n * https://api.slack.com/authentication/oauth-v2\n */\n\nimport (\n\t\"encoding/json\"\n\t\"flag\"\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strings\"\n\n\tlog \"github.com/sirupsen/logrus\"\n)\n\nvar (\n\t// irc-slack app client ID, see https://api.slack.com/apps/\n\tclientID     = os.Getenv(\"SLACK_APP_CLIENT_ID\")\n\tclientSecret = os.Getenv(\"SLACK_APP_CLIENT_SECRET\")\n)\n\nfunc httpStatus(w http.ResponseWriter, r *http.Request, statusCode int, fmtstr string, args ...interface{}) {\n\tw.WriteHeader(statusCode)\n\tmsg := fmt.Sprintf(fmtstr, args...)\n\tfullmsg := fmt.Sprintf(\"%d - %s\\n%s\", statusCode, http.StatusText(statusCode), msg)\n\tif _, err := w.Write([]byte(fullmsg)); err != nil {\n\t\tlog.Warningf(\"Cannot write response: %v\", err)\n\t}\n}\n\ntype slackChallenge struct {\n\tToken, Challenge, Type string\n}\n\nfunc handleSlackChallenge(w http.ResponseWriter, r *http.Request) {\n\tdata, err := ioutil.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Infof(\"Cannot read body: %v\", err)\n\t\thttpStatus(w, r, 500, \"\")\n\t\treturn\n\t}\n\tvar sc slackChallenge\n\tif err := json.Unmarshal(data, &sc); err != nil {\n\t\tlog.Infof(\"Cannot unmarshal JSON: %v\", err)\n\t\thttpStatus(w, r, 400, \"\")\n\t\treturn\n\t}\n\tif _, err := w.Write([]byte(sc.Challenge)); err != nil {\n\t\tlog.Warningf(\"Failed to write response: %v\", err)\n\t}\n}\n\nfunc handleSlackAuth(w http.ResponseWriter, r *http.Request) {\n\tcode := r.URL.Query()[\"code\"]\n\tif len(code) < 1 {\n\t\tlog.Info(\"Missing \\\"code\\\" parameter in request\")\n\t\thttpStatus(w, r, 400, \"\")\n\t\treturn\n\t}\n\t// get token from Slack\n\tform := url.Values{}\n\tform.Add(\"code\", code[0])\n\tform.Add(\"client_id\", clientID)\n\tform.Add(\"client_secret\", clientSecret)\n\taccessURL := \"https://slack.com/api/oauth.access\"\n\n\tclient := http.Client{}\n\treq, err := http.NewRequest(\"POST\", accessURL, strings.NewReader(form.Encode()))\n\tif err != nil {\n\t\tlog.Infof(\"Failed to build request for Slack auth API: %v\", err)\n\t\thttpStatus(w, r, 500, \"\")\n\t\treturn\n\t}\n\treq.Header.Add(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\tlog.Infof(\"Failed to request token to Slack auth API: %v\", err)\n\t\thttpStatus(w, r, 500, \"\")\n\t\treturn\n\t}\n\tdefer resp.Body.Close()\n\tdata, err := ioutil.ReadAll(resp.Body)\n\tif err != nil {\n\t\tlog.Infof(\"Failed to read body from Slack auth API response: %v\", err)\n\t\thttpStatus(w, r, 500, \"\")\n\t\treturn\n\t}\n\t// parse the response message\n\t// Documented at https://api.slack.com/methods/oauth.access .\n\t// There are other fields, but we don't care about them here.\n\ttype authmsg struct {\n\t\tOk           bool    `json:\"ok\"`\n\t\tError        string  `json:\"error\"`\n\t\tAccessToken  string  `json:\"access_token\"`\n\t\tTeamName     string  `json:\"team_name\"`\n\t\tTeamID       string  `json:\"team_id\"`\n\t\tScope        string  `json:\"scope\"`\n\t\tEnterpriseID *string `json:\"enterprise_id,omitempty\"`\n\t}\n\tvar msg authmsg\n\tif err := json.Unmarshal(data, &msg); err != nil {\n\t\tlog.Infof(\"Failed to unmarshal API response response: %v\", err)\n\t\thttpStatus(w, r, 500, \"\")\n\t}\n\tif !msg.Ok {\n\t\tlog.Infof(\"Got error response from auth API: %s\", msg.Error)\n\t\thttpStatus(w, r, 500, \"\")\n\t\treturn\n\t}\n\tindented, err := json.MarshalIndent(msg, \"\", \"    \")\n\tif err != nil {\n\t\tlog.Infof(\"Cannot re-marshal API response with indentation: %v\", err)\n\t\thttpStatus(w, r, 500, \"\")\n\t\treturn\n\t}\n\thttpStatus(w, r, 200, \"%s\", string(indented))\n}\n\nfunc main() {\n\tflag.Parse()\n\taddr := \":2020\"\n\tif flag.Arg(0) != \"\" {\n\t\taddr = flag.Arg(0)\n\t}\n\tif clientID == \"\" {\n\t\tlog.Fatalf(\"SLACK_APP_CLIENT_ID is empty or not set\")\n\t}\n\tif clientSecret == \"\" {\n\t\tlog.Fatalf(\"SLACK_APP_CLIENT_SECRET is empty or not set\")\n\t}\n\thttp.HandleFunc(\"/irc-slack/challenge/\", handleSlackChallenge)\n\thttp.HandleFunc(\"/irc-slack/auth/\", handleSlackAuth)\n\tlog.Printf(\"Listening on %s\", addr)\n\tlog.Fatal(http.ListenAndServe(addr, nil))\n}\n"
  }
]