Full Code of insomniacslk/irc-slack for AI

master c8acefd1f7e4 cached
30 files
116.8 KB
38.2k tokens
130 symbols
1 requests
Download .txt
Repository: insomniacslk/irc-slack
Branch: master
Commit: c8acefd1f7e4
Files: 30
Total size: 116.8 KB

Directory structure:
gitextract_xhv39y9c/

├── .dockerignore
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── docker.yml
│       └── tests.yml
├── .gitignore
├── .stickler.yml
├── Dockerfile
├── Dockerfile.autotoken
├── LICENSE
├── README.md
├── cmd/
│   └── irc-slack/
│       ├── Makefile
│       └── main.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── pkg/
│   └── ircslack/
│       ├── channel.go
│       ├── channels.go
│       ├── channels_test.go
│       ├── event_handler.go
│       ├── file_handler.go
│       ├── irc_context.go
│       ├── irc_server.go
│       ├── logger.go
│       ├── server.go
│       ├── users.go
│       ├── users_test.go
│       ├── wordwrap.go
│       └── wordwrap_test.go
└── tools/
    ├── autotoken/
    │   └── main.go
    └── slackapp/
        └── main.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .dockerignore
================================================
Dockerfile
.travis
.dockerignore
coverage.txt


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "gomod"
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/docker.yml
================================================
name: Docker push

on:
  push:
    branches:
      - master


jobs:
  docker_publish:
    runs-on: 1.25
    env:
      DOCKER_USER: ${{ secrets.DOCKERHUB_USERNAME }}
      DOCKER_PASS: ${{ secrets.DOCKERHUB_PASSWORD }}
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 2
      - name: Login to Docker hub
        run: docker login -u $DOCKER_USER -p $DOCKER_PASS
      - name: Build image
        run: |
          pwd
          ls -l
          docker build -t ${{ github.repository }} -f Dockerfile .
      - name: Publish image
        run: |
          docker tag ${{ github.repository }} ${{ github.repository }}:latest
          docker push ${{ github.repository }}


================================================
FILE: .github/workflows/tests.yml
================================================
name: Tests

on: [push, pull_request]


jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go: ['1.25']
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 2
          # clone in the gopath
          path: src/github.com/${{ github.repository }}
      - uses: actions/setup-go@v2
        with:
          stable: false
          go-version: ${{ matrix.go }}
      - run: |
          echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV
      - run: |
          cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }}/cmd/irc-slack
          make
          ./irc-slack --version
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        go: ['1.25']
    steps:
      - uses: actions/checkout@v2
        with:
          fetch-depth: 2
          # clone in the gopath
          path: src/github.com/${{ github.repository }}
      - uses: actions/setup-go@v2
        with:
          stable: false
          go-version: ${{ matrix.go }}
      - run: |
          echo "GOPATH=$GITHUB_WORKSPACE" >> $GITHUB_ENV
      - run: |
          cd $GITHUB_WORKSPACE/src/github.com/${{ github.repository }}
          go get -v -t ./...
          echo "" > coverage.txt
          for d in $(go list ./...); do
              go test -v -race -coverprofile=profile.out -covermode=atomic "${d}"
              if [ -f profile.out ]; then
                cat profile.out >> coverage.txt
                rm profile.out
              fi
          done
          bash <(curl -s https://codecov.io/bash) -c -f coverage.txt -F unittest


================================================
FILE: .gitignore
================================================
coverage.txt
cmd/irc-slack/irc-slack


================================================
FILE: .stickler.yml
================================================
linters:
  golint:
    fixer: true
fixers:
  enable: true


================================================
FILE: Dockerfile
================================================
############################
# STEP 1 build executable binary
############################
FROM golang:1.23-alpine AS builder

LABEL BUILD="docker build -t insomniacslk/irc-slack -f Dockerfile ."
LABEL RUN="docker run --rm -p 6666:6666 -it insomniacslk/irc-slack"

# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache git bash make
COPY . $GOPATH/src/github.com/insomniacslk/irc-slack
ENV GO111MODULE=on
WORKDIR $GOPATH/src/github.com/insomniacslk/irc-slack/cmd/irc-slack
# Build the binary.
RUN make
RUN cp irc-slack /go/bin

############################
# STEP 2 build a small image
############################
FROM scratch
# Copy the ssl certs so we can talk to slack
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
# Copy our static executable.
COPY --from=builder /go/bin/irc-slack /go/bin/irc-slack
ENV PATH="/go/bin:$PATH"
# Run the irc-slack binary.
CMD ["/go/bin/irc-slack", "-H", "0.0.0.0"]


================================================
FILE: Dockerfile.autotoken
================================================
############################
# STEP 1 build executable binary
############################
FROM golang:1.16-alpine AS builder

LABEL BUILD="docker build -t insomniacslk/irc-slack/tools-autotoken -f Dockerfile.autotoken ."
LABEL RUN="docker run --rm -it insomniacslk/irc-slack/tools-autotoken"

# Install git.
# Git is required for fetching the dependencies.
RUN apk update && apk add --no-cache --purge git bash chromium
COPY . $GOPATH/src/github.com/insomniacslk/irc-slack/
ENV GO111MODULE=on
WORKDIR $GOPATH/src/github.com/insomniacslk/irc-slack/tools/autotoken
# Build the binary.
RUN CGO_ENABLED=0 go build -ldflags="-w -s" -o /go/bin/autotoken
ENV PATH="/go/bin:$PATH"
WORKDIR /tmp
USER guest
# Run the autotoken binary.
CMD ["/go/bin/autotoken", "-h"]


================================================
FILE: LICENSE
================================================
BSD 3-Clause License

Copyright (c) 2018, Andrea Barberio
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* Neither the name of the copyright holder nor the names of its
  contributors may be used to endorse or promote products derived from
  this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: README.md
================================================
# IRC-to-Slack gateway

`irc-slack` is an IRC-to-Slack gateway. It is an IRC server that lets you
connect to your Slack teams with your IRC client.

[![](images/team_chat_2x.png)](https://xkcd.com/1782/)

(That guy is me)

Slack has ended support for IRC and XMPP gateway on the 15th of May 2018. So
what's left to do for people like me, who want to still be able to log in via
IRC? Either you use [wee-slack](https://github.com/wee-slack/wee-slack) (~~but I
don't use WeeChat~~), or you implement your own stuff.

NOTE: 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 :-)
Please keep reporting bugs and sending PRs!

## How to use it

```
cd cmd/irc-slack
make # use `make` instead of `go build` to include build information when running with `-v`
./irc-slack # by default on port 6666
```

Then configure your IRC client to connect to localhost:6666 and use one of the methods in the Tokens section to set the connection password.

You can also [run it with Docker](#run-it-with-docker).

## Feature matrix

|     | public channel | private channel | multiparty IM | IM |
| --- | --- | --- | --- | --- |
| from me | works | works | doesn't work ([#168](https://github.com/insomniacslk/irc-slack/issues/168)) | works |
| to me | works | works | works | works |
| 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)) |
| thread to me | works | works | untested | works but sends in the IM chat ([#167](https://github.com/insomniacslk/irc-slack/issues/167)) |

## Encryption

`irc-slack` by default does not use encryption when communicating with your IRC
client (but the communication between `irc-slack` and the Slack servers is
encrypted).
If you want to use TLS, you can use the `-key` and `-cert` command line
parameters, and point them to a TLS certificate that you own.
This is useful if you plan to connect to to `irc-slack` over the internet.

For example, you can generate a valid certificate with LetsEncrypt (adjust the relevant
fields of course):
```
sudo certbot certonly \
    -n \
    -d your.domain.example.com \
    --test-cert \
    --standalone \
    -m your@email.example.com \
    --agree-tos
```

Then your key and certificate will be generated under
`/etc/letsencrypt/live/your.domain.example.com`
with the names `privkey.pem` and `cert.pem` respectively.

## Authentication

To connect to Slack via `irc-slack` you need an authentication string. There are
three possible methods:
* User tokens with auth cookies (recommended)
* Slack app tokens (if you can install apps on your slack team)
* legacy tokens (soon to be deprecated)

These options are discussed in more detail below.
Then just add `-key <path/to/privkey.pem> -cert <path/to/cert.pem>` to enable
TLS on `irc-slack`, and enable TLS on your IRC client.


### User tokens with auth cookie

This approach does not require legacy tokens nor installing any app, but in order to
get the token there are a few manual steps to execute.

This type of token starts with `xoxc-`, and requires an auth cookie to be paired
to it in order to work.

There are two possible procedures, an entirely manual one, using the browser
console, and a semi-automated one, which requires Chrome or Chromium in headless
mode.

**manual procedure via browser**

This is the same procedure as described in two similar projects, see:
* https://github.com/adsr/irslackd/wiki/IRC-Client-Config#xoxc-tokens
* https://github.com/ltworf/localslackirc/#obtain-a-token

But in short, log via browser on the Slack team, open the browser's network tab
in the developer tools, and look for an XHR transaction. Then look for
* the token (it starts with `xoxc-`) in the request data
* the auth cookie, contained in the `d` key-value in the request cookies (it looks like `d=XXXX;`)

Then concatenate the token and the auth cookie using a `|` character, like this:
```
xoxc-XXXX|d=XXXX;
```

and use the above as your IRC password.

**semi-automated procedure using Chrome/Chromium in headless mode**

See [autotoken](tools/autotoken). Just build it with `go build` and run with
`./autotoken -h` to see the usage help.

If you prefer to run `autotoken` via Docker, you can test your luck with:
```
docker build -t insomniacslk/irc-slack/tools-autotoken -f Dockerfile.autotoken .
docker run --rm -it -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix insomniacslk/irc-slack/tools-autotoken autotoken -h
```

### Slack App tokens

As an alternative, you can install the irc-slack app on your workspace, and use the token that it returns after you authorize it.

In order to run the application, you need to do the following steps:
* create a Slack app using their v1 OauthV2 API (note: not their v2 version) at https://api.slack.com/apps
* configure the redirect URL to your endpoint (in this case
  https://my-server/irc-slack/auth/)
* 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

The token starts with `xoxp-`, and you can use it as your IRC password when
connecting to `irc-slack`.

This is a Slack app with full user permissions, that is used to generate a Slack user token.
Note that you need to install this app on every workspace you want to use it
for, and the workspace owners may reject it.

This app exchanges your temporary authentication code with a permanent token.

### Legacy tokens

This is the easiest method, but it's deprecated and Slack will soon disable it.
Slack has announced that they will stop issuing legacy tokens starting the 4th
of May 2020, so this section will stay here for historical reasons.

Get you Slack legacy token at https://api.slack.com/custom-integrations/legacy-tokens ,
and set it as your IRC password when connecting to `irc-slack`.


## Run it with Docker

Thanks to [halkeye](https://github.com/halkeye) you can run `irc-slack` via
Docker. The `Dockerfile` is published on
https://hub.docker.com/r/insomniacslk/irc-slack and will by default listen on
`0.0.0.0:6666`. You can pull and run it with:

```
docker run --rm -p 6666:6666 insomniacslk/irc-slack
```

If you want to build it locally, just run:
```
docker build -f Dockerfile . -t insomniacslk/irc-slack
```


### Connecting with irssi
```
/network add yourteam.slack.com
/server add -auto -network yourteam.slack.com localhost 6666 xoxp-<your-slack-token>
/connect yourteam.slack.com
```

Remember to add `-tls` to the `/connect` command if you're running `irc-slack`
with TLS.
Also remember to replace `localhost` with the name of the host you're connecting to,
if different.

### Connecting with WeeChat

```
/server add yourteam.slack.com localhost/6666
/set irc.server.yourteam.slack.com.password xoxp-<your-slack-token>
/connect yourteam.slack.com
```

To enable TLS, also run the following before the `/connect` command:
```
/set irc.server.yourteam.slack.com.ssl on
/set irc.server.yourteam.slack.com.ssl_verify on
```

Also remember to replace `localhost` with the name of the host you're connecting to,
if different.

## Gateway usage

There are a few options that you can pass to the server, e.g. to change the listener port, or the server name:

```
$ ./irc-slack -h
Usage of ./irc-slack:
  -c, --cert string         TLS certificate for HTTPS server. Requires -key
  -C, --chunk int           Maximum size of a line to send to the client. Only works for certain reply types (default 512)
  -D, --debug               Enable debug logging of the Slack API
  -d, --download string     If set will download attachments to this location
  -l, --fileprefix string   If set will overwrite urls to attachments with this prefix and local file name inside the path set with -d
  -H, --host string         IP address to listen on (default "127.0.0.1")
  -k, --key string          TLS key for HTTPS server. Requires -cert
  -L, --loglevel string     Log level. One of [none debug info warning error fatal] (default "info")
  -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
  -p, --port int            Local port to listen on (default 6666)
  -s, --server string       IRC server name (i.e. the host name to send to clients)
pflag: help requested
exit status 2
```

## Deploying with Puppet

You can use the [irc-slack module for Puppet](https://github.com/b4ldr/puppet-irc_slack) by [John Bond](https://github.com/b4ldr).

## TODO

A lot of things. Want to help? Grep "TODO", "FIXME" and "XXX" in the code and send me a PR :)

This currently "works for me", but I published it in the hope that someone would use it so we can find and fix bugs.

## BUGS

Plenty 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.

## Authors

* [Andrea Barberio](https://insomniac.slackware.it)
* [Josip Janzic](https://github.com/janza)

## Thanks

Special thanks to
* Stefan Stasik for helping me find, fix and troubleshoot a zillion of bugs :)
* [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


================================================
FILE: cmd/irc-slack/Makefile
================================================
CMD=irc-slack

REVISION := $(shell git rev-parse --short HEAD)
BRANCH := $(shell git rev-parse --abbrev-ref HEAD)

all: build

build: $(wildcard *.go)
	CGO_ENABLED=0 go build -ldflags "-X main.Version=git-$(REVISION)_$(BRANCH)" -o $(CMD)


================================================
FILE: cmd/irc-slack/main.go
================================================
package main

import (
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"net"
	"os"

	"github.com/insomniacslk/irc-slack/pkg/ircslack"

	"github.com/coredhcp/coredhcp/logger"
	"github.com/sirupsen/logrus"
	flag "github.com/spf13/pflag"
)

// Version information. Will be populated with the git revision and branch
// information when running `make`.
var (
	ProgramName        = "irc-slack"
	Version     string = "unknown (please build with `make`)"
)

// To authenticate, the IRC client has to send a PASS command with a Slack
// legacy token for the desired team. See README.md for details.
var (
	port                 = flag.IntP("port", "p", 6666, "Local port to listen on")
	host                 = flag.StringP("host", "H", "127.0.0.1", "IP address to listen on")
	serverName           = flag.StringP("server", "s", "", "IRC server name (i.e. the host name to send to clients)")
	chunkSize            = flag.IntP("chunk", "C", 512, "Maximum size of a line to send to the client. Only works for certain reply types")
	fileDownloadLocation = flag.StringP("download", "d", "", "If set will download attachments to this location")
	fileProxyPrefix      = flag.StringP("fileprefix", "l", "", "If set will overwrite urls to attachments with this prefix and local file name inside the path set with -d")
	logLevel             = flag.StringP("loglevel", "L", "info", fmt.Sprintf("Log level. One of %v", getLogLevels()))
	flagSlackDebug       = flag.BoolP("debug", "D", false, "Enable debug logging of the Slack API")
	flagPagination       = 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")
	flagKey              = flag.StringP("key", "k", "", "TLS key for HTTPS server. Requires -cert")
	flagCert             = flag.StringP("cert", "c", "", "TLS certificate for HTTPS server. Requires -key")
	flagVersion          = flag.BoolP("version", "v", false, "Print version and exit")
)

var log = logger.GetLogger("main")

var logLevels = map[string]func(*logrus.Logger){
	"none":    func(l *logrus.Logger) { l.SetOutput(ioutil.Discard) },
	"debug":   func(l *logrus.Logger) { l.SetLevel(logrus.DebugLevel) },
	"info":    func(l *logrus.Logger) { l.SetLevel(logrus.InfoLevel) },
	"warning": func(l *logrus.Logger) { l.SetLevel(logrus.WarnLevel) },
	"error":   func(l *logrus.Logger) { l.SetLevel(logrus.ErrorLevel) },
	"fatal":   func(l *logrus.Logger) { l.SetLevel(logrus.FatalLevel) },
}

func getLogLevels() []string {
	var levels []string
	for k := range logLevels {
		levels = append(levels, k)
	}
	return levels
}

func main() {
	flag.CommandLine.SortFlags = false
	flag.Parse()
	if *flagVersion {
		fmt.Printf("%s version %s\n", ProgramName, Version)
		os.Exit(0)
	}

	fn, ok := logLevels[*logLevel]
	if !ok {
		log.Fatalf("Invalid log level '%s'. Valid log levels are %v", *logLevel, getLogLevels())
	}
	fn(log.Logger)
	log.Infof("Setting log level to '%s'", *logLevel)
	var sName string
	if *serverName == "" {
		sName = "localhost"
	} else {
		sName = *serverName
	}
	localAddr := net.TCPAddr{Port: *port}
	ip := net.ParseIP(*host)
	if ip == nil {
		log.Fatalf("Invalid IP address to listen on: '%s'", *host)
	}
	localAddr.IP = ip
	log.Printf("Starting server on %v", localAddr.String())
	if *fileDownloadLocation != "" {
		dInfo, err := os.Stat(*fileDownloadLocation)
		if err != nil || !dInfo.IsDir() {
			log.Fatalf("Missing or invalid download directory: %s", *fileDownloadLocation)
		}
	}
	doTLS := false
	if *flagKey != "" && *flagCert != "" {
		doTLS = true
	}
	var tlsConfig *tls.Config
	if doTLS {
		if *flagKey == "" || *flagCert == "" {
			log.Fatalf("-key and -cert must be specified together")
		}
		cert, err := tls.LoadX509KeyPair(*flagCert, *flagKey)
		if err != nil {
			log.Fatalf("Failed to load TLS key/cert: %v", err)
		}
		tlsConfig = &tls.Config{Certificates: []tls.Certificate{cert}}
	}
	server := ircslack.Server{
		LocalAddr:            &localAddr,
		Name:                 sName,
		ChunkSize:            *chunkSize,
		FileDownloadLocation: *fileDownloadLocation,
		FileProxyPrefix:      *fileProxyPrefix,
		SlackDebug:           *flagSlackDebug,
		Pagination:           *flagPagination,
		TLSConfig:            tlsConfig,
	}
	if err := server.Start(); err != nil {
		log.Fatal(err)
	}
}


================================================
FILE: docker-compose.yml
================================================
services:
  irc-slack:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - 6666:6666


================================================
FILE: go.mod
================================================
module github.com/insomniacslk/irc-slack

go 1.25

require (
	github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d
	github.com/chromedp/chromedp v0.14.2
	github.com/coredhcp/coredhcp v0.0.0-20250806070228-f7e98e4e350b
	github.com/sirupsen/logrus v1.9.4
	github.com/slack-go/slack v0.19.0
	github.com/spf13/pflag v1.0.10
	github.com/stretchr/testify v1.11.1
)

require (
	github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb // indirect
	github.com/chromedp/sysutil v1.1.0 // indirect
	github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
	github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 // indirect
	github.com/gobwas/httphead v0.1.0 // indirect
	github.com/gobwas/pool v0.2.1 // indirect
	github.com/gobwas/ws v1.4.0 // indirect
	github.com/google/go-cmp v0.6.0 // indirect
	github.com/gorilla/websocket v1.5.3 // indirect
	github.com/kr/pretty v0.3.1 // indirect
	github.com/mattn/go-colorable v0.1.14 // indirect
	github.com/mattn/go-isatty v0.0.20 // indirect
	github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
	github.com/onsi/ginkgo v1.16.4 // indirect
	github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
	github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 // indirect
	github.com/rogpeppe/go-internal v1.10.0 // indirect
	golang.org/x/crypto v0.45.0 // indirect
	golang.org/x/sys v0.38.0 // indirect
	golang.org/x/term v0.37.0 // indirect
	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)


================================================
FILE: go.sum
================================================
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg=
github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb/go.mod h1:xzXc1S/L+64uglB3pw54o8kqyM6KFYpTeC9Q6+qZIu8=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d h1:ZtA1sedVbEW7EW80Iz2GR3Ye6PwbJAJXjv7D74xG6HU=
github.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a266d/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
github.com/chromedp/chromedp v0.14.2 h1:r3b/WtwM50RsBZHMUm9fsNhhzRStTHrKdr2zmwbZSzM=
github.com/chromedp/chromedp v0.14.2/go.mod h1:rHzAv60xDE7VNy/MYtTUrYreSc0ujt2O1/C3bzctYBo=
github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
github.com/coredhcp/coredhcp v0.0.0-20250806070228-f7e98e4e350b h1:v+UeSM6ffD0hXGpaggA86L7rzysxJSProX2YZKWufvA=
github.com/coredhcp/coredhcp v0.0.0-20250806070228-f7e98e4e350b/go.mod h1:A2iJXPupXJVeJZFSrP1Vx/tK6cZ7oci0V/8egMjp2LI=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3 h1:02WINGfSX5w0Mn+F28UyRoSt9uvMhKguwWMlOAh6U/0=
github.com/go-json-experiment/json v0.0.0-20250910080747-cc2cfa0554c3/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo=
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI=
github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 h1:mZHayPoR0lNmnHyvtYjDeq0zlVHn9K/ZXoy17ylucdo=
github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5/go.mod h1:GEXHk5HgEKCvEIIrSpFI3ozzG5xOKA2DVlEX/gGnewM=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/slack-go/slack v0.19.0 h1:J8lL/nGTsIUX53HU8YxZeI3PDkA+sxZsFrI2Dew7h44=
github.com/slack-go/slack v0.19.0/go.mod h1:K81UmCivcYd/5Jmz8vLBfuyoZ3B4rQC2GHVXHteXiAE=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=


================================================
FILE: pkg/ircslack/channel.go
================================================
package ircslack

import (
	"fmt"
	"strings"
	"time"

	"github.com/slack-go/slack"
)

// Constants for public, private, and multi-party conversation prefixes.
// Channel threads are prefixed with "+" but they are not conversation types
// so they do not belong here. A thread is just a message whose destination
// is within another message in a public, private, or multi-party conversation.
const (
	ChannelPrefixPublicChannel  = "#"
	ChannelPrefixPrivateChannel = "@"
	ChannelPrefixMpIM           = "&"
	// NOTE: a thread is not a channel type
	ChannelPrefixThread = "+"
)

// HasChannelPrefix returns true if the channel name starts with one of the
// supproted channel prefixes.
func HasChannelPrefix(name string) bool {
	if len(name) == 0 {
		return false
	}
	switch string(name[0]) {
	case ChannelPrefixPublicChannel, ChannelPrefixPrivateChannel, ChannelPrefixMpIM, ChannelPrefixThread:
		return true
	default:
		return false
	}
}

// StripChannelPrefix returns a channel name without its channel prefix. If no
// channel prefix is present, the string is returned unchanged.
func StripChannelPrefix(name string) string {
	if HasChannelPrefix(name) {
		return name[1:]
	}
	return name
}

// ChannelMembers returns a list of users in the given conversation.
func ChannelMembers(ctx *IrcContext, channelID string) ([]slack.User, error) {
	var (
		members, m []string
		nextCursor string
		err        error
		page       int
	)
	for {
		attempt := 0
		for {
			// retry if rate-limited, no more than MaxSlackAPIAttempts times
			if attempt >= MaxSlackAPIAttempts {
				return nil, fmt.Errorf("ChannelMembers: exceeded the maximum number of attempts (%d) with the Slack API", MaxSlackAPIAttempts)
			}
			log.Debugf("ChannelMembers: page %d attempt #%d nextCursor=%s", page, attempt, nextCursor)
			m, nextCursor, err = ctx.SlackClient.GetUsersInConversation(&slack.GetUsersInConversationParameters{ChannelID: channelID, Cursor: nextCursor, Limit: 1000})
			if err != nil {
				log.Errorf("Failed to get users in conversation '%s': %v", channelID, err)
				if rlErr, ok := err.(*slack.RateLimitedError); ok {
					// we were rate-limited. Let's wait as much as Slack
					// instructs us to do
					log.Warningf("Hit Slack API rate limiter. Waiting %v", rlErr.RetryAfter)
					time.Sleep(rlErr.RetryAfter)
					attempt++
					continue
				}
				return nil, fmt.Errorf("Cannot get member list for conversation %s: %v", channelID, err)
			}
			break
		}
		members = append(members, m...)
		log.Debugf("Fetched %d user IDs for channel %s (fetched so far: %d)", len(m), channelID, len(members))
		// TODO call ctx.Users.FetchByID here in a goroutine to see if this
		// speeds up
		if nextCursor == "" {
			break
		}
		page++
	}
	log.Debugf("Retrieving user information for %d users", len(members))
	users, err := ctx.Users.FetchByIDs(ctx.SlackClient, false, members...)
	if err != nil {
		return nil, fmt.Errorf("Failed to fetch users by their IDs: %v", err)
	}
	return users, nil
}

// Channel wraps a Slack conversation with a few utility functions.
type Channel slack.Channel

// IsPublicChannel returns true if the channel is public.
func (c *Channel) IsPublicChannel() bool {
	return c.IsChannel && !c.IsPrivate
}

// IsPrivateChannel returns true if the channel is private.
func (c *Channel) IsPrivateChannel() bool {
	return c.IsGroup && c.IsPrivate
}

// IsMP returns true if it is a multi-party conversation.
func (c *Channel) IsMP() bool {
	return c.IsMpIM
}

// IRCName returns the channel name as it would appear on IRC.
// Examples:
// * #channel for public groups
// * @channel for private groups
// * &Gxxxx|nick1-nick2-nick3 for multi-party IMs
func (c *Channel) IRCName() string {
	switch {
	case c.IsPublicChannel():
		return ChannelPrefixPublicChannel + c.Name
	case c.IsPrivateChannel():
		return ChannelPrefixPrivateChannel + c.Name
	case c.IsMP():
		name := ChannelPrefixMpIM + c.ID + "|" + c.Name
		name = strings.Replace(name, "mpdm-", "", -1)
		name = strings.Replace(name, "--", "-", -1)
		if len(name) >= 30 {
			return name[:29] + "…"
		}
		return name
	default:
		log.Warningf("Unknown channel type for channel %+v", c)
		return "<unknow-channel-type>"
	}
}

// SlackName returns the slack.Channel.Name field.
func (c *Channel) SlackName() string {
	return c.Name
}


================================================
FILE: pkg/ircslack/channels.go
================================================
package ircslack

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/slack-go/slack"
)

// Channels wraps the channel list with convenient operations and cache.
type Channels struct {
	channels   map[string]Channel
	Pagination int
	mu         sync.Mutex
}

// NewChannels creates a new Channels object.
func NewChannels(pagination int) *Channels {
	return &Channels{
		channels:   make(map[string]Channel),
		Pagination: pagination,
	}
}

// SupportedChannelPrefixes returns a list of supported channel prefixes.
func SupportedChannelPrefixes() []string {
	return []string{
		ChannelPrefixPublicChannel,
		ChannelPrefixPrivateChannel,
		ChannelPrefixMpIM,
		ChannelPrefixThread,
	}

}

// AsMap returns the channels as a map of name -> channel. The map is copied to
// avoid data races
func (c *Channels) AsMap() map[string]Channel {
	c.mu.Lock()
	defer c.mu.Unlock()
	ret := make(map[string]Channel, len(c.channels))
	for k, v := range c.channels {
		ret[k] = v
	}
	return ret
}

// FetchByIDs fetches the channels with the specified IDs and updates the
// internal channel mapping.
func (c *Channels) FetchByIDs(client *slack.Client, skipCache bool, channelIDs ...string) ([]Channel, error) {
	var (
		toRetrieve       []string
		alreadyRetrieved []Channel
	)

	if !skipCache {
		c.mu.Lock()
		for _, cid := range channelIDs {
			if ch, ok := c.channels[cid]; !ok {
				toRetrieve = append(toRetrieve, cid)
			} else {
				alreadyRetrieved = append(alreadyRetrieved, ch)
			}
		}
		c.mu.Unlock()
		log.Debugf("Fetching information for %d channels out of %d (%d already in cache)", len(toRetrieve), len(channelIDs), len(channelIDs)-len(toRetrieve))
	} else {
		toRetrieve = channelIDs
	}
	allFetchedChannels := make([]Channel, 0, len(channelIDs))
	for i := 0; i < len(toRetrieve); i++ {
		for {
			attempt := 0
			if attempt >= MaxSlackAPIAttempts {
				return nil, fmt.Errorf("Channels.FetchByIDs: exceeded the maximum number of attempts (%d) with the Slack API", MaxSlackAPIAttempts)
			}
			log.Debugf("Fetching %d channels of %d, attempt %d of %d", len(toRetrieve), len(channelIDs), attempt+1, MaxSlackAPIAttempts)
			slackChannel, err := client.GetConversationInfo(&slack.GetConversationInfoInput{ChannelID: toRetrieve[i], IncludeLocale: true, IncludeNumMembers: true})
			if err != nil {
				if rlErr, ok := err.(*slack.RateLimitedError); ok {
					// we were rate-limited. Let's wait the recommended delay
					log.Warningf("Hit Slack API rate limiter. Waiting %v", rlErr.RetryAfter)
					time.Sleep(rlErr.RetryAfter)
					attempt++
					continue
				}
				return nil, err
			}
			ch := Channel(*slackChannel)
			allFetchedChannels = append(allFetchedChannels, ch)
			// also update the local users map
			c.mu.Lock()
			c.channels[ch.ID] = ch
			c.mu.Unlock()
			break
		}
	}
	allChannels := append(alreadyRetrieved, allFetchedChannels...)
	if len(channelIDs) != len(allChannels) {
		return allFetchedChannels, fmt.Errorf("Found %d users but %d were requested", len(allChannels), len(channelIDs))
	}
	return allChannels, nil
}

// Fetch retrieves all the channels on a given Slack team. The Slack client has
// to be valid and connected.
func (c *Channels) Fetch(client *slack.Client) error {
	log.Infof("Fetching all channels, might take a while on large Slack teams")
	// currently slack-go does not expose a way to change channel pagination as
	// it does for the users API.
	var (
		err      error
		ctx      = context.Background()
		channels = make(map[string]Channel)
	)
	start := time.Now()
	params := slack.GetConversationsParameters{
		Types: []string{"public_channel", "private_channel"},
		Limit: c.Pagination,
	}
	for err == nil {
		chans, nextCursor, err := client.GetConversationsContext(ctx, &params)
		if err == nil {
			log.Debugf("Retrieved %d channels (current total is %d)", len(chans), len(channels))
			for _, sch := range chans {
				// WARNING WARNING WARNING: channels are internally mapped by
				// the Slack name, while users are mapped by Slack ID.
				ch := Channel(sch)
				channels[ch.SlackName()] = ch
			}
		} else if rateLimitedError, ok := err.(*slack.RateLimitedError); ok {
			select {
			case <-ctx.Done():
				err = ctx.Err()
			case <-time.After(rateLimitedError.RetryAfter):
				err = nil
			}
		}
		if nextCursor == "" {
			break
		}
		params.Cursor = nextCursor
	}
	log.Infof("Retrieved %d channels in %s", len(channels), time.Since(start))
	c.mu.Lock()
	c.channels = channels
	for name, ch := range channels {
		log.Debugf("Retrieved channel: %s -> %+v", name, ch)
	}
	c.mu.Unlock()
	return nil
}

// Count returns the number of channels. This method must be called after
// `Fetch`.
func (c *Channels) Count() int {
	return len(c.channels)
}

// ByID retrieves a channel by its Slack ID.
func (c *Channels) ByID(id string) *Channel {
	c.mu.Lock()
	defer c.mu.Unlock()
	for _, c := range c.channels {
		if c.ID == id {
			return &c
		}
	}
	return nil
}

// ByName retrieves a channel by its Slack or IRC name.
func (c *Channels) ByName(name string) *Channel {
	if HasChannelPrefix(name) {
		// without prefix, the channel now has the form of a Slack name
		name = name[1:]
	}
	c.mu.Lock()
	defer c.mu.Unlock()
	if ch, ok := c.channels[name]; ok {
		return &ch
	}
	return nil
}


================================================
FILE: pkg/ircslack/channels_test.go
================================================
package ircslack

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"testing"

	"github.com/slack-go/slack"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestChannelsNewChannels(t *testing.T) {
	u := NewChannels(100)
	require.NotNil(t, u)
	assert.NotNil(t, u.channels)
}

type fakeErrorChannelsPaginationComplete struct{}

type fakeChannelsResponse struct {
	Members []slack.Channel
	Channel slack.Channel
}

func (f fakeErrorChannelsPaginationComplete) Error() string {
	return "pagination complete"
}

type fakeSlackHTTPClientChannels struct{}

func (c fakeSlackHTTPClientChannels) Do(req *http.Request) (*http.Response, error) {
	switch req.URL.Path {
	case "/api/conversations.list":
		// reply as per https://api.slack.com/methods/channels.list
		data := []byte(`{"channels": [{"id": "1234", "name": "general", "is_channel": true}], "response_metadata": {"next_cursor": ""}}`)
		return &http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.1",
			ProtoMajor: 1,
			ProtoMinor: 1,
			Body:       ioutil.NopCloser(bytes.NewBuffer(data)),
		}, nil
	default:
		return nil, fmt.Errorf("testing: http client URL not supported: %s", req.URL)
	}
}

func TestChannelsFetch(t *testing.T) {
	client := slack.New("test-token", slack.OptionHTTPClient(fakeSlackHTTPClientChannels{}))
	channels := NewChannels(100)
	err := channels.Fetch(client)
	require.NoError(t, err)
	assert.Equal(t, 1, channels.Count())
}

func TestChannelsById(t *testing.T) {
	client := slack.New("test-token", slack.OptionHTTPClient(fakeSlackHTTPClientChannels{}))
	channels := NewChannels(100)
	err := channels.Fetch(client)
	require.NoError(t, err)
	u := channels.ByID("1234")
	require.NotNil(t, u)
	assert.Equal(t, "1234", u.ID)
	assert.Equal(t, "general", u.Name)
}

func TestChannelsByName(t *testing.T) {
	client := slack.New("test-token", slack.OptionHTTPClient(fakeSlackHTTPClientChannels{}))
	channels := NewChannels(100)
	err := channels.Fetch(client)
	require.NoError(t, err)
	u := channels.ByName("general")
	require.NotNil(t, u)
	assert.Equal(t, "1234", u.ID)
	assert.Equal(t, "general", u.Name)
}


================================================
FILE: pkg/ircslack/event_handler.go
================================================
package ircslack

import (
	"fmt"
	"math"
	"strings"

	"github.com/slack-go/slack"
)

func joinText(first string, second string, separator string) string {
	if first == "" {
		return second
	}
	if second == "" {
		return first
	}
	return first + separator + second
}

func formatThreadChannelName(threadTimestamp string, channel *Channel) string {
	return ChannelPrefixThread + channel.Name + "-" + threadTimestamp
}

func resolveChannelName(ctx *IrcContext, msgChannel, threadTimestamp string) string {
	if strings.HasPrefix(msgChannel, "C") || strings.HasPrefix(msgChannel, "G") {
		// Channel message
		channel := ctx.Channels.ByID(msgChannel)
		if channel == nil {
			// try fetching it, in case it's a new channel
			channels, err := ctx.Channels.FetchByIDs(ctx.SlackClient, false, msgChannel)
			if err != nil || len(channels) == 0 {
				ctx.SendUnknownError("Failed to fetch channel with ID `%s`: %v", msgChannel, err)
				return ""
			}
			channel = &channels[0]
		}

		if channel == nil {
			ctx.SendUnknownError("Unknown channel ID `%s` when resolving channel name", msgChannel)
			return ""
		} else if threadTimestamp != "" {
			channame := formatThreadChannelName(threadTimestamp, channel)
			openingText, err := ctx.GetThreadOpener(msgChannel, threadTimestamp)
			if err != nil {
				ctx.SendUnknownError("Failed to get thread opener for `%s`: %v", msgChannel, err)
				return ""
			}
			IrcSendChanInfoAfterJoinCustom(
				ctx,
				channame,
				msgChannel,
				openingText.Text,
				[]slack.User{},
			)

			privmsg := fmt.Sprintf(":%v!%v@%v PRIVMSG %v :%s%s%s\r\n",
				channame, openingText.User, ctx.ServerName,
				channame, "", openingText.Text, "",
			)
			if _, err := ctx.Conn.Write([]byte(privmsg)); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
			return channame
		} else if channel.IsMpIM {
			if ctx.Channels.ByName(channel.IRCName()) == nil {
				members, err := ChannelMembers(ctx, channel.ID)
				if err != nil {
					log.Warningf("Failed to fetch channel members for `%s`: %v", channel.Name, err)
				} else {
					IrcSendChanInfoAfterJoin(ctx, channel, members)
				}
			}
			return channel.IRCName()
		}

		return channel.IRCName()
	} else if strings.HasPrefix(msgChannel, "D") {
		// Direct message to me
		channel := ctx.Channels.ByID(msgChannel)
		if channel == nil {
			// not found locally, try to get it via Slack API
			channels, err := ctx.Channels.FetchByIDs(ctx.SlackClient, false, msgChannel)
			if err != nil || len(channels) == 0 {
				ctx.SendUnknownError("Failed to fetch IM chat with ID `%s`: %v", msgChannel, err)
				return ""
			}
			channel = &channels[0]
		}
		members, err := ChannelMembers(ctx, channel.ID)
		if err != nil {
			ctx.SendUnknownError("Failed to fetch channel members for `%s`: %v", channel.Name, err)
			return ""
		}
		// we expect only two members in a direct message. Raise an
		// error if not.
		if len(members) == 0 || len(members) > 2 {
			ctx.SendUnknownError("Want 1 or 2 users in conversation, got %d (conversation ID: %s)", len(members), msgChannel)
			return ""
		}
		// of the two users, one is me. Otherwise fail
		if ctx.UserID() == "" {
			ctx.SendUnknownError("Cannot get my own user ID")
			return ""
		}
		user1 := members[0]
		var user2 slack.User
		if len(members) == 2 {
			user2 = members[1]
		} else {
			// len is 1. Sending a message to myself
			user2 = user1
		}
		if user1.ID != ctx.UserID() && user2.ID != ctx.UserID() {
			ctx.SendUnknownError("Got a direct message where I am not part of the members list (conversation: %s)", msgChannel)
			return ""
		}
		var recipientID string
		if user1.ID == ctx.UserID() {
			// then it's the other user
			recipientID = user2.ID
		} else {
			recipientID = user1.ID
		}
		// now resolve the ID to the user's nickname
		nickname := ctx.GetUserInfo(recipientID)
		if nickname == nil {
			// ERR_UNKNOWNERROR
			ctx.SendUnknownError("Unknown destination user ID %s for direct message %s", recipientID, msgChannel)
			return ""
		}
		return nickname.Name
	}
	log.Warningf("Unknown recipient ID: %s", msgChannel)
	return ""
}

func appendIfNotMoreThan(slice []slack.Msg, msg slack.Msg) []slack.Msg {
	if len(slice) == 100 {
		return append(slice[1:], msg)
	}
	return append(slice, msg)
}

func getConversationDetails(
	ctx *IrcContext,
	channelID string,
	timestamp string,
) (slack.Message, error) {
	message, err := ctx.SlackClient.GetConversationHistory(&slack.GetConversationHistoryParameters{
		ChannelID: channelID,
		Latest:    timestamp,
		Limit:     1,
		Inclusive: true,
	})
	if err != nil {
		return slack.Message{}, err
	}
	if len(message.Messages) > 0 {
		return message.Messages[0], nil
	}
	return slack.Message{}, fmt.Errorf("No such message found")
}

func replacePermalinkWithText(ctx *IrcContext, text string) string {
	matches := rxSlackArchiveURL.FindStringSubmatch(text)
	if len(matches) != 4 {
		return text
	}
	channel := matches[1]
	timestamp := matches[2] + "." + matches[3]
	message, err := getConversationDetails(ctx, channel, timestamp)
	if err != nil {
		log.Printf("could not get message details from permalink %s %s %s %v", matches[0], channel, timestamp, err)
		return text
	}
	return text + "\n> " + message.Text
}

func printMessage(ctx *IrcContext, message slack.Msg, prefix string) {
	user := ctx.GetUserInfo(message.User)
	name := ""
	if user == nil {
		if message.User != "" {
			log.Warningf("Failed to get user info for %v %s", message.User, message.Username)
			name = message.User
		} else {
			name = strings.ReplaceAll(message.Username, " ", "_")
		}
	} else {
		name = user.Name
	}
	// get channel or other recipient (e.g. recipient of a direct message)
	channame := resolveChannelName(ctx, message.Channel, message.ThreadTimestamp)

	text := message.Text
	for _, attachment := range message.Attachments {
		text = joinText(text, attachment.Pretext, "\n")
		text = joinText(text, attachment.Title, "\n")
		if attachment.Text != "" {
			text = joinText(text, attachment.Text, "\n")
		} else {
			text = joinText(text, attachment.Fallback, "\n")
		}
		text = joinText(text, attachment.ImageURL, "\n")
	}
	for _, file := range message.Files {
		text = joinText(text, ctx.FileHandler.Download(file), " ")
	}

	log.Debugf("SLACK msg from %v (%v) on %v: %v",
		message.User,
		name,
		message.Channel,
		text,
	)
	if name == "" && text == "" {
		log.Warningf("Empty username and message: %+v", message)
		return
	}
	text = replacePermalinkWithText(ctx, text)
	text = ctx.ExpandUserIds(text)
	text = ExpandText(text)
	text = joinText(prefix, text, " ")

	if name == ctx.Nick() {
		botID := message.BotID
		if (ctx.usingLegacyToken && user != nil && botID != user.Profile.BotID) ||
			(!ctx.usingLegacyToken && message.ClientMsgID == "") {
			// Don't print my own messages.
			// When using legacy tokens, we distinguish our own messages sent
			// from other clients by checking the bot ID.
			// With new style tokens, we check the client message ID.
			log.Debugf("Skipping message sent by me")
			return
		}
	}
	// handle multi-line messages
	var linePrefix, lineSuffix string
	if message.SubType == "me_message" {
		// handle /me messages
		linePrefix = "\x01ACTION "
		lineSuffix = "\x01"
	}
	for _, line := range strings.Split(text, "\n") {
		privmsg := fmt.Sprintf(":%v!%v@%v PRIVMSG %v :%s%s%s\r\n",
			name, message.User, ctx.ServerName,
			channame, linePrefix, line, lineSuffix,
		)
		log.Debug(privmsg)
		if _, err := ctx.Conn.Write([]byte(privmsg)); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	}
}

func eventHandler(ctx *IrcContext, rtm *slack.RTM) {
	log.Info("Started Slack event listener")
	for msg := range rtm.IncomingEvents {
		switch ev := msg.Data.(type) {
		case *slack.MessageEvent:
			// https://api.slack.com/events/message
			message := ev.Msg
			if message.Hidden {
				continue
			}
			switch message.SubType {
			case "message_changed":
				// https://api.slack.com/events/message/message_changed
				editedMessage, err := getConversationDetails(ctx, message.Channel, message.Timestamp)
				if err != nil {
					fmt.Printf("could not get changed conversation details %s", err)
					continue
				}
				log.Printf("edited msg chan %v", editedMessage.Msg.Channel)
				editedMessage.Msg.Channel = message.Channel
				printMessage(ctx, editedMessage.Msg, "(edited)")
				continue
			case "channel_topic":
				// https://api.slack.com/events/message/channel_topic
				// Send out new topic
				channel := ctx.Channels.ByID(message.Channel)
				if channel == nil {
					log.Warningf("Cannot get channel name for %v", message.Channel)
				} else {
					newTopic := fmt.Sprintf(":%v TOPIC %s :%v\r\n", ctx.Mask(), channel.IRCName(), message.Topic)
					log.Infof("Got new topic: %v", newTopic)
					if _, err := ctx.Conn.Write([]byte(newTopic)); err != nil {
						log.Warningf("Failed to send IRC message: %v", err)
					}
				}
			case "channel_join", "channel_leave":
				// https://api.slack.com/events/message/channel_join
				// https://api.slack.com/events/message/channel_leave
				// Note: this is handled by slack.MemberJoinedChannelEvent
				// and slack.MemberLeftChannelEvent.
			default:
				printMessage(ctx, message, "")
			}
		case *slack.ConnectedEvent:
			log.Info("Connected to Slack")
			ctx.SlackConnected = true
		case *slack.DisconnectedEvent:
			de := msg.Data.(*slack.DisconnectedEvent)
			log.Warningf("Disconnected from Slack (intentional: %v, cause: %v)", de.Intentional, de.Cause)
			ctx.SlackConnected = false
			ctx.Conn.Close()
			ctx.Users, ctx.Channels = nil, nil
			return
		case *slack.MemberJoinedChannelEvent:
			// This is the currently preferred way to notify when a user joins a
			// channel, see https://api.slack.com/changelog/2017-05-rethinking-channel-entrance-and-exit-events-and-messages
			// https://api.slack.com/events/member_joined_channel
			log.Infof("Event: Member Joined Channel: %+v", ev)
			ch := ctx.Channels.ByID(ev.Channel)
			if ch == nil {
				log.Warningf("Unknown channel: %s", ev.Channel)
				continue
			}
			if _, err := ctx.Conn.Write([]byte(fmt.Sprintf(":%s JOIN %s\r\n", ctx.Mask(), ch.IRCName()))); err != nil {
				log.Warningf("Failed to send IRC JOIN message for `%s`: %v", ch.IRCName(), err)
			}
		case *slack.MemberLeftChannelEvent:
			// This is the currently preferred way to notify when a user leaves a
			// channel, see https://api.slack.com/changelog/2017-05-rethinking-channel-entrance-and-exit-events-and-messages
			// https://api.slack.com/events/member_left_channel
			log.Infof("Event: Member Left Channel: %+v", ev)
			ch := ctx.Channels.ByID(ev.Channel)
			if ch == nil {
				log.Warningf("Unknown channel: %s", ev.Channel)
				continue
			}
			if _, err := ctx.Conn.Write([]byte(fmt.Sprintf(":%v PART %s\r\n", ctx.Mask(), ch.IRCName()))); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
		case *slack.TeamJoinEvent:
			// https://api.slack.com/events/team_join
			// update the users list
			if _, err := ctx.Users.FetchByIDs(ctx.SlackClient, false, ev.User.ID); err != nil {
				log.Warningf("Failed to fetch users: %v", err)
			}
		case *slack.UserChangeEvent:
			// https://api.slack.com/events/user_change
			// update the user list
			if _, err := ctx.Users.FetchByIDs(ctx.SlackClient, false, ev.User.ID); err != nil {
				log.Warningf("Failed to fetch users: %v", err)
			}
		case *slack.ChannelJoinedEvent, *slack.ChannelLeftEvent:
			// https://api.slack.com/events/channel_joined
			// Note: this is handled by slack.MemberJoinedChannelEvent
			// and slack.MemberLeftChannelEvent.
		case *slack.ReactionAddedEvent:
			// https://api.slack.com/events/reaction_added
			channame := resolveChannelName(ctx, ev.Item.Channel, "")
			user := ctx.GetUserInfo(ev.User)
			name := ""
			if user == nil {
				log.Warningf("Error getting user info for %v", ev.User)
				name = ev.User
			} else {
				name = user.Name
			}
			msg, err := getConversationDetails(ctx, ev.Item.Channel, ev.Item.Timestamp)
			if err != nil {
				fmt.Printf("could not get Conversation details %s", err)
				continue
			}
			msgText := msg.Text

			msgText = ctx.ExpandUserIds(msgText)
			msgText = ExpandText(msgText)
			msgText = strings.Split(msgText, "\n")[0]

			msgText = msgText[:int(math.Min(float64(len(msgText)), 100))]

			privmsg := fmt.Sprintf(":%v!%v@%v PRIVMSG %v :\x01ACTION reacted with %s to: \x0315%s\x03\x01\r\n",
				name, ev.User, ctx.ServerName,
				channame, ev.Reaction, msgText,
			)
			log.Debug(privmsg)
			if _, err := ctx.Conn.Write([]byte(privmsg)); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
		case *slack.UserTypingEvent:
			// https://api.slack.com/events/user_typing
			u := ctx.GetUserInfo(ev.User)
			username := "<unknown>"
			if u != nil {
				username = u.Name
			}
			c, err := ctx.GetConversationInfo(ev.Channel)
			channame := "<unknown or IM chat>"
			if err == nil {
				channame = c.Name
			}
			log.Infof("User %s (%s) is typing on channel %s (%s)", ev.User, username, ev.Channel, channame)
		case *slack.DesktopNotificationEvent:
			// TODO implement actions on notifications
			log.Infof("Event: Desktop notification: %+v", ev)
		case *slack.LatencyReport:
			log.Infof("Current Slack latency: %v", ev.Value)
		case *slack.RTMError:
			log.Warningf("Slack RTM error: %v", ev.Error())
		case *slack.InvalidAuthEvent:
			log.Warningf("Invalid slack credentials")
		default:
			log.Debugf("SLACK event: %v: %+v", msg.Type, msg.Data)
		}
	}
}


================================================
FILE: pkg/ircslack/file_handler.go
================================================
package ircslack

import (
	"fmt"
	"io"
	"math"
	"net"
	"net/http"
	"net/url"
	"os"
	"path/filepath"
	"strings"
	"time"

	"github.com/slack-go/slack"
)

const (
	maxHTTPAttempts = 3
	retryInterval   = time.Second
)

// FileHandler downloads files from slack
type FileHandler struct {
	SlackAPIKey          string
	FileDownloadLocation string
	ProxyPrefix          string
}

func retryableNetError(err error) bool {
	if err == nil {
		return false
	}
	switch err := err.(type) {
	case net.Error:
		if err.Timeout() {
			return true
		}
	}
	return false
}

func retryableHTTPError(resp *http.Response) bool {
	if resp == nil {
		return false
	}
	if resp.StatusCode == 500 || resp.StatusCode == 502 {
		return true
	}
	return false
}

// Download downloads url contents to a local file and returns a url to either
// the file on slack's server or a downloaded file
func (handler *FileHandler) Download(file slack.File) string {
	fileURL := file.URLPrivate
	if handler.FileDownloadLocation == "" || file.IsExternal || handler.SlackAPIKey == "" {
		return fileURL
	}
	localFileName := fmt.Sprintf("%s_%s", file.ID, file.Title)
	if !strings.HasSuffix(localFileName, file.Filetype) {
		localFileName += "." + file.Filetype
	}
	localFilePath := filepath.Join(handler.FileDownloadLocation, localFileName)
	go func() {
		out, err := os.Create(localFilePath)
		if err != nil {
			log.Warningf("Could not create file for download %s: %v", localFilePath, err)
			return
		}

		defer out.Close()
		request, _ := http.NewRequest("GET", fileURL, nil)
		request.Header.Add("Authorization", "Bearer "+handler.SlackAPIKey)
		var client = &http.Client{}
		var resp *http.Response
		for attempt := 0; attempt < maxHTTPAttempts; attempt++ {
			resp, err = client.Do(request)
			if err != nil && retryableNetError(err) || retryableHTTPError(resp) {
				time.Sleep(retryInterval * time.Duration(math.Pow(float64(attempt), 2)))
				continue
			}
			if err == nil {
				break
			}
			log.Warningf("Error downloading %s: %v", fileURL, err)
			return
		}
		if resp.StatusCode != http.StatusOK {
			log.Debugf("Got %d while downloading %s", resp.StatusCode, fileURL)
			return
		}
		defer resp.Body.Close()
		_, err = io.Copy(out, resp.Body)
		if err != nil {
			log.Warningf("Error writing %s: %v", fileURL, err)
		}
	}()
	if handler.ProxyPrefix != "" {
		return handler.ProxyPrefix + url.PathEscape(localFileName)
	}
	return fileURL
}


================================================
FILE: pkg/ircslack/irc_context.go
================================================
package ircslack

import (
	"fmt"
	"net"
	"strings"
	"time"

	"github.com/slack-go/slack"
)

// SlackPostMessage represents a message sent to slack api
type SlackPostMessage struct {
	Target   string
	TargetTs string
	Text     string
}

// IrcContext holds the client context information
type IrcContext struct {
	Conn net.Conn
	User *slack.User
	// TODO make RealName a function
	RealName          string
	OrigName          string
	SlackClient       *slack.Client
	SlackRTM          *slack.RTM
	SlackAPIKey       string
	SlackDebug        bool
	SlackConnected    bool
	ServerName        string
	Channels          *Channels
	Users             *Users
	ChunkSize         int
	postMessage       chan SlackPostMessage
	conversationCache map[string]*slack.Channel
	FileHandler       *FileHandler
	// set to `true` if we are using a deprecated legacy token, false otherwise
	usingLegacyToken bool
}

// Nick returns the nickname of the user, if known
func (ic *IrcContext) Nick() string {
	if ic.User == nil {
		return "<unknown>"
	}
	return ic.User.Name
}

// UserName returns the user's name. Currently this is equivalent to the user's
// Slack ID
func (ic *IrcContext) UserName() string {
	if ic.User == nil {
		return "<unknown>"
	}
	return ic.User.ID
}

// GetThreadOpener returns text of the first message in a thread that provided message belongs to
func (ic *IrcContext) GetThreadOpener(channel string, threadTimestamp string) (slack.Message, error) {
	msgs, _, _, err := ic.SlackClient.GetConversationReplies(&slack.GetConversationRepliesParameters{
		ChannelID: channel,
		Timestamp: threadTimestamp,
	})
	if err != nil || len(msgs) == 0 {
		return slack.Message{}, err
	}
	return msgs[0], nil
}

// ExpandUserIds will convert slack user tags with user's nicknames
func (ic *IrcContext) ExpandUserIds(text string) string {
	return rxSlackUser.ReplaceAllStringFunc(text, func(subs string) string {
		uid := subs[2 : len(subs)-1]
		user := ic.GetUserInfo(uid)
		if user == nil {
			return subs
		}
		return fmt.Sprintf("@%s", user.Name)
	})
}

// Start handles batching of messages to slack
func (ic *IrcContext) Start() {
	textBuffer := make(map[string]string)
	timer := time.NewTimer(time.Second)
	var message SlackPostMessage
	for {
		select {
		case message = <-ic.postMessage:
			log.Debugf("Got new message %v", message)
			textBuffer[message.Target] += message.Text + "\n"
			timer.Reset(time.Second)
		case <-timer.C:
			for target, text := range textBuffer {
				opts := []slack.MsgOption{}
				opts = append(opts, slack.MsgOptionAsUser(true))
				opts = append(opts, slack.MsgOptionText(strings.TrimSpace(text), false))
				if message.TargetTs != "" {
					opts = append(opts, slack.MsgOptionTS(message.TargetTs))
				}
				if _, _, err := ic.SlackClient.PostMessage(target, opts...); err != nil {
					log.Warningf("Failed to post message to Slack to target %s: %v", target, err)
				}
			}
			textBuffer = make(map[string]string)
		}
	}
}

// PostTextMessage batches all messages that should be posted to slack
func (ic *IrcContext) PostTextMessage(target, text, targetTs string) {
	ic.postMessage <- SlackPostMessage{
		Target:   target,
		TargetTs: targetTs,
		Text:     text,
	}
}

// GetUserInfo returns a slack.User instance from a given user ID, or nil if
// no user with that ID was found
func (ic *IrcContext) GetUserInfo(userID string) *slack.User {
	u := ic.Users.ByID(userID)
	if u == nil {
		log.Warningf("GetUserInfo: unknown user ID '%s'", userID)
	}
	return u
}

// GetUserInfoByName returns a slack.User instance from a given user name, or
// nil if no user with that name was found
func (ic *IrcContext) GetUserInfoByName(username string) *slack.User {
	u := ic.Users.ByName(username)
	if u == nil {
		log.Warningf("GetUserInfoByName: unknown user name '%s'", username)
	}
	return u
}

// UserID returns the user's Slack ID
func (ic IrcContext) UserID() string {
	if ic.User == nil {
		return "<unknown>"
	}
	return ic.User.ID
}

// Mask returns the IRC mask for the current user
func (ic IrcContext) Mask() string {
	return fmt.Sprintf("%v!%v@%v", ic.Nick(), ic.UserName(), ic.Conn.RemoteAddr().(*net.TCPAddr).IP)
}

// GetConversationInfo is cached version of slack.GetConversationInfo
func (ic IrcContext) GetConversationInfo(conversation string) (*slack.Channel, error) {
	c, ok := ic.conversationCache[conversation]
	if ok {
		return c, nil
	}
	c, err := ic.SlackClient.GetConversationInfo(&slack.GetConversationInfoInput{ChannelID: conversation, IncludeLocale: true, IncludeNumMembers: true})
	if err != nil {
		return c, err
	}
	ic.conversationCache[conversation] = c
	return c, nil
}

// Maps of user contexts and nicknames
var (
	UserContexts = map[net.Addr]*IrcContext{}
)

// SendUnknownError sends an IRC 400 (ERR_UNKNOWNERROR) message to the client
// and prints a warning about it.
func (ic *IrcContext) SendUnknownError(fmtstr string, args ...interface{}) {
	msg := fmt.Sprintf(fmtstr, args...)
	log.Warningf("Sending ERR_UNKNOWNERROR (400) to client with message: %s", msg)
	if err := SendIrcNumeric(ic, 400, ic.Nick(), msg); err != nil {
		log.Warningf("Failed to send ERR_UNKNOWNERROR (400) to client: %v", err)
	}
}


================================================
FILE: pkg/ircslack/irc_server.go
================================================
package ircslack

import (
	"errors"
	"fmt"
	"html"
	"net/http"
	"net/url"
	"regexp"
	"strings"
	"time"

	"github.com/coredhcp/coredhcp/logger"
	"github.com/sirupsen/logrus"
	"github.com/slack-go/slack"
)

// Project constants
const (
	ProjectAuthor       = "Andrea Barberio"
	ProjectAuthorEmail  = "insomniac@slackware.it"
	ProjectURL          = "https://github.com/insomniacslk/irc-slack"
	MaxSlackAPIAttempts = 3
)

// IrcCommandHandler is the prototype that every IRC command handler has to implement
type IrcCommandHandler func(*IrcContext, string, string, []string, string)

// IrcCommandHandlers maps each IRC command to its handler function
var IrcCommandHandlers = map[string]IrcCommandHandler{
	"CAP":     IrcCapHandler,
	"NICK":    IrcNickHandler,
	"USER":    IrcUserHandler,
	"PING":    IrcPingHandler,
	"PRIVMSG": IrcPrivMsgHandler,
	"QUIT":    IrcQuitHandler,
	"MODE":    IrcModeHandler,
	"PASS":    IrcPassHandler,
	"WHOIS":   IrcWhoisHandler,
	"WHO":     IrcWhoHandler,
	"JOIN":    IrcJoinHandler,
	"PART":    IrcPartHandler,
	"TOPIC":   IrcTopicHandler,
	"NAMES":   IrcNamesHandler,
}

// IrcNumericsSafeToChunk is a list of IRC numeric replies that are safe
// to chunk. As per RFC2182, the maximum message size is 512, including
// newlines. Sending longer lines breaks some clients like ZNC. See
// https://github.com/insomniacslk/irc-slack/issues/38 for background.
// This list is meant to grow if we find more IRC numerics that are safe
// to split.
// Being safe to split doesn't mean that it *will* be split. The actual
// behaviour depends on the IrcContext.ChunkSize value.
var IrcNumericsSafeToChunk = []int{
	// RPL_WHOREPLY
	352,
	// RPL_NAMREPLY
	353,
}

// SplitReply will split a reply message if necessary. See
// IrcNumericSafeToChunk for background on why splitting.
// The function will return a list of chunks to be sent
// separately.
// The first argument is the entire message to be split.
// The second argument is the chunk size to use to determine
// whether the message should be split. Any value equal or above
// 512 will cause splitting. Any other value will return the
// unmodified string as only item of the list.
func SplitReply(preamble, msg string, chunksize int) []string {
	if chunksize < 512 || chunksize >= len(preamble)+len(msg)+2 {
		// return the whole string as one chunk
		return []string{preamble + msg + "\r\n"}
	}
	log.Debugf("Splitting reply in %d-byte chunks", chunksize)
	// Split and build a string until it's long enough to fit the
	// chunk. Splitting ignores multiple contiguous white-spaces.
	// We assume this is safe (unless we find out it's not).
	// Additionally, squeezing multiple contiguous spaces could
	// render the final reply shorter than the chunk size, but we
	// don't care here.
	maxLen := chunksize - len(preamble) - 2
	lines := WordWrap(strings.Fields(msg), maxLen)
	reply := make([]string, len(lines))
	for idx, line := range lines {
		reply[idx] = preamble + line + "\r\n"
	}
	return reply
}

var (
	rxSlackUrls       = regexp.MustCompile(`<[^>]+>?`)
	rxSlackUser       = regexp.MustCompile(`<@[UW][A-Z0-9]+>`)
	rxSlackArchiveURL = regexp.MustCompile(`https?:\\/\\/[a-z0-9\\-]+\\.slack\\.com\\/archives\\/([a-zA-Z0-9]+)\\/p([0-9]{10})([0-9]{6})`)
)

// ExpandText expands and unquotes text and URLs from Slack's messages. Slack
// quotes the text and URLS, and the latter are enclosed in < and >. It also
// translates potential URLs into actual URLs (e.g. when you type "example.com"),
// so you will get something like <http://example.com|example.com>. This
// function tries to detect them and unquote and expand them for a better
// visualization on IRC.
func ExpandText(text string) string {

	text = rxSlackUrls.ReplaceAllStringFunc(text, func(subs string) string {
		if !strings.HasPrefix(subs, "<") && !strings.HasSuffix(subs, ">") {
			return subs
		}

		// Slack URLs may contain an URL followed by a "|", followed by the
		// original message. Detect the pipe and only parse the URL.
		var (
			slackURL = subs[1 : len(subs)-1]
			slackMsg string
		)
		idx := strings.LastIndex(slackURL, "|")
		if idx >= 0 {
			slackMsg = slackURL[idx+1:]
			slackURL = slackURL[:idx]
		}

		u, err := url.Parse(slackURL)
		if err != nil {
			return subs
		}
		// Slack escapes the URLs passed by the users, let's undo that
		//u.RawQuery = html.UnescapeString(u.RawQuery)
		if slackMsg == "" {
			return u.String()
		}
		return fmt.Sprintf("%s (%s)", slackMsg, u.String())
	})
	text = html.UnescapeString(text)
	return text
}

// SendIrcNumeric sends a numeric code message to the recipient
func SendIrcNumeric(ctx *IrcContext, code int, args, desc string) error {
	preamble := fmt.Sprintf(":%s %03d %s :", ctx.ServerName, code, args)
	//reply := fmt.Sprintf(":%s %03d %s :%s\r\n", ctx.ServerName, code, args, desc)
	chunks := SplitReply(preamble, desc, ctx.ChunkSize)
	for _, chunk := range chunks {
		log.Debugf("Sending numeric reply: %s", chunk)
		_, err := ctx.Conn.Write([]byte(chunk))
		if err != nil {
			return err
		}
	}
	return nil
}

// IrcSendChanInfoAfterJoin sends channel information to the user about a joined
// channel.
func IrcSendChanInfoAfterJoin(ctx *IrcContext, ch *Channel, members []slack.User) {
	IrcSendChanInfoAfterJoinCustom(ctx, ch.IRCName(), ch.ID, ch.Purpose.Value, members)
}

// IrcSendChanInfoAfterJoinCustom sends channel information to the user about a joined
// channel. It can be used as an alternative to IrcSendChanInfoAfterJoin when
// you need to specify custom chan name, id, and topic.
func IrcSendChanInfoAfterJoinCustom(ctx *IrcContext, chanName, chanID, topic string, members []slack.User) {
	memberNames := make([]string, 0, len(members))
	for _, m := range members {
		memberNames = append(memberNames, m.Name)
	}
	// TODO wrap all these Conn.Write into a function
	if _, err := ctx.Conn.Write([]byte(fmt.Sprintf(":%s JOIN %s\r\n", ctx.Mask(), chanName))); err != nil {
		log.Warningf("Failed to send IRC JOIN message: %v", err)
	}
	// RPL_TOPIC
	if err := SendIrcNumeric(ctx, 332, fmt.Sprintf("%s %s", ctx.Nick(), chanName), topic); err != nil {
		log.Warningf("Failed to send IRC TOPIC message: %v", err)
	}
	// RPL_NAMREPLY
	if len(members) > 0 {
		if err := SendIrcNumeric(ctx, 353, fmt.Sprintf("%s = %s", ctx.Nick(), chanName), strings.Join(memberNames, " ")); err != nil {
			log.Warningf("Failed to send IRC NAMREPLY message: %v", err)
		}
	}
	// RPL_ENDOFNAMES
	if err := SendIrcNumeric(ctx, 366, fmt.Sprintf("%s %s", ctx.Nick(), chanName), "End of NAMES list"); err != nil {
		log.Warningf("Failed to send IRC ENDOFNAMES message: %v", err)
	}
	log.Infof("Joined channel %s", chanName)
}

// joinChannel will join the channel with the given ID, name and topic, and send back a
// response to the IRC client
func joinChannel(ctx *IrcContext, ch *Channel) error {
	log.Infof("%s topic=%s members=%d", ch.IRCName(), ch.Purpose.Value, ch.NumMembers)
	// the channels are already joined, notify the IRC client of their
	// existence
	members, err := ChannelMembers(ctx, ch.ID)
	if err != nil {
		jErr := fmt.Errorf("Failed to fetch users in channel `%s (channel ID: %s): %v", ch.Name, ch.ID, err)
		ctx.SendUnknownError("%s", jErr.Error())
		return jErr
	}
	go IrcSendChanInfoAfterJoin(ctx, ch, members)
	return nil
}

// joinChannels gets all the available Slack channels and sends an IRC JOIN message
// for each of the joined channels on Slack
func joinChannels(ctx *IrcContext) error {
	for _, sch := range ctx.Channels.AsMap() {
		ch := Channel(sch)
		if !ch.IsPublicChannel() && !ch.IsPrivateChannel() {
			continue
		}
		if ch.IsMember {
			if err := joinChannel(ctx, &ch); err != nil {
				return err
			}
		}
	}
	return nil
}

// IrcAfterLoggingIn is called once the user has successfully logged on IRC
func IrcAfterLoggingIn(ctx *IrcContext, rtm *slack.RTM) error {
	if ctx.OrigName != ctx.Nick() {
		// Force the user into the Slack nick
		if _, err := ctx.Conn.Write([]byte(fmt.Sprintf(":%s NICK %s\r\n", ctx.OrigName, ctx.Nick()))); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	}
	// Send a welcome to the user, to let the client knows that it's connected
	// RPL_WELCOME
	if err := SendIrcNumeric(ctx, 1, ctx.Nick(), fmt.Sprintf("Welcome to the %s IRC chat, %s!", ctx.ServerName, ctx.Nick())); err != nil {
		log.Warningf("Failed to send IRC message: %v", err)
	}
	// RPL_MOTDSTART
	if err := SendIrcNumeric(ctx, 375, ctx.Nick(), ""); err != nil {
		log.Warningf("Failed to send IRC message: %v", err)
	}
	// RPL_MOTD
	motd := func(s string) {
		if err := SendIrcNumeric(ctx, 372, ctx.Nick(), s); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	}
	// RPL_ISUPPORT
	if err := SendIrcNumeric(ctx, 005, ctx.Nick(), "CHANTYPES="+strings.Join(SupportedChannelPrefixes(), "")); err != nil {
		log.Warningf("Failed to send IRC message: %v", err)
	}
	motd(fmt.Sprintf("This is an IRC-to-Slack gateway, written by %s <%s>.", ProjectAuthor, ProjectAuthorEmail))
	motd(fmt.Sprintf("More information at %s.", ProjectURL))
	motd(fmt.Sprintf("Slack team name: %s", ctx.SlackRTM.GetInfo().Team.Name))
	motd(fmt.Sprintf("Your user info: "))
	motd(fmt.Sprintf("  Name     : %s", ctx.User.Name))
	motd(fmt.Sprintf("  ID       : %s", ctx.User.ID))
	motd(fmt.Sprintf("  RealName : %s", ctx.User.RealName))
	// RPL_ENDOFMOTD
	if err := SendIrcNumeric(ctx, 376, ctx.Nick(), ""); err != nil {
		log.Warningf("Failed to send IRC message: %v", err)
	}

	// get channels
	if err := joinChannels(ctx); err != nil {
		return err
	}

	go eventHandler(ctx, rtm)
	return nil
}

// IrcCapHandler is called when a CAP command is sent
func IrcCapHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	if len(args) > 1 {
		if args[0] == "LS" {
			reply := fmt.Sprintf(":%s CAP * LS :\r\n", ctx.ServerName)
			if _, err := ctx.Conn.Write([]byte(reply)); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
		} else {
			log.Debugf("Got CAP %v", args)
		}
	}
}

// parseMentions parses mentions and converts them to the syntax that
// Slack will parse, i.e. <@nickname>
func parseMentions(text string) string {
	tokens := strings.Split(text, " ")
	for idx, token := range tokens {
		if token == "@here" {
			tokens[idx] = "<!here>"
		} else if token == "@channel" {
			tokens[idx] = "<!channel>"
		} else if token == "@everyone" {
			tokens[idx] = "<!everyone>"
		} else if strings.HasPrefix(token, "@") {
			tokens[idx] = "<" + token + ">"
		}
	}
	return strings.Join(tokens, " ")
}

func getTargetTs(channelName string) string {
	if !strings.HasPrefix(channelName, "+") {
		return ""
	}
	chanNameSplit := strings.Split(channelName, "-")
	return chanNameSplit[len(chanNameSplit)-1]
}

// IrcPrivMsgHandler is called when a PRIVMSG command is sent
func IrcPrivMsgHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	var channelParameter, text string
	switch len(args) {
	case 1:
		channelParameter = args[0]
		text = trailing
	case 2:
		channelParameter = args[0]
		text = args[1]
	default:
		log.Warningf("Invalid number of parameters for PRIVMSG, want 1 or 2, got %d", len(args))
	}
	if channelParameter == "" || text == "" {
		log.Warningf("Invalid PRIVMSG command args: %v %v", args, trailing)
		return
	}
	channel := ctx.Channels.ByName(channelParameter)
	target := ""
	if channel != nil {
		// known channel
		target = channel.SlackName()
	} else {
		// assume private message
		target = "@" + channelParameter
	}

	if strings.HasPrefix(text, "\x01ACTION ") && strings.HasSuffix(text, "\x01") {
		// The Slack API has a bug, where a chat.meMessage is
		// documented to accept a channel name or ID, but actually
		// only the channel ID will work. So until this is fixed,
		// resolve the channel ID for chat.meMessage .
		// TODO revert this when the bug in the Slack API is fixed
		key := target
		ch := ctx.Channels.ByName(key)
		if ch == nil {
			log.Warningf("Unknown channel ID for %s", key)
			return
		}
		target = ch.SlackName()

		// this is a MeMessage
		// strip off the ACTION and \x01 wrapper
		text = text[len("\x01ACTION ") : len(text)-1]
		/*
		 * workaround: I believe that there is an issue with the
		 * slack API for the method chat.meMessage . Until this
		 * is clarified, I will emulate a "me message" using a
		 * simple italic formatting for the message.
		 * See https://github.com/insomniacslk/irc-slack/pull/39
		 */
		// TODO once clarified the issue, restore the
		//      MsgOptionMeMessage, remove the MsgOptionAsUser,
		//      and remove the italic text
		//opts = append(opts, slack.MsgOptionMeMessage())
		text = "_" + text + "_"
	}
	ctx.PostTextMessage(
		target,
		parseMentions(text),
		getTargetTs(channelParameter),
	)
}

// wrapped logger that satisfies the slack.logger interface
type loggerWrapper struct {
	*logrus.Entry
}

func (l *loggerWrapper) Output(calldepth int, s string) error {
	l.Print(s)
	return nil
}

// custom HTTP client used to set the auth cookie if requested, and only over
// TLS.
type httpClient struct {
	c      http.Client
	cookie string
}

func (hc httpClient) Do(req *http.Request) (*http.Response, error) {
	if hc.cookie != "" {
		log.Debugf("Setting auth cookie")
		if strings.ToLower(req.URL.Scheme) == "https" {
			req.Header.Add("Cookie", hc.cookie)
		} else {
			log.Warning("Cookie is set but connection is not HTTPS, skipping")
		}
	}
	return hc.c.Do(req)
}

// passwordToTokenAndCookie parses the password specified by the user into a
// Slack token and optionally a cookie Auth cookies can be specified by
// appending a "|" symbol and the base64-encoded auth cookie to the Slack token.
func passwordToTokenAndCookie(p string) (string, string, error) {
	parts := strings.Split(p, "|")

	switch len(parts) {
	case 1:
		// XXX should check that the token starts with xoxp- ?
		return parts[0], "", nil
	case 2:
		if !strings.HasPrefix(parts[0], "xoxc-") {
			return "", "", errors.New("auth cookie is set, but token does not start with xoxc-")
		}
		if parts[1] == "" {
			return "", "", errors.New("auth cookie is empty")
		}
		if !strings.HasPrefix(parts[1], "d=") || !strings.HasSuffix(parts[1], ";") {
			return "", "", errors.New("auth cookie must have the format 'd=XXX;'")
		}
		return parts[0], parts[1], nil
	default:
		return "", "", fmt.Errorf("failed to parse password into token and cookie, got %d components, want 1 or 2", len(parts))
	}
}

func connectToSlack(ctx *IrcContext) error {
	token, cookie, err := passwordToTokenAndCookie(ctx.SlackAPIKey)
	if err != nil {
		return err
	}
	ctx.SlackClient = slack.New(
		token,
		slack.OptionDebug(ctx.SlackDebug),
		slack.OptionLog(&loggerWrapper{logger.GetLogger("slack-api")}),
		slack.OptionHTTPClient(&httpClient{cookie: cookie}),
	)
	if cookie == "" {
		// legacy token
		ctx.usingLegacyToken = true
	}
	rtm := ctx.SlackClient.NewRTM()
	ctx.SlackRTM = rtm
	go rtm.ManageConnection()
	log.Info("Starting Slack client")
	// Wait until the websocket is connected, then print client info
	var info *slack.Info
	// FIXME tune the timeout to a value that makes sense
	timeout := 10 * time.Second
	start := time.Now()
	for {
		if info = rtm.GetInfo(); info != nil {
			break
		}
		if time.Now().After(start.Add(timeout)) {
			return fmt.Errorf("Connection to Slack timed out after %v", timeout)
		}
		time.Sleep(100 * time.Millisecond)
	}
	log.Info("CLIENT INFO:")
	log.Infof("  URL     : %s", info.URL)
	log.Infof("  User    : %+v", *info.User)
	log.Infof("  Team    : %+v", *info.Team)
	// the users cache is not yet populated at this point, so we call the Slack
	// API directly.
	user, err := ctx.SlackClient.GetUserInfo(info.User.ID)
	if err != nil {
		return fmt.Errorf("Cannot get info for user %s (ID: %s): %v", info.User.Name, info.User.ID, err)
	}
	ctx.User = user
	ctx.RealName = user.RealName
	// do not fetch users here, they will be fetched later upon joining channels
	if err := ctx.Channels.Fetch(ctx.SlackClient); err != nil {
		ctx.Conn.Close()
		return fmt.Errorf("Failed to fetch channels: %v", err)
	}
	return IrcAfterLoggingIn(ctx, rtm)
}

// IrcNickHandler is called when a NICK command is sent
func IrcNickHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	nick := trailing
	if len(args) == 1 {
		nick = args[0]
	}
	if nick == "" {
		log.Warningf("Invalid NICK command args: %v %v", args, trailing)
		return
	}

	if ctx.SlackClient != nil {
		if nick != ctx.Nick() {
			// You cannot change nick, so force it back
			if _, err := ctx.Conn.Write([]byte(fmt.Sprintf(":%s NICK %s\r\n", nick, ctx.Nick()))); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
		}
		return
	}

	// We need the original nick later to change it
	ctx.OrigName = nick

	// If we're ready, connect
	if ctx.RealName != "" && ctx.SlackAPIKey != "" {
		if err := connectToSlack(ctx); err != nil {
			log.Warningf("Cannot connect to Slack: %v", err)
			// close the IRC connection to the client
			ctx.Conn.Close()
		}
	}
}

// IrcUserHandler is called when a USER command is sent
func IrcUserHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	// ignore the user-specified username. Will use the Slack ID instead
	// TODO get user info and set the real name with that info
	ctx.RealName = trailing

	// If we're ready, connect
	if ctx.SlackClient == nil && ctx.SlackAPIKey != "" && ctx.OrigName != "" {
		if err := connectToSlack(ctx); err != nil {
			log.Warningf("Cannot connect to Slack: %v", err)
			// close the IRC connection to the client
			ctx.Conn.Close()
		}
	}
}

// IrcPingHandler is called when a PING command is sent
func IrcPingHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	msg := fmt.Sprintf("PONG %s", strings.Join(args, " "))
	if trailing != "" {
		msg += " :" + trailing
	}
	if _, err := ctx.Conn.Write([]byte(msg + "\r\n")); err != nil {
		log.Warningf("Failed to send IRC message: %v", err)
	}
}

