Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions web/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package web

const (
ChatIOSRedirectURL = "https://apps.apple.com/fr/app/twake-chat/id6473384641"
ChatAndroidRedirectURL = "https://play.google.com/store/apps/details?id=app.twake.android.chat"
ChatFallbackURL = "https://twake.app"
ChatInvitePrefix = "/chat/@"
)
39 changes: 39 additions & 0 deletions web/matrix.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package web

import (
"strings"
)

func ExtractMatrixID(path string) string {
if !strings.HasPrefix(path, ChatInvitePrefix) {
return ""
}
return strings.TrimPrefix(path, "/chat/")
}

func ExtractDomainFromMatrixID(matrixID string) string {
if matrixID == "" || !strings.Contains(matrixID, ":") {
return ""
}
parts := strings.Split(matrixID, ":")
if len(parts) != 2 {
return ""
}
domain := parts[1]
return domain
}

func GetSignUpURLForDomain(domain string) string {
switch domain {
case "twake.app":
return "sign-up.twake.app"
case "stg.lin-saas.com":
return "sign-up.stg.lin-saas.com"
case "cozy.lin-saas.com":
return "sign-up.cozy.lin-saas.com"
case "qa.lin-saas.com":
return "sign-up.qa.lin-saas.com"
default:
return ""
}
}
85 changes: 85 additions & 0 deletions web/matrix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package web

import (
"testing"
)

func TestExtractMatrixID(t *testing.T) {
tests := []struct {
name string
path string
want string
wantErr bool
}{
{
name: "Valid chat invite path",
path: "/chat/@jdoe:twake.app",
want: "@jdoe:twake.app",
wantErr: false,
},
{
name: "Invalid path - missing prefix",
path: "/@jdoe:twake.app",
want: "",
wantErr: true,
},
{
name: "Empty path",
path: "",
want: "",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ExtractMatrixID(tt.path)
if tt.wantErr && got != "" {
t.Errorf("ExtractMatrixID() expected error but got result: %v", got)
}
if !tt.wantErr && got != tt.want {
t.Errorf("ExtractMatrixID() = %v, want %v", got, tt.want)
}
})
}
}

func TestExtractDomainFromMatrixID(t *testing.T) {
tests := []struct {
name string
matrixID string
want string
wantErr bool
}{
{
name: "Valid Matrix ID",
matrixID: "@jdoe:twake.app",
want: "twake.app",
wantErr: false,
},
{
name: "Invalid Matrix ID - no colon",
matrixID: "@jdoe",
want: "",
wantErr: true,
},
{
name: "Empty Matrix ID",
matrixID: "",
want: "",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ExtractDomainFromMatrixID(tt.matrixID)
if tt.wantErr && got != "" {
t.Errorf("ExtractDomainFromMatrixID() expected error but got result: %v", got)
}
if !tt.wantErr && got != tt.want {
t.Errorf("ExtractDomainFromMatrixID() = %v, want %v", got, tt.want)
}
})
}
}
66 changes: 66 additions & 0 deletions web/universal_links.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,66 @@ func universalLink(c echo.Context) error {
return c.String(http.StatusOK, content.String())
}

func isChatInvite(c echo.Context) bool {
requestPath := c.Request().URL.Path
return strings.HasPrefix(requestPath, ChatInvitePrefix)
}

func createDirectChatURL(baseURL, requestPath string) string {
parsedURL := url.URL{
Scheme: "https",
Host: baseURL,
Path: "/",
}

query := parsedURL.Query()
query.Set("redirect", "true")
query.Set("slug", "chat")
query.Set("path", "/#/bridge/web/#"+requestPath)

parsedURL.RawQuery = query.Encode()
return parsedURL.String()
}

func redirectChatInvite(c echo.Context) (bool, error) {
userAgent := c.Request().UserAgent()
platform := GetPlatformFromUserAgent(userAgent)

var redirectURL string
switch platform {
case "ios":
redirectURL = ChatIOSRedirectURL
case "android":
redirectURL = ChatAndroidRedirectURL
case "web":
// Extract Matrix ID and domain to determine the sign-up URL
requestPath := c.Request().URL.Path
matrixID := ExtractMatrixID(requestPath)
if matrixID == "" {
redirectURL = ChatFallbackURL
break
}

domain := ExtractDomainFromMatrixID(matrixID)
if domain == "" {
redirectURL = ChatFallbackURL
break
}

signUpURL := GetSignUpURLForDomain(domain)
if signUpURL == "" {
redirectURL = ChatFallbackURL
break
}

redirectURL = createDirectChatURL(signUpURL, requestPath)
default:
redirectURL = ChatFallbackURL
}

return true, c.Redirect(http.StatusSeeOther, redirectURL)
}

