johnste / finicky

A macOS app for customizing which browser to start
MIT License
3.67k stars 136 forks source link

Open Slack links in the client #96

Open ephimoff opened 4 years ago

ephimoff commented 4 years ago

Hi,

I was trying to use a handler to open the Slack links in the Slack.app client on mac but it didn't work. The app opens up but not on the right message. Any thoughts on how to achieve that?

Here is the code I used:

{
      // Open these urls in Slack app
      match: finicky.matchHostnames([
        "slack.com",
      ]),
      browser: "com.tinyspeck.slackmacgap"
},
ovelindstrom commented 4 years ago

I think we should need to rewrite the https://slack.com/app_redirect to the deeplink slack://open format. I will give it a try during easter.

Ref: https://api.slack.com/reference/deep-linking

ovelindstrom commented 4 years ago

This at least rewrites the url to the correct one.

rewrite: [
    {
      // Redirect all https://slack.com/app_redirect?team=team=apegroup&channel=random 
      // to slack://channel?team=apegroup&id=random
      match: ({ url }) => url.host.includes("slack.com") && url.pathname.includes("app_redirect"),
      url({ url }) {
        const team = url.search.split('&').filter(part => part.startsWith('team'));
        var channel = "" + url.search.split('&').filter(part => part.startsWith('channel'));
        var id = channel.replace("channel", "id");

        return {
            protocol: "slack",
            username: "",
            password: "",
            host: "channel",
            port: null,
            pathname: "",
            search : team + '&' + id,
            hash: ""

        }

      }
    } 
  ]
ephimoff commented 4 years ago

Awesome! Do I just put it into my .finicky.js?

ovelindstrom commented 4 years ago

Yes. Still haven't figured out how to get it to actually open Slack in a correct way.

beefcheeks commented 3 years ago

Spent some time starting with what @ovelindstrom posted and modified it for my use case. Managed to get most links to work that I use (not sure about others). There didn't appear to be any documentation on how to translate deep linked messages here, but eventually figured out through trial and error how to format the message identifier. Below is the config that I use, but be sure to populate the org map with the proper subdomains and team identifiers:

  handlers: [
    {
      // Redirect all web links
      // from: https://app.slack.com/client/<team id>/<channel>
      // to: slack://channel?team=<team-id>&id=<channel-id>
      //
      // Redirect all deep linked messages
      // from: https://<subdomain>.slack.com/archives/<channel-id>/p<16-digit-timestamp>
      // to: slack://channel?team=<team-id>&id=<channel-id>&message=<10-digit-6-decimal-timestamp>      
      browser: "/Applications/Slack.app",
      match: [
        '*.slack.com/client/*',
        '*.slack.com/archives/*'
      ],
      url({ url }) {
        const parts = url.pathname.split('/')
        // Return input URL if no expected path is found
        if (parts.length < 2) return url
        let team
        switch (parts[1]) {
          // For direct web links
          case 'client':
            team = parts[2]
            parts.splice(2, 1) // Remove team identifier to match archives format
            break
          // For deep links
          case 'archives':
            const org = url.host.split('.')[0]
            switch (org) {
              case '<org subdomain>':
                // Starts with a T and can be found in the web app URL for any channel in your org
                team = '<team id>'
                break
              default:
                // Return input URL if no team lookup available
                return url
              }
        }
        search = `team=${team}`
        let channel = parts[2]
        if (parts.length === 3) {
        // If this is a link to a channel/user
          search = `${search}&id=${channel}`
        // If this is a link to a message
        } else if (parts.length === 4) {
          const message = parts[3].slice(1, 11) + '.' + parts[3].slice(11)
          search = `${search}&channel=${channel}&message=${message}`
        }
        return {
            protocol: "slack",
            username: "",
            password: "",
            host: "channel",
            port: null,
            pathname: "",
            search: search,
            hash: ""
        }
      }
    }
  ]

Also, may be worth linking this response to another related issue: #158

opalelement commented 3 years ago

I wrote a more robust version of this which supports team hosts, enterprise hosts, and the generic app.slack.com host. It's also just regex matches so if more URL formats are discovered it should be easy to add them to the list. It works in two parts, first doing a rewrite to the slack:// URL then using a handler for the slack protocol to send to the Slack app. This lets us fall back to using the browser if the conversion wasn't successful. I've tested it with all of the following URL formats I could find for our Slack Enterprise and they work as expected:

Note that I did not add support for app_redirect URLs as they can sometimes work but not universally. Any of the following are valid and can be forwarded via the browser:

But deeplinks only support IDs rather than names, so only the first could actually be converted. It's possible a regex could be made to match only IDs, but I couldn't find much information about the endpoint or the ID formats it supports so I opted to ignore it.

Also JavaScript isn't my language of choice, so there may be bugs and the code probably isn't optimal.

module.exports = {
  handlers: [
    {
      match: ({ url }) => url.protocol === "slack",
      browser: "/Applications/Slack.app"
    }
  ],
  rewrite: [
    {
      match: [
        '*.slack.com/*',
      ],
      url: function({ url, urlString }) {
        const subdomain = url.host.slice(0, -10)
        const pathParts = url.pathname.split("/")

        let team, patterns = {}
        if (subdomain != 'app') {
          switch (subdomain) {
            case '<teamname>':
            case '<corpname>.enterprise':
              team = 'T00000000'
              break
            default:
              finicky.notify(
                `No Slack team ID found for ${url.host}`, 
                `Add the team ID to ~/.finicky.js to allow direct linking to Slack.`
              )
              return url
          }

          if (subdomain.slice(-11) == '.enterprise') {
            patterns = {
              'file': [/\/files\/\w+\/(?<id>\w+)/]
            }
          } else {
            patterns = {
              'file': [/\/messages\/\w+\/files\/(?<id>\w+)/],
              'team': [/(?:\/messages\/\w+)?\/team\/(?<id>\w+)/],
              'channel': [/\/(?:messages|archives)\/(?<id>\w+)(?:\/(?<message>p\d+))?/]
            }
          }
        } else {
          patterns = {
            'file': [
              /\/client\/(?<team>\w+)\/\w+\/files\/(?<id>\w+)/,
              /\/docs\/(?<team>\w+)\/(?<id>\w+)/
            ],
            'team': [/\/client\/(?<team>\w+)\/\w+\/user_profile\/(?<id>\w+)/],
            'channel': [/\/client\/(?<team>\w+)\/(?<id>\w+)(?:\/(?<message>[\d.]+))?/]
          }
        }

        for (let [host, host_patterns] of Object.entries(patterns)) {
          for (let pattern of host_patterns) {
            let match = pattern.exec(url.pathname)
            if (match) {
              let search = `team=${team || match.groups.team}`

              if (match.groups.id) {
                search += `&id=${match.groups.id}`
              }

              if (match.groups.message) {
                let message = match.groups.message
                if (message.charAt(0) == 'p') {
                  message = message.slice(1, 11) + '.' + message.slice(11)
                }
                search += `&message=${message}`
              }

              let output = {
                protocol: "slack",
                username: "",
                password: "",
                host: host,
                port: null,
                pathname: "",
                search: search,
                hash: ""
              }
              let outputStr = `${output.protocol}://${output.host}?${output.search}`
              finicky.log(`Rewrote Slack URL ${urlString} to deep link ${outputStr}`)
              return output
            }
          }
        }

        return url
      }
    }
  ]
}