// IrcQuitHandler is called when a QUIT command is sent
func IrcQuitHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	ctx.Conn.Close()
}

// IrcModeHandler is called when a MODE command is sent
func IrcModeHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	switch len(args) {
	case 0:
		log.Warningf("Invalid call to MODE handler: no arguments passed")
	case 1:
		// get mode request. Always no mode (for now)
		mode := "+"
		// RPL_CHANNELMODEIS
		if err := SendIrcNumeric(ctx, 324, fmt.Sprintf("%s %s %s", ctx.Nick(), args[0], mode), ""); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	default:
		// more than 1
		// set mode request. Not handled yet
		// TODO handle mode set
		// ERR_UMODEUNKNOWNFLAG
		if err := SendIrcNumeric(ctx, 501, args[0], fmt.Sprintf("Unknown MODE flags %s", strings.Join(args[1:], " "))); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	}
}

// IrcPassHandler is called when a PASS command is sent
func IrcPassHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	if len(args) != 1 {
		log.Warningf("Invalid PASS arguments. Arguments are not shown for this method because they may contain Slack tokens or cookies")
		// ERR_PASSWDMISMATCH
		if err := SendIrcNumeric(ctx, 464, "", "Invalid password"); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		return
	}
	ctx.SlackAPIKey = args[0]
	ctx.FileHandler.SlackAPIKey = ctx.SlackAPIKey

	// If we're ready, connect
	if ctx.SlackClient == nil && ctx.RealName != "" && ctx.OrigName != "" {
		if err := connectToSlack(ctx); err != nil {
			log.Warningf("Cannot connect to Slack: %v", err)
			// close the IRC connection to the client
			ctx.Conn.Close()
		}
	}
}