func universalLinkRedirect(c echo.Context) error {
space, err := getSpaceFromHost(c)
if err != nil {
Expand All @@ -38,6 +98,12 @@ func universalLinkRedirect(c echo.Context) error {
spacePrefix := space.GetPrefix()
fallback := c.QueryParam("fallback")

// Custom redirection for chat app invite links
if isChatInvite(c) {
_, err := redirectChatInvite(c)
return err
}

// The following code has been made to handle an iOS bug during JSON recovery.
// It should be removed if a fix is found one day.
// See https://openradar.appspot.com/33893852
Expand Down
36 changes: 36 additions & 0 deletions web/universal_links_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package web

import (
"testing"
)

func TestCreateDirectChatURL(t *testing.T) {
tests := []struct {
name string
baseURL string
path string
want string
}{
{
name: "Basic chat invite URL",
baseURL: "sign-up.twake.app",
path: "/chat/@jdoe:twake.app",
want: "https://sign-up.twake.app/?path=%2F%23%2Fbridge%2Fweb%2F%23%2Fchat%2F%40jdoe%3Atwake.app&redirect=true&slug=chat",
},
{
name: "Staging chat invite URL",
baseURL: "sign-up.stg.lin-saas.com",
path: "/chat/@jdoe:stg.lin-saas.com",
want: "https://sign-up.stg.lin-saas.com/?path=%2F%23%2Fbridge%2Fweb%2F%23%2Fchat%2F%40jdoe%3Astg.lin-saas.com&redirect=true&slug=chat",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := createDirectChatURL(tt.baseURL, tt.path)
if got != tt.want {
t.Errorf("createDirectChatURL() = %v, want %v", got, tt.want)
}
})
}
}
19 changes: 19 additions & 0 deletions web/user_agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package web

import "strings"

func GetPlatformFromUserAgent(userAgent string) string {
userAgentLower := strings.ToLower(userAgent)

if strings.Contains(userAgentLower, "iphone") ||
strings.Contains(userAgentLower, "ipad") ||
strings.Contains(userAgentLower, "ipod") {
return "ios"
}

if strings.Contains(userAgentLower, "android") {
return "android"
}

return "web"
}
85 changes: 85 additions & 0 deletions web/user_agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package web

import "testing"

func TestGetPlatformFromUserAgent(t *testing.T) {
tests := []struct {
name string
userAgent string
want string
}{
// iOS
{
name: "iPhone",
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Mobile/15E148 Safari/604.1",
want: "ios",
},
{
name: "iPad",
userAgent: "Mozilla/5.0 (iPad; CPU OS 14_6 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0 Mobile/15E148 Safari/604.1",
want: "ios",
},
{
name: "iPod",
userAgent: "Mozilla/5.0 (iPod touch; CPU iPhone OS 12_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1",
want: "ios",
},
// Android
{
name: "Android phone",
userAgent: "Mozilla/5.0 (Linux; Android 11; Pixel 5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.91 Mobile Safari/537.36",
want: "android",
},
{
name: "Android tablet",
userAgent: "Mozilla/5.0 (Linux; Android 10; SM-T510) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.105 Safari/537.36",
want: "android",
},
// Web
{
name: "Chrome on Windows",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
want: "web",
},
{
name: "Firefox on macOS",
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:89.0) Gecko/20100101 Firefox/89.0",
want: "web",
},
{
name: "Safari on macOS",
userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15",
want: "web",
},
{
name: "Edge on Windows",
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.59",
want: "web",
},
{
name: "Chrome on Linux",
userAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36",
want: "web",
},
// Edge cases
{
name: "Empty user agent",
userAgent: "",
want: "web",
},
{
name: "Unknown user agent",
userAgent: "SomeBot/1.0",
want: "web",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetPlatformFromUserAgent(tt.userAgent)
if got != tt.want {
t.Errorf("GetPlatformFromUserAgent() = %v, want %v", got, tt.want)
}
})
}
}
Loading