Skip to content

Commit

Permalink
Merge pull request #44 from rneatherway/dol
Browse files Browse the repository at this point in the history
Add support for sending as well as reading messages
  • Loading branch information
rneatherway authored May 23, 2023
2 parents e477c34 + 7cd1a70 commit 122546d
Show file tree
Hide file tree
Showing 10 changed files with 902 additions and 207 deletions.
59 changes: 45 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,51 @@ This project provides a means of archiving a Slack conversation or thread as mar
## Usage

Usage:
gh slack [OPTIONS] [Start]

Application Options:
-l, --limit= Number of _channel_ messages to be fetched after the starting message (all thread messages are fetched) (default: 20)
-v, --verbose Show verbose debug information
--version Output version information
-d, --details Wrap the markdown output in HTML <details> tags
-i, --issue= The URL of a repository to post the output as a new issue, or the URL of an issue to add a comment to that issue

Help Options:
-h, --help Show this help message

Arguments:
Start: Required. Permalink for the first message to fetch. Following messages are then fetched from that channel (or thread if applicable)
gh-slack [command]

If no command is specified, the default is "read". The default command also requires a permalink argument <START> for the first message to fetch.
Use "gh-slack read --help" for more information about the default command behaviour.

Examples:
gh-slack -i <issue-url> <slack-permalink> # defaults to read command
gh-slack read <slack-permalink>
gh-slack read -i <issue-url> <slack-permalink>
gh-slack send -m <message> -c <channel-id> -t <team-name>

# Example configuration (add to gh's configuration file at $HOME/.config/gh/config.yml):
extensions:
slack:
team: github
channel: ops
bot: hubot # Can be a user id (most reliable), bot profile name or username

Available Commands:
completion Generate the autocompletion script for the specified shell
help Help about any command
read Reads a Slack channel and outputs the messages as markdown
send Sends a message to a Slack channel

Flags:
-h, --help help for gh-slack
-v, --verbose Show verbose debug information

Use "gh-slack [command] --help" for more information about a command.

## Configuration

The `send` subcommand supports storing default values for the `team`, `bot` and
`channel` required parameters in gh's own configuration file using a block like:

```yaml
extensions:
slack:
team: foo
channel: ops
bot: robot # Can be a user id (most reliable), bot profile name or username
```
This is particularly useful if you want to use the `send` subcommand to interact
with a bot serving chatops in a standard operations channel.

## Limitations

Expand Down
202 changes: 202 additions & 0 deletions cmd/gh-slack/cmd/read.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package cmd

import (
"errors"
"fmt"
"io"
"log"
"net/url"
"os"
"regexp"

"github.com/rneatherway/gh-slack/internal/gh"
"github.com/rneatherway/gh-slack/internal/markdown"
"github.com/rneatherway/gh-slack/internal/slackclient"
"github.com/rneatherway/gh-slack/internal/version"
"github.com/spf13/cobra"
)

var readCmd = &cobra.Command{
Use: "read [flags] <START>",
Short: "Reads a Slack channel and outputs the messages as markdown",
Long: `Reads a Slack channel and outputs the messages as markdown for GitHub issues.`,
RunE: func(cmd *cobra.Command, args []string) error {
return readSlack(args)
},
Example: ` gh-slack read <slack-permalink>
gh-slack read -i <issue-url> <slack-permalink>`,
}

var (
permalinkRE = regexp.MustCompile("https://([^./]+).slack.com/archives/([A-Z0-9]+)/p([0-9]+)([0-9]{6})")
nwoRE = regexp.MustCompile("^/[^/]+/[^/]+/?$")
issueRE = regexp.MustCompile("^/[^/]+/[^/]+/(issues|pull)/[0-9]+/?$")
)

type linkParts struct {
team string
channelID string
timestamp string
}

// https://github.slack.com/archives/CP9GMKJCE/p1648028606962719
// returns (github, CP9GMKJCE, 1648028606.962719, nil)
func parsePermalink(link string) (linkParts, error) {
result := permalinkRE.FindStringSubmatch(link)
if result == nil {
return linkParts{}, fmt.Errorf("not a permalink: %q", link)
}

return linkParts{
team: result[1],
channelID: result[2],
timestamp: result[3] + "." + result[4],
}, nil
}

var opts struct {
Args struct {
Start string
}
Limit int
Version bool
Details bool
Issue string
}

func init() {
readCmd.Flags().IntVarP(&opts.Limit, "limit", "l", 20, "Number of _channel_ messages to be fetched after the starting message (all thread messages are fetched)")
readCmd.Flags().BoolVar(&opts.Version, "version", false, "Output version information")
readCmd.Flags().BoolVarP(&opts.Details, "details", "d", false, "Wrap the markdown output in HTML <details> tags")
readCmd.Flags().StringVarP(&opts.Issue, "issue", "i", "", "The URL of a repository to post the output as a new issue, or the URL of an issue (or pull request) to add a comment to")
readCmd.SetHelpTemplate(readCmdUsage)
readCmd.SetUsageTemplate(readCmdUsage)
}