// IrcWhoHandler is called when a WHO command is sent
func IrcWhoHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	sendErr := func() {
		ctx.SendUnknownError("Invalid WHO command. Syntax: WHO <nickname|channel>")
	}
	if len(args) != 1 && len(args) != 2 {
		sendErr()
		return
	}
	target := args[0]
	var rargs, desc string
	if HasChannelPrefix(target) {
		ch := ctx.Channels.ByName(target)
		if ch == nil {
			// ERR_NOSUCHCHANNEL
			if err := SendIrcNumeric(ctx, 403, ctx.Nick(), fmt.Sprintf("No such channel %s", target)); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
			return
		}
		for _, un := range ch.Members {
			// FIXME can we use the cached users?
			u := ctx.Users.ByID(un)
			if u == nil {
				log.Warningf("Failed to get info for user name '%s'", un)
				continue
			}
			log.Infof("%+v", u.Name)
			rargs = fmt.Sprintf("%s %s %s %s %s %s *", ctx.Nick(), target, u.ID, ctx.ServerName, ctx.ServerName, u.Name)
			desc = fmt.Sprintf("0 %s", u.RealName)
			// RPL_WHOREPLY
			// "<channel> <user> <host> <server> <nick> \
			//  <H|G>[*][@|+] :<hopcount> <real name>"
			if err := SendIrcNumeric(ctx, 352, rargs, desc); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
		}
		// RPL_ENDOFWHO
		// "<name> :End of /WHO list"
		if err := SendIrcNumeric(ctx, 315, fmt.Sprintf("%s %s", ctx.Nick(), target), "End of WHO list"); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		return
	}
	user := ctx.GetUserInfoByName(target)
	if user == nil {
		// ERR_NOSUCHNICK
		if err := SendIrcNumeric(ctx, 401, ctx.Nick(), fmt.Sprintf("No such nick %s", target)); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		return
	}
	// FIXME get channel
	rargs = fmt.Sprintf("#general %s %s %s %s %s *", ctx.Nick(), user.ID, ctx.ServerName, ctx.ServerName, user.Name)
	desc = fmt.Sprintf("0 %s", user.RealName)
	// RPL_WHOREPLY
	// "<channel> <user> <host> <server> <nick> \
	//  <H|G>[*][@|+] :<hopcount> <real name>"
	if err := SendIrcNumeric(ctx, 352, rargs, desc); err != nil {
		log.Warningf("Failed to send IRC message: %v", err)
	}
}

