diff --git a/pkg/controller/chat/chat.go b/pkg/controller/chat/chat.go index e97ed93..c4ce8c3 100644 --- a/pkg/controller/chat/chat.go +++ b/pkg/controller/chat/chat.go @@ -10,6 +10,7 @@ import ( "time" "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/geoip" "code.nonshy.com/nonshy/website/pkg/log" "code.nonshy.com/nonshy/website/pkg/models" "code.nonshy.com/nonshy/website/pkg/photo" @@ -22,16 +23,32 @@ import ( // JWT claims. type Claims struct { // Custom claims. - IsAdmin bool `json:"op"` - Avatar string `json:"img"` - ProfileURL string `json:"url"` - Nickname string `json:"nick"` + IsAdmin bool `json:"op,omitempty"` + Avatar string `json:"img,omitempty"` + ProfileURL string `json:"url,omitempty"` + Nickname string `json:"nick,omitempty"` + Emoji string `json:"emoji,omitempty"` + Gender string `json:"gender,omitempty"` // Standard claims. Notes: // subject = username jwt.RegisteredClaims } +// Gender returns the BareRTC gender string for the user's gender selection. +func Gender(u *models.User) string { + switch u.GetProfileField("gender") { + case "Man", "Trans (FTM)": + return "m" + case "Woman", "Trans (MTF)": + return "f" + case "Non-binary", "Trans", "Other": + return "o" + default: + return "" + } +} + // Landing page for chat rooms. func Landing() http.HandlerFunc { tmpl := templates.Must("chat.html") @@ -80,12 +97,23 @@ func Landing() http.HandlerFunc { avatar = "/static/img/shy-friends.png" } + // Country flag emoji. + emoji, err := geoip.GetRequestCountryFlag(r) + if err != nil { + emoji, err = geoip.CountryFlagEmojiWithCode("US") + if err != nil { + emoji = "πŸ΄β€β˜ οΈ" + } + } + // Create the JWT claims. claims := Claims{ IsAdmin: currentUser.HasAdminScope(config.ScopeChatModerator), Avatar: avatar, ProfileURL: "/u/" + currentUser.Username, Nickname: currentUser.NameOrUsername(), + Emoji: emoji, + Gender: Gender(currentUser), RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)), IssuedAt: jwt.NewNumericDate(time.Now()), diff --git a/pkg/geoip/geoip.go b/pkg/geoip/geoip.go new file mode 100644 index 0000000..b562a70 --- /dev/null +++ b/pkg/geoip/geoip.go @@ -0,0 +1,75 @@ +// Package geoip provides IP address geolocation features. +package geoip + +import ( + "errors" + "net" + "net/http" + "strings" + + "code.nonshy.com/nonshy/website/pkg/config" + "code.nonshy.com/nonshy/website/pkg/utility" + "github.com/oschwald/geoip2-golang" +) + +// GetRequestCity returns the GeoIP City result for the current HTTP request. +func GetRequestCity(r *http.Request) (*geoip2.City, error) { + var ( + addr = utility.IPAddress(r) + ip = net.ParseIP(addr) + ) + return GetCity(ip) +} + +// GetRequestCountryFlag returns the country flag based on the current HTTP request IP address. +func GetRequestCountryFlag(r *http.Request) (string, error) { + city, err := GetRequestCity(r) + if err != nil { + // If the remote addr is localhost (local dev testing), default to US flag. + if addr := utility.IPAddress(r); addr == "127.0.0.1" || addr == "::1" { + return CountryFlagEmoji("US") + } + + return "", err + } + + return CountryFlagEmoji(city.Country.IsoCode) +} + +// GetCity queries the GeoIP database for city information for an IP address. +func GetCity(ip net.IP) (*geoip2.City, error) { + db, err := geoip2.Open(config.GeoIPPath) + if err != nil { + return nil, err + } + + return db.City(ip) +} + +// CountryFlagEmoji returns the emoji sequence for a country flag based on +// the two-letter country code. +func CountryFlagEmoji(alpha2 string) (string, error) { + if len(alpha2) != 2 { + return "", errors.New("country code must be two letters long") + } + + alpha2 = strings.ToLower(alpha2) + + var ( + flagBaseIndex = '\U0001F1E6' - 'a' + box = func(ch byte) string { + return string(rune(ch) + flagBaseIndex) + } + ) + + return string(box(alpha2[0]) + box(alpha2[1])), nil +} + +// CountryFlagEmojiWithCode returns a string consisting of the country flag, a space, and the alpha2 code passed in. +func CountryFlagEmojiWithCode(alpha2 string) (string, error) { + if emoji, err := CountryFlagEmoji(alpha2); err != nil { + return emoji, err + } else { + return emoji + " " + alpha2, nil + } +} diff --git a/pkg/geoip/geoip_test.go b/pkg/geoip/geoip_test.go new file mode 100644 index 0000000..bd3aaee --- /dev/null +++ b/pkg/geoip/geoip_test.go @@ -0,0 +1,34 @@ +package geoip_test + +import ( + "testing" + + "code.nonshy.com/nonshy/website/pkg/geoip" +) + +func TestCountryFlags(t *testing.T) { + table := []struct { + input string + expect string + err bool + }{ + {"US", "πŸ‡ΊπŸ‡Έ", false}, + {"CA", "πŸ‡¨πŸ‡¦", false}, + {"AU", "πŸ‡¦πŸ‡Ί", false}, + {"NZ", "πŸ‡³πŸ‡Ώ", false}, + {"CN", "πŸ‡¨πŸ‡³", false}, + {"invalid", "", true}, + } + + for _, test := range table { + emoji, err := geoip.CountryFlagEmoji(test.input) + if err != nil && !test.err { + t.Errorf("Country %s: got an error but did not expect to: %s", test.input, err) + continue + } + + if emoji != test.expect { + t.Errorf("Country %s: did not get expected emoji %s, got %+v", test.input, test.expect, emoji) + } + } +}