func readSlack(args []string) error {
if opts.Version {
fmt.Printf("gh-slack %s (%s)\n", version.Version(), version.Commit())
return nil
}

if len(args) == 0 {
return errors.New("the required argument <START> was not provided")
}
opts.Args.Start = args[0]
if opts.Args.Start == "" {
return errors.New("the required argument <START> was not provided")
}

var repoUrl, issueOrPrUrl, subCmd string
if opts.Issue != "" {
u, err := url.Parse(opts.Issue)
if err != nil {
return err
}

matches := issueRE.FindStringSubmatch(u.Path)
if matches != nil {
issueOrPrUrl = opts.Issue
subCmd = "issue"
if matches[1] == "pull" {
subCmd = "pr"
}
} else if nwoRE.MatchString(u.Path) {
repoUrl = opts.Issue
} else {
return fmt.Errorf("not a repository or issue URL: %q", opts.Issue)
}
}

linkParts, err := parsePermalink(opts.Args.Start)
if err != nil {
return err
}

logger := log.New(io.Discard, "", log.LstdFlags)
if verbose {
logger = log.Default()
}

client, err := slackclient.New(linkParts.team, logger)
if err != nil {
return err
}

history, err := client.History(linkParts.channelID, linkParts.timestamp, opts.Limit)
if err != nil {
return err
}

output, err := markdown.FromMessages(client, history)
if err != nil {
return err
}

var channelName string
if opts.Details {
channelInfo, err := client.ChannelInfo(linkParts.channelID)
if err != nil {
return err
}

channelName = channelInfo.Name
output = markdown.WrapInDetails(channelName, opts.Args.Start, output)
}

if repoUrl != "" {
if channelName == "" {
channelInfo, err := client.ChannelInfo(linkParts.channelID)
if err != nil {
return err
}
channelName = channelInfo.Name
}

err := gh.NewIssue(repoUrl, channelName, output)
if err != nil {
return err
}
} else if issueOrPrUrl != "" {
err := gh.AddComment(subCmd, issueOrPrUrl, output)
if err != nil {
return err
}
} else {
os.Stdout.WriteString(output)
}

return nil
}

const readCmdUsage string = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command] <START>{{end}}
where <START> is a required argument which should be permalink for the first message to fetch. Following messages are then fetched from that channel (or thread if applicable).{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package main
package cmd

import "testing"

Expand Down
80 changes: 80 additions & 0 deletions cmd/gh-slack/cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package cmd

import (
"os"

"github.com/spf13/cobra"
)

const sendConfigExample = `
# Example configuration (add to gh's configuration file at $HOME/.config/gh/config.yml):
extensions:
slack:
team: foo
channel: ops
bot: robot # Can be a user id (most reliable), bot profile name or username`

var rootCmd = &cobra.Command{
SilenceUsage: true,
SilenceErrors: true,
Use: "gh-slack [command]",
Short: "Command line tool for interacting with Slack through gh cli",
Long: `A command line tool for interacting with Slack through the gh cli.`,
Example: ` gh-slack -i <issue-url> <slack-permalink> # defaults to read command
gh-slack read <slack-permalink>
gh-slack read -i <issue-url> <slack-permalink>
gh-slack send -m <message> -c <channel-id> -t <team-name>
` + sendConfigExample,
}

func Execute() error {
cmd, _, err := rootCmd.Find(os.Args[1:])
if err != nil || cmd == nil {
args := append([]string{"read"}, os.Args[1:]...)
rootCmd.SetArgs(args)
}
return rootCmd.Execute()
}

var verbose bool = false

func init() {
rootCmd.AddCommand(readCmd)
rootCmd.AddCommand(sendCmd)
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Show verbose debug information")
rootCmd.SetHelpTemplate(rootCmdUsageTemplate)
rootCmd.SetUsageTemplate(rootCmdUsageTemplate)
}

const rootCmdUsageTemplate string = `Usage:{{if .Runnable}}
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
{{.CommandPath}} [command]{{end}}
If no command is specified, the default is "read". The default command also requires a permalink argument <START> for the first message to fetch.
Use "gh-slack read --help" for more information about the default command behaviour.{{if gt (len .Aliases) 0}}
Aliases:
{{.NameAndAliases}}{{end}}{{if .HasExample}}
Examples:
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
Global Flags:
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
`
Loading

0 comments on commit 122546d

Please sign in to comment.