// IrcWhoisHandler is called when a WHOIS command is sent
func IrcWhoisHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	if len(args) != 1 && len(args) != 2 {
		ctx.SendUnknownError("Invalid WHOIS command. Syntax: WHOIS <username>")
		return
	}
	username := args[0]
	// if the second argument is the same as the first, it's a request of WHOIS
	// with idle time
	withIdleTime := false
	if len(args) == 2 && args[0] == args[1] {
		withIdleTime = true
	}
	user := ctx.GetUserInfoByName(username)
	if user == nil {
		// ERR_NOSUCHNICK
		if err := SendIrcNumeric(ctx, 401, ctx.Nick(), fmt.Sprintf("No such nick %s", username)); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	} else {
		// RPL_WHOISUSER
		// "<nick> <user> <host> * :<real name>"
		if err := SendIrcNumeric(ctx, 311, fmt.Sprintf("%s %s %s %s *", ctx.Nick(), username, user.ID, ctx.ServerName), user.RealName); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		// RPL_WHOISSERVER
		// "<nick> <server> :<server info>"
		if err := SendIrcNumeric(ctx, 312, fmt.Sprintf("%s %s %s", ctx.Nick(), username, ctx.ServerName), "irc-slack, https://github.com/insomniacslk/irc-slack"); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		// Send additional user status information, abusing the RPL_WHOISSERVER
		// reply. If there is a better method, please let us know!
		if user.Profile.StatusText != "" || user.Profile.StatusEmoji != "" {
			userStatus := fmt.Sprintf("user status: '%s' %s", user.Profile.StatusText, user.Profile.StatusEmoji)
			if user.Profile.StatusExpiration != 0 {
				userStatus += " until " + time.Unix(int64(user.Profile.StatusExpiration), 0).String()
			}
			if err := SendIrcNumeric(ctx, 312, fmt.Sprintf("%s %s %s", ctx.Nick(), username, ctx.ServerName), userStatus); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
		}
		// RPL_WHOISCHANNELS
		// "<nick> :{[@|+]<channel><space>}"
		var channels []string
		for chname, ch := range ctx.Channels.AsMap() {
			for _, u := range ch.Members {
				if u == user.ID {
					channels = append(channels, chname)
				}
			}
		}
		if err := SendIrcNumeric(ctx, 319, fmt.Sprintf("%s %s", ctx.Nick(), username), strings.Join(channels, " ")); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		if withIdleTime {
			// TODO send RPL_WHOISIDLE (317)
			// "<nick> <integer> :seconds idle"
		}
		// RPL_ENDOFWHOIS
		// "<nick> :End of /WHOIS list"
		if err := SendIrcNumeric(ctx, 318, fmt.Sprintf("%s %s", ctx.Nick(), username), ":End of /WHOIS list"); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	}
}

// IrcJoinHandler is called when a JOIN command is sent
func IrcJoinHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	if len(args) != 1 {
		ctx.SendUnknownError("Invalid JOIN command")
		return
	}
	// Because it is possible for an IRC Client to join multiple channels
	// via a multi join (e.g. /join #chan1,#chan2,#chan3) the argument
	// needs to be splitted by commas and each channel needs to be joined
	// separately.
	channames := strings.Split(args[0], ",")
	for _, channame := range channames {
		if strings.HasPrefix(channame, ChannelPrefixMpIM) || strings.HasPrefix(channame, ChannelPrefixThread) {
			log.Debugf("JOIN: ignoring channel `%s`, cannot join multi-party IMs or threads", channame)
			continue
		}
		sch, _, _, err := ctx.SlackClient.JoinConversation(channame)
		if err != nil {
			log.Warningf("Cannot join channel %s: %v", channame, err)
			continue
		}
		log.Infof("Joined channel %s", channame)
		ch := Channel(*sch)
		if err := joinChannel(ctx, &ch); err != nil {
			log.Warningf("Failed to join channel `%s`: %v", ch.Name, err)
			continue
		}
	}
}

// IrcPartHandler is called when a PART command is sent
func IrcPartHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	if len(args) != 1 {
		ctx.SendUnknownError("Invalid PART command")
		return
	}
	channame := StripChannelPrefix(args[0])
	// Slack needs the channel ID to leave it, not the channel name. The only
	// way to get the channel ID from the name is retrieving the whole channel
	// list and finding the one whose name is the one we want to leave
	if err := ctx.Channels.Fetch(ctx.SlackClient); err != nil {
		log.Warningf("Cannot leave channel %s: %v", channame, err)
		ctx.SendUnknownError("Cannot leave channel: %v", err)
		return
	}
	var chanID string
	for _, ch := range ctx.Channels.AsMap() {
		if ch.Name == channame {
			chanID = ch.ID
			log.Debugf("Trying to leave channel: %+v", ch)
			break
		}
	}
	if chanID == "" {
		// ERR_USERNOTINCHANNEL
		if err := SendIrcNumeric(ctx, 441, ctx.Nick(), fmt.Sprintf("User is not in channel %s", channame)); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
			return
		}
		notInChan, err := ctx.SlackClient.LeaveConversation(chanID)
		if err != nil {
			log.Warningf("Cannot leave channel %s (id: %s): %v", channame, chanID, err)
			return
		}
		if notInChan {
			// ERR_USERNOTINCHANNEL
			if err := SendIrcNumeric(ctx, 441, ctx.Nick(), fmt.Sprintf("User is not in channel %s", channame)); err != nil {
				log.Warningf("Failed to send IRC message: %v", err)
			}
			return
		}
		log.Debugf("Left channel %s", channame)
		if _, err := ctx.Conn.Write([]byte(fmt.Sprintf(":%v PART #%v\r\n", ctx.Mask(), channame))); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
	}
}

// IrcTopicHandler is called when a TOPIC command is sent
func IrcTopicHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	if len(args) < 1 {
		// ERR_NEEDMOREPARAMS
		if err := SendIrcNumeric(ctx, 461, ctx.Nick(), "TOPIC :Not enough parameters"); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		return
	}
	channame := args[0]
	topic := trailing
	channel := ctx.Channels.ByName(channame)
	if channel == nil {
		log.Warningf("IrcTopicHandler: unknown channel %s", channame)
		return
	}
	newTopic, err := ctx.SlackClient.SetPurposeOfConversation(channel.ID, topic)
	if err != nil {
		ctx.SendUnknownError("%s :Cannot set topic: %v", channame, err)
		return
	}
	// RPL_TOPIC
	if err := SendIrcNumeric(ctx, 332, fmt.Sprintf("%s :%s", ctx.Nick(), channame), newTopic.Purpose.Value); err != nil {
		log.Warningf("Failed to send IRC message: %v", err)
	}
}

// IrcNamesHandler is called when a NAMES command is sent
func IrcNamesHandler(ctx *IrcContext, prefix, cmd string, args []string, trailing string) {
	if len(args) < 1 {
		// ERR_NEEDMOREPARAMS
		if err := SendIrcNumeric(ctx, 461, ctx.Nick(), "NAMES :Not enough parameters"); err != nil {
			log.Warningf("Failed to send IRC message: %v", err)
		}
		return
	}
	ch := ctx.Channels.ByName(args[0])
	if ch == nil {
		ctx.SendUnknownError("Channel `%s` not found", args[0])
		return
	}

	members, err := ChannelMembers(ctx, ch.ID)
	if err != nil {
		jErr := fmt.Errorf("Failed to fetch users in channel `%s (channel ID: %s): %v", ch.Name, ch.ID, err)
		ctx.SendUnknownError("%s", jErr.Error())
		return
	}
	memberNames := make([]string, 0, len(members))
	for _, m := range members {
		memberNames = append(memberNames, m.Name)
	}
	log.Printf("Found %d members in %s: %v", len(memberNames), ch.IRCName(), memberNames)
	// RPL_NAMREPLY
	if len(members) > 0 {
		if err := SendIrcNumeric(ctx, 353, fmt.Sprintf("%s = %s", ctx.Nick(), ch.IRCName()), strings.Join(memberNames, " ")); err != nil {
			log.Warningf("Failed to send IRC NAMREPLY message: %v", err)
		}
	}
	// RPL_ENDOFNAMES
	if err := SendIrcNumeric(ctx, 366, fmt.Sprintf("%s %s", ctx.Nick(), ch.IRCName()), "End of NAMES list"); err != nil {
		log.Warningf("Failed to send IRC ENDOFNAMES message: %v", err)
	}
}


================================================
FILE: pkg/ircslack/logger.go
================================================
package ircslack

import "github.com/coredhcp/coredhcp/logger"

var log = logger.GetLogger("ircslack")


================================================
FILE: pkg/ircslack/server.go
================================================
package ircslack

import (
	"bufio"
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"strings"

	"github.com/slack-go/slack"
)

// Server is the server object that exposes the Slack API with an IRC interface.
type Server struct {
	Name                 string
	LocalAddr            net.Addr
	Listener             net.Listener
	SlackAPIKey          string
	SlackDebug           bool
	ChunkSize            int
	FileDownloadLocation string
	FileProxyPrefix      string
	Pagination           int
	TLSConfig            *tls.Config
}

// Start runs the IRC server
func (s Server) Start() error {
	var err error
	if s.TLSConfig != nil {
		s.Listener, err = tls.Listen("tcp", s.LocalAddr.String(), s.TLSConfig)
	} else {
		s.Listener, err = net.Listen("tcp", s.LocalAddr.String())
	}
	if err != nil {
		return err
	}
	defer s.Listener.Close()
	log.Infof("Listening on %v", s.LocalAddr)
	for {
		conn, err := s.Listener.Accept()
		if err != nil {
			return fmt.Errorf("Error accepting: %v", err)
		}
		go s.HandleRequest(conn)
	}
}

// HandleRequest handle IRC client connections
func (s Server) HandleRequest(conn net.Conn) {
	defer conn.Close()
	reader := bufio.NewReader(conn)
	for {
		line, err := reader.ReadString('\n')
		if err != nil {
			// clean up this client's state
			delete(UserContexts, conn.RemoteAddr())
			if err == io.EOF {
				log.Warningf("Client %v disconnected", conn.RemoteAddr())
				break
			}
			log.Warningf("Error handling connection from %v: %v", conn.RemoteAddr(), err)
			break
		}
		s.HandleMsg(conn, string(line))
	}
}

// HandleMsg handles raw IRC messages
func (s *Server) HandleMsg(conn net.Conn, msg string) {
	if strings.HasPrefix(msg, "PASS ") {
		log.Debugf("%v: PASS ***** (redacted for privacy)", conn.RemoteAddr())
	} else {
		log.Debugf("%v: %v", conn.RemoteAddr(), msg)
	}
	if len(msg) < 1 {
		log.Warningf("Invalid message: '%v'", msg)
		return
	}
	var (
		prefix, data string
	)
	if msg[0] == ':' {
		prefix = strings.SplitN(msg[1:], " ", 1)[0]
		data = msg[len(prefix)+1:]
	} else {
		prefix = ""
		data = msg
	}
	if !strings.HasSuffix(data, "\r\n") {
		log.Warning("Invalid data: not terminated with <CR><LF>")
		return
	}
	data = data[:len(data)-2]

	tokens := strings.Split(data, " ")
	cmd := tokens[0]
	args := tokens[1:]
	var trailing string
	for idx, arg := range args {
		if strings.HasPrefix(arg, ":") {
			trailing = strings.Join(args[idx:], " ")[1:]
			args = args[:idx]
			break
		}
	}
	handler, ok := IrcCommandHandlers[cmd]
	if !ok {
		log.Warningf("No handler found for %v", cmd)
		return
	}
	ctx, ok := UserContexts[conn.RemoteAddr()]
	if !ok || ctx == nil {
		ctx = &IrcContext{
			Conn:              conn,
			ServerName:        s.Name,
			SlackAPIKey:       s.SlackAPIKey,
			SlackDebug:        s.SlackDebug,
			ChunkSize:         s.ChunkSize,
			postMessage:       make(chan SlackPostMessage),
			conversationCache: make(map[string]*slack.Channel),
			FileHandler: &FileHandler{
				SlackAPIKey:          s.SlackAPIKey,
				FileDownloadLocation: s.FileDownloadLocation,
				ProxyPrefix:          s.FileProxyPrefix,
			},
			Users:    NewUsers(s.Pagination),
			Channels: NewChannels(s.Pagination),
		}
		go ctx.Start()
		UserContexts[conn.RemoteAddr()] = ctx
	}
	handler(ctx, prefix, cmd, args, trailing)
}


================================================
FILE: pkg/ircslack/users.go
================================================
package ircslack

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/slack-go/slack"
)

// Users wraps the user list with convenient operations and cache.
type Users struct {
	users      map[string]slack.User
	mu         sync.Mutex
	pagination int
}

// NewUsers creates a new Users object.
func NewUsers(pagination int) *Users {
	return &Users{
		users:      make(map[string]slack.User),
		pagination: pagination,
	}
}

// FetchByIDs fetches the users with the specified IDs and updates the internal
// user mapping.
func (u *Users) FetchByIDs(client *slack.Client, skipCache bool, userIDs ...string) ([]slack.User, error) {
	var (
		toRetrieve       []string
		alreadyRetrieved []slack.User
	)

	if !skipCache {
		u.mu.Lock()
		for _, uid := range userIDs {
			if u, ok := u.users[uid]; !ok {
				toRetrieve = append(toRetrieve, uid)
			} else {
				alreadyRetrieved = append(alreadyRetrieved, u)
			}
		}
		u.mu.Unlock()
		log.Debugf("Fetching information for %d users out of %d (%d already in cache)", len(toRetrieve), len(userIDs), len(userIDs)-len(toRetrieve))
	} else {
		toRetrieve = userIDs
	}
	chunkSize := 1000
	allFetchedUsers := make([]slack.User, 0, len(userIDs))
	for i := 0; i < len(toRetrieve); i += chunkSize {
		upperLimit := i + chunkSize
		if upperLimit > len(toRetrieve) {
			upperLimit = len(toRetrieve)
		}
		for {
			attempt := 0
			if attempt >= MaxSlackAPIAttempts {
				return nil, fmt.Errorf("Users.FetchByIDs: exceeded the maximum number of attempts (%d) with the Slack API", MaxSlackAPIAttempts)
			}
			log.Debugf("Fetching %d users of %d, attempt %d of %d", len(toRetrieve), len(userIDs), attempt+1, MaxSlackAPIAttempts)
			slackUsers, err := client.GetUsersInfo(toRetrieve[i:upperLimit]...)
			if err != nil {
				if rlErr, ok := err.(*slack.RateLimitedError); ok {
					// we were rate-limited. Let's wait the recommended delay
					log.Warningf("Hit Slack API rate limiter. Waiting %v", rlErr.RetryAfter)
					time.Sleep(rlErr.RetryAfter)
					attempt++
					continue
				}
				return nil, err
			}
			if len(*slackUsers) != len(toRetrieve[i:upperLimit]) {
				log.Warningf("Tried to fetch %d users but only got %d", len(toRetrieve[i:upperLimit]), len(*slackUsers))
			}
			allFetchedUsers = append(allFetchedUsers, *slackUsers...)
			// also update the local users map
			u.mu.Lock()
			for _, user := range *slackUsers {
				u.users[user.ID] = user
			}
			u.mu.Unlock()
			break
		}
	}
	allUsers := append(alreadyRetrieved, allFetchedUsers...)
	if len(userIDs) != len(allUsers) {
		return allFetchedUsers, fmt.Errorf("Found %d users but %d were requested", len(allUsers), len(userIDs))
	}
	return allUsers, nil
}

// Fetch retrieves all the users on a given Slack team. The Slack client has to
// be valid and connected.
func (u *Users) Fetch(client *slack.Client) ([]slack.User, error) {
	log.Infof("Fetching all users, might take a while on large Slack teams")
	var opts []slack.GetUsersOption
	if u.pagination > 0 {
		log.Debugf("Setting user pagination to %d", u.pagination)
		opts = append(opts, slack.GetUsersOptionLimit(u.pagination))
	}
	up := client.GetUsersPaginated(opts...)
	var (
		err   error
		ctx   = context.Background()
		users = make(map[string]slack.User)
	)
	start := time.Now()
	var allFetchedUsers []slack.User
	for err == nil {
		up, err = up.Next(ctx)
		if err == nil {
			log.Debugf("Retrieved %d users (current total is %d)", len(up.Users), len(users))
			for _, u := range up.Users {
				users[u.ID] = u
			}
			allFetchedUsers = append(allFetchedUsers, up.Users...)
		} else if rateLimitedError, ok := err.(*slack.RateLimitedError); ok {
			select {
			case <-ctx.Done():
				err = ctx.Err()
			case <-time.After(rateLimitedError.RetryAfter):
				err = nil
			}
		}
	}
	log.Infof("Retrieved %d users in %s", len(users), time.Since(start))
	err = up.Failure(err)
	if err != nil {
		log.Warningf("Failed to get users: %v", err)
	}
	u.mu.Lock()
	u.users = users
	u.mu.Unlock()
	return allFetchedUsers, nil
}

// Count returns the number of users. This method must be called after `Fetch`.
func (u *Users) Count() int {
	return len(u.users)
}

// ByID retrieves a user by its Slack ID.
func (u *Users) ByID(id string) *slack.User {
	u.mu.Lock()
	defer u.mu.Unlock()
	for _, u := range u.users {
		if u.ID == id {
			return &u
		}
	}
	return nil
}

// ByName retrieves a user by its Slack name.
func (u *Users) ByName(name string) *slack.User {
	u.mu.Lock()
	defer u.mu.Unlock()
	for _, u := range u.users {
		if u.Name == name {
			return &u
		}
	}
	return nil
}

// IDsToNames returns a list of user names from the given IDs. The
// returned list could be shorter if there are invalid user IDs.
// Warning: this method is probably only useful for NAMES commands
// where a non-exact mapping is acceptable.
func (u *Users) IDsToNames(userIDs ...string) []string {
	u.mu.Lock()
	defer u.mu.Unlock()
	names := make([]string, 0)
	for _, uid := range userIDs {
		if u, ok := u.users[uid]; ok {
			names = append(names, u.Name)
		} else {
			log.Warningf("IDsToNames: unknown user ID %s", uid)
		}
	}
	return names
}


================================================
FILE: pkg/ircslack/users_test.go
================================================
package ircslack

import (
	"bytes"
	"fmt"
	"io/ioutil"
	"net/http"
	"testing"

	"github.com/slack-go/slack"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestUsersNewUsers(t *testing.T) {
	u := NewUsers(0)
	require.NotNil(t, u)
	assert.NotNil(t, u.users)
	assert.Equal(t, 0, u.pagination)

	u = NewUsers(200)
	require.NotNil(t, u)
	assert.NotNil(t, u.users)
	assert.Equal(t, 200, u.pagination)
}

type fakeErrorUsersPaginationComplete struct{}

type fakeUsersResponse struct {
	Members []slack.User
	User    slack.User
}

func (f fakeErrorUsersPaginationComplete) Error() string {
	return "pagination complete"
}

type fakeSlackHTTPClient struct{}

func (c fakeSlackHTTPClient) Do(req *http.Request) (*http.Response, error) {
	switch req.URL.Path {
	case "/api/users.list":
		// reply as per https://api.slack.com/methods/users.list
		data := []byte(`{"ok": true, "members": [{"id": "UABCD", "name": "insomniac"}], "response_metadata": {"next_cursor": ""}}`)
		return &http.Response{
			Status:     "200 OK",
			StatusCode: 200,
			Proto:      "HTTP/1.1",
			ProtoMajor: 1,
			ProtoMinor: 1,
			Body:       ioutil.NopCloser(bytes.NewBuffer(data)),
		}, nil
	default:
		return nil, fmt.Errorf("testing: http client URL not supported: %s", req.URL)
	}
}

func TestUsersFetch(t *testing.T) {
	client := slack.New("test-token", slack.OptionHTTPClient(fakeSlackHTTPClient{}))
	users := NewUsers(10)
	fetched, err := users.Fetch(client)
	require.NoError(t, err)
	assert.Equal(t, 1, users.Count())
	assert.Equal(t, 1, len(fetched))
}

func TestUsersById(t *testing.T) {
	client := slack.New("test-token", slack.OptionHTTPClient(fakeSlackHTTPClient{}))
	users := NewUsers(10)
	_, err := users.Fetch(client)
	require.NoError(t, err)
	u := users.ByID("UABCD")
	require.NotNil(t, u)
	assert.Equal(t, "UABCD", u.ID)
	assert.Equal(t, "insomniac", u.Name)
}

func TestUsersByName(t *testing.T) {
	client := slack.New("test-token", slack.OptionHTTPClient(fakeSlackHTTPClient{}))
	users := NewUsers(10)
	_, err := users.Fetch(client)
	require.NoError(t, err)
	u := users.ByName("insomniac")
	require.NotNil(t, u)
	assert.Equal(t, "UABCD", u.ID)
	assert.Equal(t, "insomniac", u.Name)
}

func TestUsersIDsToNames(t *testing.T) {
	client := slack.New("test-token", slack.OptionHTTPClient(fakeSlackHTTPClient{}))
	users := NewUsers(10)
	_, err := users.Fetch(client)
	require.NoError(t, err)
	names := users.IDsToNames("UABCD")
	assert.Equal(t, []string{"insomniac"}, names)
}


================================================
FILE: pkg/ircslack/wordwrap.go
================================================
package ircslack

import (
	"strings"
)

// WordWrap wraps the given words up to the maximum specified length.
// If a single word is longer than the max length, it is truncated.
func WordWrap(allWords []string, maxLen int) []string {
	var (
		lines  []string
		curLen int
		words  []string
	)
	for _, word := range allWords {
		// curLen + len(words) + len(word) is the length of the current
		// line including spaces
		if curLen+len(words)+len(word) > maxLen {
			// we have our line. That does not include the current word
			lines = append(lines, strings.Join(words, " "))
			// reset the current line, add the current word
			words = []string{word}
			curLen = len(word)
		} else {
			words = append(words, word)
			curLen += len(word)
		}
	}
	if len(words) > 0 {
		// there's one last line to add
		lines = append(lines, strings.Join(words, " "))
	}
	for idx, line := range lines {
		if len(line) > maxLen {
			// truncate
			lines[idx] = line[:maxLen]
		}
	}
	return lines
}


================================================
FILE: pkg/ircslack/wordwrap_test.go
================================================
package ircslack

import (
	"strings"
	"testing"

	"github.com/stretchr/testify/require"
)

var fox = "The quick brown fox jumps over the lazy dog"

func TestWordWrapMultiLine(t *testing.T) {
	words := strings.Fields(fox)
	wrapped := WordWrap(words, 10)
	require.Equal(t, 5, len(wrapped))
	require.Equal(t, "The quick", wrapped[0])
	require.Equal(t, "brown fox", wrapped[1])
	require.Equal(t, "jumps over", wrapped[2])
	require.Equal(t, "the lazy", wrapped[3])
	require.Equal(t, "dog", wrapped[4])
}

func TestWordWrapSingleLine(t *testing.T) {
	words := strings.Fields(fox)
	wrapped := WordWrap(words, 100)
	require.Equal(t, 1, len(wrapped))
	require.Equal(t, fox, wrapped[0])
}

func TestWordWrapTruncate(t *testing.T) {
	words := strings.Fields(fox)
	wrapped := WordWrap(words, 3)
	require.Equal(t, 9, len(wrapped))
	require.Equal(t, "The", wrapped[0])
	require.Equal(t, "qui", wrapped[1])
	require.Equal(t, "bro", wrapped[2])
	require.Equal(t, "fox", wrapped[3])
	require.Equal(t, "jum", wrapped[4])
	require.Equal(t, "ove", wrapped[5])
	require.Equal(t, "the", wrapped[6])
	require.Equal(t, "laz", wrapped[7])
	require.Equal(t, "dog", wrapped[8])
}


================================================
FILE: tools/autotoken/main.go
================================================
// autotoken retrieves a Slack token and cookie using your Slack team
// credentials.
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"strings"
	"time"

	"github.com/chromedp/cdproto/runtime"
	"github.com/chromedp/cdproto/storage"
	"github.com/chromedp/chromedp"
	"github.com/spf13/pflag"
)

var (
	flagDebug       = pflag.BoolP("debug", "d", false, "Enable debug log")
	flagShowBrowser = pflag.BoolP("show-browser", "b", false, "show browser, useful for debugging")
	flagChromePath  = pflag.StringP("chrome-path", "c", "", "Custom path for chrome browser")
	flagTimeout     = pflag.DurationP("timeout", "t", 5*time.Minute, "Timeout")
)

func main() {
	usage := func() {
		fmt.Fprintf(os.Stderr, "autotoken: log into slack team and get token and cookie.\n\n")
		fmt.Fprintf(os.Stderr, "Usage: %s [-d|--debug] [-m|--mfa <token>] [-g|--gdpr] teamname[.slack.com]\n\n", os.Args[0])
		pflag.PrintDefaults()
		os.Exit(1)
	}
	pflag.Usage = usage
	pflag.Parse()
	if len(pflag.Args()) < 1 {
		usage()
	}
	team := pflag.Arg(0)

	timeout := *flagTimeout
	token, cookie, err := fetchCredentials(context.TODO(), team, timeout, *flagDebug, *flagChromePath)
	if err != nil {
		log.Fatalf("Failed to fetch credentials for team `%s`: %v", team, err)
	}

	fmt.Printf("%s|%s\n", token, cookie)
}

// fetchCredentials fetches Slack token and cookie for a given team.
func fetchCredentials(ctx context.Context, team string, timeout time.Duration, doDebug bool, chromePath string) (string, string, error) {
	if !strings.HasSuffix(team, ".slack.com") {
		team += ".slack.com"
	}

	var cancel func()
	ctx, cancel = context.WithTimeout(ctx, timeout)
	defer cancel()

	// show browser
	var allocatorOpts []chromedp.ExecAllocatorOption
	if *flagShowBrowser {
		allocatorOpts = append(allocatorOpts, chromedp.NoFirstRun, chromedp.NoDefaultBrowserCheck)
	} else {
		allocatorOpts = append(allocatorOpts, chromedp.Headless)
	}
	if chromePath != "" {
		allocatorOpts = append(allocatorOpts, chromedp.ExecPath(chromePath))
	}
	ctx, cancel = chromedp.NewExecAllocator(ctx, allocatorOpts...)
	defer cancel()

	var opts []chromedp.ContextOption
	if doDebug {
		opts = append(opts, chromedp.WithDebugf(log.Printf))
	}

	ctx, cancel = chromedp.NewContext(ctx, opts...)
	defer cancel()

	fmt.Fprintf(os.Stderr, "Fetching token and cookie for %s \n", team)
	// run chromedp tasks
	return extractTokenAndCookie(ctx, team)
}

// extractTokenAndCookie extracts Slack token and cookie from an existing
// context.
func extractTokenAndCookie(ctx context.Context, team string) (string, string, error) {
	teamURL := "https://" + team
	var token, cookie string
	tasks := chromedp.Tasks{
		chromedp.Navigate(teamURL),
		chromedp.WaitVisible(".p-workspace__primary_view_contents"),
		chromedp.ActionFunc(func(ctx context.Context) error {
			v, exp, err := runtime.Evaluate(`q=JSON.parse(localStorage.localConfig_v2)["teams"]; q[Object.keys(q)[0]]["token"]`).Do(ctx)
			if err != nil {
				return err
			}
			if exp != nil {
				return exp
			}
			if err := json.Unmarshal(v.Value, &token); err != nil {
				return fmt.Errorf("failed to unmarshal token: %v", err)
			}
			return nil
		}),
		chromedp.ActionFunc(func(ctx context.Context) error {
			cookies, err := storage.GetCookies().Do(ctx)
			if err != nil {
				return err
			}

			for _, c := range cookies {
				if c.Name == "d" {
					cookie = fmt.Sprintf("d=%s;", c.Value)
				}
			}

			return nil
		}),
	}
	if err := chromedp.Run(ctx, tasks); err != nil {
		return "", "", err
	}
	return token, cookie, nil
}


================================================
FILE: tools/slackapp/main.go
================================================
package main

/* Slack Oauth app built according to
 * https://api.slack.com/authentication/oauth-v2
 */

import (
	"encoding/json"
	"flag"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"os"
	"strings"

	log "github.com/sirupsen/logrus"
)

var (
	// irc-slack app client ID, see https://api.slack.com/apps/
	clientID     = os.Getenv("SLACK_APP_CLIENT_ID")
	clientSecret = os.Getenv("SLACK_APP_CLIENT_SECRET")
)

func httpStatus(w http.ResponseWriter, r *http.Request, statusCode int, fmtstr string, args ...interface{}) {
	w.WriteHeader(statusCode)
	msg := fmt.Sprintf(fmtstr, args...)
	fullmsg := fmt.Sprintf("%d - %s\n%s", statusCode, http.StatusText(statusCode), msg)
	if _, err := w.Write([]byte(fullmsg)); err != nil {
		log.Warningf("Cannot write response: %v", err)
	}
}

type slackChallenge struct {
	Token, Challenge, Type string
}

func handleSlackChallenge(w http.ResponseWriter, r *http.Request) {
	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Infof("Cannot read body: %v", err)
		httpStatus(w, r, 500, "")
		return
	}
	var sc slackChallenge
	if err := json.Unmarshal(data, &sc); err != nil {
		log.Infof("Cannot unmarshal JSON: %v", err)
		httpStatus(w, r, 400, "")
		return
	}
	if _, err := w.Write([]byte(sc.Challenge)); err != nil {
		log.Warningf("Failed to write response: %v", err)
	}
}

func handleSlackAuth(w http.ResponseWriter, r *http.Request) {
	code := r.URL.Query()["code"]
	if len(code) < 1 {
		log.Info("Missing \"code\" parameter in request")
		httpStatus(w, r, 400, "")
		return
	}
	// get token from Slack
	form := url.Values{}
	form.Add("code", code[0])
	form.Add("client_id", clientID)
	form.Add("client_secret", clientSecret)
	accessURL := "https://slack.com/api/oauth.access"

	client := http.Client{}
	req, err := http.NewRequest("POST", accessURL, strings.NewReader(form.Encode()))
	if err != nil {
		log.Infof("Failed to build request for Slack auth API: %v", err)
		httpStatus(w, r, 500, "")
		return
	}
	req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
	resp, err := client.Do(req)
	if err != nil {
		log.Infof("Failed to request token to Slack auth API: %v", err)
		httpStatus(w, r, 500, "")
		return
	}
	defer resp.Body.Close()
	data, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		log.Infof("Failed to read body from Slack auth API response: %v", err)
		httpStatus(w, r, 500, "")
		return
	}
	// parse the response message
	// Documented at https://api.slack.com/methods/oauth.access .
	// There are other fields, but we don't care about them here.
	type authmsg struct {
		Ok           bool    `json:"ok"`
		Error        string  `json:"error"`
		AccessToken  string  `json:"access_token"`
		TeamName     string  `json:"team_name"`
		TeamID       string  `json:"team_id"`
		Scope        string  `json:"scope"`
		EnterpriseID *string `json:"enterprise_id,omitempty"`
	}
	var msg authmsg
	if err := json.Unmarshal(data, &msg); err != nil {
		log.Infof("Failed to unmarshal API response response: %v", err)
		httpStatus(w, r, 500, "")
	}
	if !msg.Ok {
		log.Infof("Got error response from auth API: %s", msg.Error)
		httpStatus(w, r, 500, "")
		return
	}
	indented, err := json.MarshalIndent(msg, "", "    ")
	if err != nil {
		log.Infof("Cannot re-marshal API response with indentation: %v", err)
		httpStatus(w, r, 500, "")
		return
	}
	httpStatus(w, r, 200, "%s", string(indented))
}

func main() {
	flag.Parse()
	addr := ":2020"
	if flag.Arg(0) != "" {
		addr = flag.Arg(0)
	}
	if clientID == "" {
		log.Fatalf("SLACK_APP_CLIENT_ID is empty or not set")
	}
	if clientSecret == "" {
		log.Fatalf("SLACK_APP_CLIENT_SECRET is empty or not set")
	}
	http.HandleFunc("/irc-slack/challenge/", handleSlackChallenge)
	http.HandleFunc("/irc-slack/auth/", handleSlackAuth)
	log.Printf("Listening on %s", addr)
	log.Fatal(http.ListenAndServe(addr, nil))
}
Download .txt
gitextract_xhv39y9c/

├── .dockerignore
├── .github/
│   ├── dependabot.yml
│   └── workflows/
│       ├── docker.yml
│       └── tests.yml
├── .gitignore
├── .stickler.yml
├── Dockerfile
├── Dockerfile.autotoken
├── LICENSE
├── README.md
├── cmd/
│   └── irc-slack/
│       ├── Makefile
│       └── main.go
├── docker-compose.yml
├── go.mod
├── go.sum
├── pkg/
│   └── ircslack/
│       ├── channel.go
│       ├── channels.go
│       ├── channels_test.go
│       ├── event_handler.go
│       ├── file_handler.go
│       ├── irc_context.go
│       ├── irc_server.go
│       ├── logger.go
│       ├── server.go
│       ├── users.go
│       ├── users_test.go
│       ├── wordwrap.go
│       └── wordwrap_test.go
└── tools/
    ├── autotoken/
    │   └── main.go
    └── slackapp/
        └── main.go
Download .txt
SYMBOL INDEX (130 symbols across 15 files)

FILE: cmd/irc-slack/main.go
  function getLogLevels (line 52) | func getLogLevels() []string {
  function main (line 60) | func main() {

FILE: pkg/ircslack/channel.go
  constant ChannelPrefixPublicChannel (line 16) | ChannelPrefixPublicChannel  = "#"
  constant ChannelPrefixPrivateChannel (line 17) | ChannelPrefixPrivateChannel = "@"
  constant ChannelPrefixMpIM (line 18) | ChannelPrefixMpIM           = "&"
  constant ChannelPrefixThread (line 20) | ChannelPrefixThread = "+"
  function HasChannelPrefix (line 25) | func HasChannelPrefix(name string) bool {
  function StripChannelPrefix (line 39) | func StripChannelPrefix(name string) string {
  function ChannelMembers (line 47) | func ChannelMembers(ctx *IrcContext, channelID string) ([]slack.User, er...
  type Channel (line 95) | type Channel
    method IsPublicChannel (line 98) | func (c *Channel) IsPublicChannel() bool {
    method IsPrivateChannel (line 103) | func (c *Channel) IsPrivateChannel() bool {
    method IsMP (line 108) | func (c *Channel) IsMP() bool {
    method IRCName (line 117) | func (c *Channel) IRCName() string {
    method SlackName (line 138) | func (c *Channel) SlackName() string {

FILE: pkg/ircslack/channels.go
  type Channels (line 13) | type Channels struct
    method AsMap (line 40) | func (c *Channels) AsMap() map[string]Channel {
    method FetchByIDs (line 52) | func (c *Channels) FetchByIDs(client *slack.Client, skipCache bool, ch...
    method Fetch (line 109) | func (c *Channels) Fetch(client *slack.Client) error {
    method Count (line 158) | func (c *Channels) Count() int {
    method ByID (line 163) | func (c *Channels) ByID(id string) *Channel {
    method ByName (line 175) | func (c *Channels) ByName(name string) *Channel {
  function NewChannels (line 20) | func NewChannels(pagination int) *Channels {
  function SupportedChannelPrefixes (line 28) | func SupportedChannelPrefixes() []string {

FILE: pkg/ircslack/channels_test.go
  function TestChannelsNewChannels (line 15) | func TestChannelsNewChannels(t *testing.T) {
  type fakeErrorChannelsPaginationComplete (line 21) | type fakeErrorChannelsPaginationComplete struct
    method Error (line 28) | func (f fakeErrorChannelsPaginationComplete) Error() string {
  type fakeChannelsResponse (line 23) | type fakeChannelsResponse struct
  type fakeSlackHTTPClientChannels (line 32) | type fakeSlackHTTPClientChannels struct
    method Do (line 34) | func (c fakeSlackHTTPClientChannels) Do(req *http.Request) (*http.Resp...
  function TestChannelsFetch (line 52) | func TestChannelsFetch(t *testing.T) {
  function TestChannelsById (line 60) | func TestChannelsById(t *testing.T) {
  function TestChannelsByName (line 71) | func TestChannelsByName(t *testing.T) {

FILE: pkg/ircslack/event_handler.go
  function joinText (line 11) | func joinText(first string, second string, separator string) string {
  function formatThreadChannelName (line 21) | func formatThreadChannelName(threadTimestamp string, channel *Channel) s...
  function resolveChannelName (line 25) | func resolveChannelName(ctx *IrcContext, msgChannel, threadTimestamp str...
  function appendIfNotMoreThan (line 138) | func appendIfNotMoreThan(slice []slack.Msg, msg slack.Msg) []slack.Msg {
  function getConversationDetails (line 145) | func getConversationDetails(
  function replacePermalinkWithText (line 165) | func replacePermalinkWithText(ctx *IrcContext, text string) string {
  function printMessage (line 180) | func printMessage(ctx *IrcContext, message slack.Msg, prefix string) {
  function eventHandler (line 257) | func eventHandler(ctx *IrcContext, rtm *slack.RTM) {

FILE: pkg/ircslack/file_handler.go
  constant maxHTTPAttempts (line 19) | maxHTTPAttempts = 3
  constant retryInterval (line 20) | retryInterval   = time.Second
  type FileHandler (line 24) | type FileHandler struct
    method Download (line 55) | func (handler *FileHandler) Download(file slack.File) string {
  function retryableNetError (line 30) | func retryableNetError(err error) bool {
  function retryableHTTPError (line 43) | func retryableHTTPError(resp *http.Response) bool {

FILE: pkg/ircslack/irc_context.go
  type SlackPostMessage (line 13) | type SlackPostMessage struct
  type IrcContext (line 20) | type IrcContext struct
    method Nick (line 43) | func (ic *IrcContext) Nick() string {
    method UserName (line 52) | func (ic *IrcContext) UserName() string {
    method GetThreadOpener (line 60) | func (ic *IrcContext) GetThreadOpener(channel string, threadTimestamp ...
    method ExpandUserIds (line 72) | func (ic *IrcContext) ExpandUserIds(text string) string {
    method Start (line 84) | func (ic *IrcContext) Start() {
    method PostTextMessage (line 112) | func (ic *IrcContext) PostTextMessage(target, text, targetTs string) {
    method GetUserInfo (line 122) | func (ic *IrcContext) GetUserInfo(userID string) *slack.User {
    method GetUserInfoByName (line 132) | func (ic *IrcContext) GetUserInfoByName(username string) *slack.User {
    method UserID (line 141) | func (ic IrcContext) UserID() string {
    method Mask (line 149) | func (ic IrcContext) Mask() string {
    method GetConversationInfo (line 154) | func (ic IrcContext) GetConversationInfo(conversation string) (*slack....
    method SendUnknownError (line 174) | func (ic *IrcContext) SendUnknownError(fmtstr string, args ...interfac...

FILE: pkg/ircslack/irc_server.go
  constant ProjectAuthor (line 20) | ProjectAuthor       = "Andrea Barberio"
  constant ProjectAuthorEmail (line 21) | ProjectAuthorEmail  = "insomniac@slackware.it"
  constant ProjectURL (line 22) | ProjectURL          = "https://github.com/insomniacslk/irc-slack"
  constant MaxSlackAPIAttempts (line 23) | MaxSlackAPIAttempts = 3
  type IrcCommandHandler (line 27) | type IrcCommandHandler
  function SplitReply (line 71) | func SplitReply(preamble, msg string, chunksize int) []string {
  function ExpandText (line 104) | func ExpandText(text string) string {
  function SendIrcNumeric (line 139) | func SendIrcNumeric(ctx *IrcContext, code int, args, desc string) error {
  function IrcSendChanInfoAfterJoin (line 155) | func IrcSendChanInfoAfterJoin(ctx *IrcContext, ch *Channel, members []sl...
  function IrcSendChanInfoAfterJoinCustom (line 162) | func IrcSendChanInfoAfterJoinCustom(ctx *IrcContext, chanName, chanID, t...
  function joinChannel (line 190) | func joinChannel(ctx *IrcContext, ch *Channel) error {
  function joinChannels (line 206) | func joinChannels(ctx *IrcContext) error {
  function IrcAfterLoggingIn (line 222) | func IrcAfterLoggingIn(ctx *IrcContext, rtm *slack.RTM) error {
  function IrcCapHandler (line 270) | func IrcCapHandler(ctx *IrcContext, prefix, cmd string, args []string, t...
  function parseMentions (line 285) | func parseMentions(text string) string {
  function getTargetTs (line 301) | func getTargetTs(channelName string) string {
  function IrcPrivMsgHandler (line 310) | func IrcPrivMsgHandler(ctx *IrcContext, prefix, cmd string, args []strin...
  type loggerWrapper (line 374) | type loggerWrapper struct
    method Output (line 378) | func (l *loggerWrapper) Output(calldepth int, s string) error {
  type httpClient (line 385) | type httpClient struct
    method Do (line 390) | func (hc httpClient) Do(req *http.Request) (*http.Response, error) {
  function passwordToTokenAndCookie (line 405) | func passwordToTokenAndCookie(p string) (string, string, error) {
  function connectToSlack (line 428) | func connectToSlack(ctx *IrcContext) error {
  function IrcNickHandler (line 482) | func IrcNickHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcUserHandler (line 516) | func IrcUserHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcPingHandler (line 532) | func IrcPingHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcQuitHandler (line 543) | func IrcQuitHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcModeHandler (line 548) | func IrcModeHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcPassHandler (line 571) | func IrcPassHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcWhoHandler (line 594) | func IrcWhoHandler(ctx *IrcContext, prefix, cmd string, args []string, t...
  function IrcWhoisHandler (line 657) | func IrcWhoisHandler(ctx *IrcContext, prefix, cmd string, args []string,...
  function IrcJoinHandler (line 723) | func IrcJoinHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcPartHandler (line 753) | func IrcPartHandler(ctx *IrcContext, prefix, cmd string, args []string, ...
  function IrcTopicHandler (line 801) | func IrcTopicHandler(ctx *IrcContext, prefix, cmd string, args []string,...
  function IrcNamesHandler (line 828) | func IrcNamesHandler(ctx *IrcContext, prefix, cmd string, args []string,...

FILE: pkg/ircslack/server.go
  type Server (line 15) | type Server struct
    method Start (line 29) | func (s Server) Start() error {
    method HandleRequest (line 51) | func (s Server) HandleRequest(conn net.Conn) {
    method HandleMsg (line 71) | func (s *Server) HandleMsg(conn net.Conn, msg string) {

FILE: pkg/ircslack/users.go
  type Users (line 13) | type Users struct
    method FetchByIDs (line 29) | func (u *Users) FetchByIDs(client *slack.Client, skipCache bool, userI...
    method Fetch (line 95) | func (u *Users) Fetch(client *slack.Client) ([]slack.User, error) {
    method Count (line 139) | func (u *Users) Count() int {
    method ByID (line 144) | func (u *Users) ByID(id string) *slack.User {
    method ByName (line 156) | func (u *Users) ByName(name string) *slack.User {
    method IDsToNames (line 171) | func (u *Users) IDsToNames(userIDs ...string) []string {
  function NewUsers (line 20) | func NewUsers(pagination int) *Users {

FILE: pkg/ircslack/users_test.go
  function TestUsersNewUsers (line 15) | func TestUsersNewUsers(t *testing.T) {
  type fakeErrorUsersPaginationComplete (line 27) | type fakeErrorUsersPaginationComplete struct
    method Error (line 34) | func (f fakeErrorUsersPaginationComplete) Error() string {
  type fakeUsersResponse (line 29) | type fakeUsersResponse struct
  type fakeSlackHTTPClient (line 38) | type fakeSlackHTTPClient struct
    method Do (line 40) | func (c fakeSlackHTTPClient) Do(req *http.Request) (*http.Response, er...
  function TestUsersFetch (line 58) | func TestUsersFetch(t *testing.T) {
  function TestUsersById (line 67) | func TestUsersById(t *testing.T) {
  function TestUsersByName (line 78) | func TestUsersByName(t *testing.T) {
  function TestUsersIDsToNames (line 89) | func TestUsersIDsToNames(t *testing.T) {

FILE: pkg/ircslack/wordwrap.go
  function WordWrap (line 9) | func WordWrap(allWords []string, maxLen int) []string {

FILE: pkg/ircslack/wordwrap_test.go
  function TestWordWrapMultiLine (line 12) | func TestWordWrapMultiLine(t *testing.T) {
  function TestWordWrapSingleLine (line 23) | func TestWordWrapSingleLine(t *testing.T) {
  function TestWordWrapTruncate (line 30) | func TestWordWrapTruncate(t *testing.T) {

FILE: tools/autotoken/main.go
  function main (line 27) | func main() {
  function fetchCredentials (line 51) | func fetchCredentials(ctx context.Context, team string, timeout time.Dur...
  function extractTokenAndCookie (line 88) | func extractTokenAndCookie(ctx context.Context, team string) (string, st...

FILE: tools/slackapp/main.go
  function httpStatus (line 26) | func httpStatus(w http.ResponseWriter, r *http.Request, statusCode int, ...
  type slackChallenge (line 35) | type slackChallenge struct
  function handleSlackChallenge (line 39) | func handleSlackChallenge(w http.ResponseWriter, r *http.Request) {
  function handleSlackAuth (line 57) | func handleSlackAuth(w http.ResponseWriter, r *http.Request) {
  function main (line 123) | func main() {
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (130K chars).
[
  {
    "path": ".dockerignore",
    "chars": 46,
    "preview": "Dockerfile\n.travis\n.dockerignore\ncoverage.txt\n"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 487,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/docker.yml",
    "chars": 701,
    "preview": "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      D"
  },
  {
    "path": ".github/workflows/tests.yml",
    "chars": 1576,
    "preview": "name: Tests\n\non: [push, pull_request]\n\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        go"
  },
  {
    "path": ".gitignore",
    "chars": 37,
    "preview": "coverage.txt\ncmd/irc-slack/irc-slack\n"
  },
  {
    "path": ".stickler.yml",
    "chars": 58,
    "preview": "linters:\n  golint:\n    fixer: true\nfixers:\n  enable: true\n"
  },
  {
    "path": "Dockerfile",
    "chars": 992,
    "preview": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.23-alpine AS bu"
  },
  {
    "path": "Dockerfile.autotoken",
    "chars": 758,
    "preview": "############################\n# STEP 1 build executable binary\n############################\nFROM golang:1.16-alpine AS bu"
  },
  {
    "path": "LICENSE",
    "chars": 1515,
    "preview": "BSD 3-Clause License\n\nCopyright (c) 2018, Andrea Barberio\nAll rights reserved.\n\nRedistribution and use in source and bin"
  },
  {
    "path": "README.md",
    "chars": 9815,
    "preview": "# 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 "
  },
  {
    "path": "cmd/irc-slack/Makefile",
    "chars": 238,
    "preview": "CMD=irc-slack\n\nREVISION := $(shell git rev-parse --short HEAD)\nBRANCH := $(shell git rev-parse --abbrev-ref HEAD)\n\nall: "
  },
  {
    "path": "cmd/irc-slack/main.go",
    "chars": 4325,
    "preview": "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\""
  },
  {
    "path": "docker-compose.yml",
    "chars": 109,
    "preview": "services:\n  irc-slack:\n    build:\n      context: .\n      dockerfile: Dockerfile\n    ports:\n      - 6666:6666\n"
  },
  {
    "path": "go.mod",
    "chars": 1580,
    "preview": "module github.com/insomniacslk/irc-slack\n\ngo 1.25\n\nrequire (\n\tgithub.com/chromedp/cdproto v0.0.0-20250803210736-d308e07a"
  },
  {
    "path": "go.sum",
    "chars": 14574,
    "preview": "github.com/chappjc/logrus-prefix v0.0.0-20180227015900-3a1d64819adb h1:aZTKxMminKeQWHtzJBbV8TttfTxzdJ+7iEJFE6FmUzg=\ngith"
  },
  {
    "path": "pkg/ircslack/channel.go",
    "chars": 4292,
    "preview": "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, "
  },
  {
    "path": "pkg/ircslack/channels.go",
    "chars": 5237,
    "preview": "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 chan"
  },
  {
    "path": "pkg/ircslack/channels_test.go",
    "chars": 2146,
    "preview": "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.co"
  },
  {
    "path": "pkg/ircslack/event_handler.go",
    "chars": 13474,
    "preview": "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"
  },
  {
    "path": "pkg/ircslack/file_handler.go",
    "chars": 2408,
    "preview": "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"
  },
  {
    "path": "pkg/ircslack/irc_context.go",
    "chars": 5166,
    "preview": "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 represe"
  },
  {
    "path": "pkg/ircslack/irc_server.go",
    "chars": 29652,
    "preview": "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/co"
  },
  {
    "path": "pkg/ircslack/logger.go",
    "chars": 103,
    "preview": "package ircslack\n\nimport \"github.com/coredhcp/coredhcp/logger\"\n\nvar log = logger.GetLogger(\"ircslack\")\n"
  },
  {
    "path": "pkg/ircslack/server.go",
    "chars": 3251,
    "preview": "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// Ser"
  },
  {
    "path": "pkg/ircslack/users.go",
    "chars": 5082,
    "preview": "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 li"
  },
  {
    "path": "pkg/ircslack/users_test.go",
    "chars": 2497,
    "preview": "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.co"
  },
  {
    "path": "pkg/ircslack/wordwrap.go",
    "chars": 983,
    "preview": "package ircslack\n\nimport (\n\t\"strings\"\n)\n\n// WordWrap wraps the given words up to the maximum specified length.\n// If a s"
  },
  {
    "path": "pkg/ircslack/wordwrap_test.go",
    "chars": 1154,
    "preview": "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 f"
  },
  {
    "path": "tools/autotoken/main.go",
    "chars": 3544,
    "preview": "// autotoken retrieves a Slack token and cookie using your Slack team\n// credentials.\npackage main\n\nimport (\n\t\"context\"\n"
  },
  {
    "path": "tools/slackapp/main.go",
    "chars": 3826,
    "preview": "package main\n\n/* Slack Oauth app built according to\n * https://api.slack.com/authentication/oauth-v2\n */\n\nimport (\n\t\"enc"
  }
]

About this extraction

This page contains the full source code of the insomniacslk/irc-slack GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (116.8 KB), approximately 38.2k tokens, and a symbol index with 130 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!