From 0ca8bb0f3d461b1c99b838f7c4af5449bb2d1b31 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 22 Jun 2017 19:44:20 +0200 Subject: [PATCH 01/36] Changed .mountable to a normal div --- pages/profile/profile.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index aa87fc5a..1854238c 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -79,7 +79,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .post-author Avatar(post.Author()) .post-content - .mountable!= aero.Markdown(post.Text) + div!= aero.Markdown(post.Text) .post-toolbar.active .spacer .post-likes= len(post.Likes) From cf0d47bfb0d977a6fa9944b76b8a977c1ff663aa Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 22 Jun 2017 20:51:11 +0200 Subject: [PATCH 02/36] Started working on new user registration --- auth/google.go | 21 ++++++++- main.go | 1 + middleware/UserInfo.go | 99 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 middleware/UserInfo.go diff --git a/auth/google.go b/auth/google.go index 8110020a..5e9bc99b 100644 --- a/auth/google.go +++ b/auth/google.go @@ -92,6 +92,13 @@ func InstallGoogleAuth(app *aero.Application) { user, getErr := arn.GetUserFromTable("GoogleToUser", googleUser.Sub) if getErr == nil && user != nil { + // Add GoogleToUser reference + user.Accounts.Google.ID = googleUser.Sub + arn.DB.Set("GoogleToUser", googleUser.Sub, &arn.GoogleToUser{ + ID: googleUser.Sub, + UserID: user.ID, + }) + session.Set("userId", user.ID) return ctx.Redirect("/") } @@ -104,6 +111,18 @@ func InstallGoogleAuth(app *aero.Application) { return ctx.Redirect("/") } - return ctx.Error(http.StatusForbidden, "Account does not exist", getErr) + user = arn.NewUser() + user.Nick = "g" + googleUser.Sub + user.Email = googleUser.Email + user.FirstName = googleUser.GivenName + user.LastName = googleUser.FamilyName + user.Gender = googleUser.Gender + user.Accounts.Google.ID = googleUser.Sub + user.LastLogin = arn.DateTimeUTC() + + arn.PrettyPrint(user) + // arn.RegisterUser(user) + + return ctx.Error(http.StatusForbidden, "Account does not exist", nil) }) } diff --git a/main.go b/main.go index 4e277623..4dfe6162 100644 --- a/main.go +++ b/main.go @@ -77,6 +77,7 @@ func configure(app *aero.Application) *aero.Application { // Middleware app.Use(middleware.Log()) app.Use(middleware.Session()) + app.Use(middleware.UserInfo()) // API api := api.New("/api/", arn.DB) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go new file mode 100644 index 00000000..f016a48f --- /dev/null +++ b/middleware/UserInfo.go @@ -0,0 +1,99 @@ +package middleware + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strings" + + "github.com/aerogo/aero" + "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/utils" + "github.com/fatih/color" + "github.com/mssola/user_agent" + "github.com/parnurzeal/gorequest" +) + +var apiKeys arn.APIKeys + +func init() { + data, _ := ioutil.ReadFile("security/api-keys.json") + err := json.Unmarshal(data, &apiKeys) + + if err != nil { + panic(err) + } +} + +// UserInfo updates user related information after each request. +func UserInfo() aero.Middleware { + return func(ctx *aero.Context, next func()) { + next() + + // This works asynchronously so it doesn't block the response + go updateUserInfo(ctx) + } +} + +// updateUserInfo is started asynchronously so it doesn't block the request +func updateUserInfo(ctx *aero.Context) { + user := utils.GetUser(ctx) + + // When there's no user logged in, nothing to update + if user == nil { + return + } + + // Ignore non-HTML requests + println(ctx.GetRequestHeader("Accept-Type")) + if strings.Index(ctx.GetRequestHeader("Accept-Type"), "text/html") == -1 { + return + } + + newIP := ctx.RealIP() + newUserAgent := ctx.UserAgent() + + if user.UserAgent != newUserAgent { + user.UserAgent = newUserAgent + + // Parse user agent + parsed := user_agent.New(user.UserAgent) + + // Browser + user.Browser.Name, user.Browser.Version = parsed.Browser() + + arn.PrettyPrint(user.Browser) + + // OS + os := parsed.OSInfo() + user.OS.Name = os.Name + user.OS.Version = os.Version + + arn.PrettyPrint(user.OS) + } + + if user.IP != newIP { + user.IP = newIP + locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + "2a02:8108:8dc0:3000:6cf1:af03:ce6e:679a" + "&format=json" + + response, data, err := gorequest.New().Get(locationAPI).EndBytes() + + if len(err) > 0 && err[0] != nil { + color.Red(err[0].Error()) + return + } + + if response.StatusCode != http.StatusOK { + body, _ := ioutil.ReadAll(response.Body) + fmt.Println(response.StatusCode, locationAPI) + fmt.Println(string(body)) + return + } + + json.Unmarshal(data, &user.Location) + arn.PrettyPrint(user.Location) + } + + // user.Save() +} From efe8a36b7af3c2b8b9ba87a0c8dc37d1cc9e7543 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 22 Jun 2017 22:26:52 +0200 Subject: [PATCH 03/36] Implemented new user registrations --- auth/auth.go | 7 ++++ auth/google.go | 72 ++++++++++++++++++++++++++++++++++-------- auth/log.go | 14 ++++++++ middleware/Log.go | 9 +++++- middleware/UserInfo.go | 27 +++++++--------- pages/forum/forum.pixy | 7 ++-- 6 files changed, 105 insertions(+), 31 deletions(-) create mode 100644 auth/log.go diff --git a/auth/auth.go b/auth/auth.go index 83f1259e..2a855fd2 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -1,6 +1,7 @@ package auth import "github.com/aerogo/aero" +import "github.com/animenotifier/notify.moe/utils" // Install ... func Install(app *aero.Application) { @@ -10,6 +11,12 @@ func Install(app *aero.Application) { // Logout app.Get("/logout", func(ctx *aero.Context) string { if ctx.HasSession() { + user := utils.GetUser(ctx) + + if user != nil { + authLog.Info("User logged out", user.ID, ctx.RealIP(), user.Email, user.RealName()) + } + ctx.Session().Set("userId", nil) } diff --git a/auth/google.go b/auth/google.go index 5e9bc99b..5090d4af 100644 --- a/auth/google.go +++ b/auth/google.go @@ -8,6 +8,7 @@ import ( "github.com/aerogo/aero" "github.com/animenotifier/arn" + "github.com/animenotifier/notify.moe/utils" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) @@ -88,41 +89,86 @@ func InstallGoogleAuth(app *aero.Application) { return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err) } - // Try to find an existing user by the Google user ID - user, getErr := arn.GetUserFromTable("GoogleToUser", googleUser.Sub) + // Is this an existing user connecting another social account? + user := utils.GetUser(ctx) + + if user != nil { + println("Connected") + + // Add GoogleToUser reference + err = user.ConnectGoogle(googleUser.Sub) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err) + } + + return ctx.Redirect("/") + } + + var getErr error + + // Try to find an existing user via the Google user ID + user, getErr = arn.GetUserFromTable("GoogleToUser", googleUser.Sub) if getErr == nil && user != nil { - // Add GoogleToUser reference - user.Accounts.Google.ID = googleUser.Sub - arn.DB.Set("GoogleToUser", googleUser.Sub, &arn.GoogleToUser{ - ID: googleUser.Sub, - UserID: user.ID, - }) + authLog.Info("User logged in via Google ID", user.ID, ctx.RealIP(), user.Email, user.RealName()) + + user.LastLogin = arn.DateTimeUTC() + user.Save() session.Set("userId", user.ID) return ctx.Redirect("/") } - // Try to find an existing user by the associated e-mail address + // Try to find an existing user via the associated e-mail address user, getErr = arn.GetUserByEmail(googleUser.Email) if getErr == nil && user != nil { + authLog.Info("User logged in via Email", user.ID, ctx.RealIP(), user.Email, user.RealName()) + + user.LastLogin = arn.DateTimeUTC() + user.Save() + session.Set("userId", user.ID) return ctx.Redirect("/") } + // Register new user user = arn.NewUser() user.Nick = "g" + googleUser.Sub user.Email = googleUser.Email user.FirstName = googleUser.GivenName user.LastName = googleUser.FamilyName user.Gender = googleUser.Gender - user.Accounts.Google.ID = googleUser.Sub user.LastLogin = arn.DateTimeUTC() - arn.PrettyPrint(user) - // arn.RegisterUser(user) + // Save basic user info already to avoid data inconsistency problems + user.Save() - return ctx.Error(http.StatusForbidden, "Account does not exist", nil) + // Register user + err = arn.RegisterUser(user) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Could not register a new user", err) + } + + // Connect account to a Google account + err = user.ConnectGoogle(googleUser.Sub) + + if err != nil { + ctx.Error(http.StatusInternalServerError, "Could not connect account to Google account", err) + } + + // Save user object again with updated data + user.Save() + + // Login + session.Set("userId", user.ID) + + // Log + authLog.Info("Registered new user", user.ID, ctx.RealIP(), user.Email, user.RealName()) + + // Redirect to frontpage + return ctx.Redirect("/") }) } diff --git a/auth/log.go b/auth/log.go new file mode 100644 index 00000000..d758c4e6 --- /dev/null +++ b/auth/log.go @@ -0,0 +1,14 @@ +package auth + +import ( + "os" + + "github.com/aerogo/log" +) + +var authLog = log.New() + +func init() { + authLog.AddOutput(os.Stdout) + authLog.AddOutput(log.File("logs/auth.log")) +} diff --git a/middleware/Log.go b/middleware/Log.go index 8be4d67c..59dad41d 100644 --- a/middleware/Log.go +++ b/middleware/Log.go @@ -9,6 +9,7 @@ import ( "github.com/aerogo/aero" "github.com/aerogo/log" + "github.com/animenotifier/notify.moe/utils" ) // Log middleware logs every request into logs/request.log and errors into logs/error.log. @@ -27,8 +28,14 @@ func Log() aero.Middleware { responseTimeString := strconv.Itoa(int(responseTime.Nanoseconds()/1000000)) + " ms" responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString + user := utils.GetUser(ctx) + // Log every request - request.Info(ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI()) + if user != nil { + request.Info(user.Nick, ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI()) + } else { + request.Info("[guest]", ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI()) + } // Log all requests that failed switch ctx.StatusCode { diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index f016a48f..7c653e80 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -2,7 +2,6 @@ package middleware import ( "encoding/json" - "fmt" "io/ioutil" "net/http" "strings" @@ -46,8 +45,7 @@ func updateUserInfo(ctx *aero.Context) { } // Ignore non-HTML requests - println(ctx.GetRequestHeader("Accept-Type")) - if strings.Index(ctx.GetRequestHeader("Accept-Type"), "text/html") == -1 { + if strings.Index(ctx.GetRequestHeader("Accept"), "text/html") == -1 { return } @@ -63,37 +61,36 @@ func updateUserInfo(ctx *aero.Context) { // Browser user.Browser.Name, user.Browser.Version = parsed.Browser() - arn.PrettyPrint(user.Browser) - // OS os := parsed.OSInfo() user.OS.Name = os.Name user.OS.Version = os.Version - - arn.PrettyPrint(user.OS) } if user.IP != newIP { user.IP = newIP - locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + "2a02:8108:8dc0:3000:6cf1:af03:ce6e:679a" + "&format=json" + locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json" response, data, err := gorequest.New().Get(locationAPI).EndBytes() if len(err) > 0 && err[0] != nil { - color.Red(err[0].Error()) + color.Red("Couldn't fetch location data | Error: %s | IP: %s", err[0].Error(), user.IP) return } if response.StatusCode != http.StatusOK { - body, _ := ioutil.ReadAll(response.Body) - fmt.Println(response.StatusCode, locationAPI) - fmt.Println(string(body)) + color.Red("Couldn't fetch location data | Status: %d | IP: %s", response.StatusCode, user.IP) return } - json.Unmarshal(data, &user.Location) - arn.PrettyPrint(user.Location) + newLocation := arn.UserLocation{} + json.Unmarshal(data, &newLocation) + + if newLocation.CountryName != "-" { + user.Location = newLocation + } } - // user.Save() + user.LastSeen = arn.DateTimeUTC() + user.Save() } diff --git a/pages/forum/forum.pixy b/pages/forum/forum.pixy index 43800d10..dd4f9ee1 100644 --- a/pages/forum/forum.pixy +++ b/pages/forum/forum.pixy @@ -11,5 +11,8 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int) span Load more component ThreadList(threads []*arn.Thread) - each thread in threads - ThreadLink(thread) \ No newline at end of file + if len(threads) == 0 + p No threads found. + else + each thread in threads + ThreadLink(thread) \ No newline at end of file From 325d17c871ee41ab0e2f5bdc37f0bd6aa83715b6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 22 Jun 2017 22:50:50 +0200 Subject: [PATCH 04/36] Fixed location info --- middleware/UserInfo.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index 7c653e80..d8232989 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -4,6 +4,7 @@ import ( "encoding/json" "io/ioutil" "net/http" + "strconv" "strings" "github.com/aerogo/aero" @@ -83,11 +84,18 @@ func updateUserInfo(ctx *aero.Context) { return } - newLocation := arn.UserLocation{} + newLocation := arn.IPInfoDBLocation{} json.Unmarshal(data, &newLocation) if newLocation.CountryName != "-" { - user.Location = newLocation + user.Location.CountryName = newLocation.CountryName + user.Location.CountryCode = newLocation.CountryCode + user.Location.Latitude, _ = strconv.ParseFloat(newLocation.Latitude, 64) + user.Location.Longitude, _ = strconv.ParseFloat(newLocation.Latitude, 64) + user.Location.CityName = newLocation.CityName + user.Location.RegionName = newLocation.RegionName + user.Location.TimeZone = newLocation.TimeZone + user.Location.ZipCode = newLocation.ZipCode } } From e9e6391e15ca3c67deee725989786fdd167f9580 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Thu, 22 Jun 2017 22:55:25 +0200 Subject: [PATCH 05/36] Fixed location data --- middleware/UserInfo.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index d8232989..d7704903 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -91,7 +91,7 @@ func updateUserInfo(ctx *aero.Context) { user.Location.CountryName = newLocation.CountryName user.Location.CountryCode = newLocation.CountryCode user.Location.Latitude, _ = strconv.ParseFloat(newLocation.Latitude, 64) - user.Location.Longitude, _ = strconv.ParseFloat(newLocation.Latitude, 64) + user.Location.Longitude, _ = strconv.ParseFloat(newLocation.Longitude, 64) user.Location.CityName = newLocation.CityName user.Location.RegionName = newLocation.RegionName user.Location.TimeZone = newLocation.TimeZone From d59169fe36d871d22b9200a3c97a88d619ba9d99 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 00:37:48 +0200 Subject: [PATCH 06/36] Show watching list centered when on mobile --- pages/profile/watching.scarlet | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pages/profile/watching.scarlet b/pages/profile/watching.scarlet index 29699c9d..6b3b9b13 100644 --- a/pages/profile/watching.scarlet +++ b/pages/profile/watching.scarlet @@ -6,4 +6,8 @@ .profile-watching-list-item-image width 55px !important - border-radius 2px \ No newline at end of file + border-radius 2px + +< 380px + .profile-watching-list + justify-content center \ No newline at end of file From db13da79227c7bed9f262a0658e20452875e02b9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 02:02:49 +0200 Subject: [PATCH 07/36] Improved .thread-link for mobile devices --- styles/forum.scarlet | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/styles/forum.scarlet b/styles/forum.scarlet index e9f64f82..75c239fa 100644 --- a/styles/forum.scarlet +++ b/styles/forum.scarlet @@ -3,9 +3,19 @@ // margin-bottom 1rem .thread-link - horizontal + vertical margin 0.25rem 0 + .post-author + margin-bottom 0.25rem + +> 330px + .thread-link + horizontal + + .post-author + margin-bottom 0 + .post-content ui-element flex-grow 1 From f552a803db7f6a1b8fe1173a419d56732dd28222 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 13:28:54 +0200 Subject: [PATCH 08/36] Added /scripts.js for Cloudflare tests --- assets.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/assets.go b/assets.go index fb745c3f..69cc8f80 100644 --- a/assets.go +++ b/assets.go @@ -19,6 +19,11 @@ func init() { return js }) + app.Get("/scripts.js", func(ctx *aero.Context) string { + ctx.SetResponseHeader("Content-Type", "application/javascript") + return js + }) + // Web manifest app.Get("/manifest.json", func(ctx *aero.Context) string { return ctx.JSON(app.Config.Manifest) From 45f85d9a1e65422d791ac5094c1763a6b2db8ca2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 15:38:46 +0200 Subject: [PATCH 09/36] Added host names in request log --- main.go | 1 + middleware/IPToHost.go | 47 ++++++++++++++++++++++++++++++++++++++++++ middleware/Log.go | 16 ++++++++++---- 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 middleware/IPToHost.go diff --git a/main.go b/main.go index 4dfe6162..2df2a9e9 100644 --- a/main.go +++ b/main.go @@ -78,6 +78,7 @@ func configure(app *aero.Application) *aero.Application { app.Use(middleware.Log()) app.Use(middleware.Session()) app.Use(middleware.UserInfo()) + app.Use(middleware.IPToHost()) // API api := api.New("/api/", arn.DB) diff --git a/middleware/IPToHost.go b/middleware/IPToHost.go new file mode 100644 index 00000000..f702bf3b --- /dev/null +++ b/middleware/IPToHost.go @@ -0,0 +1,47 @@ +package middleware + +import ( + "net" + "time" + + "github.com/aerogo/aero" + cache "github.com/patrickmn/go-cache" +) + +var ipToHosts = cache.New(60*time.Minute, 30*time.Minute) + +// IPToHost middleware tries to find domain names for the given IP. +func IPToHost() aero.Middleware { + return func(ctx *aero.Context, next func()) { + next() + + go findHostsForIP(ctx.RealIP()) + } +} + +// GetHostsForIP returns all host names for the given IP (if cached). +func GetHostsForIP(ip string) []string { + hosts, found := ipToHosts.Get(ip) + + if !found || hosts == nil { + return nil + } + + return hosts.([]string) +} + +// Finds all host names for the given IP +func findHostsForIP(ip string) { + hosts, err := net.LookupAddr(ip) + + if err != nil { + return + } + + if len(hosts) == 0 { + return + } + + // Cache host names + ipToHosts.Set(ip, hosts, cache.DefaultExpiration) +} diff --git a/middleware/Log.go b/middleware/Log.go index 59dad41d..84c53f4d 100644 --- a/middleware/Log.go +++ b/middleware/Log.go @@ -29,12 +29,20 @@ func Log() aero.Middleware { responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString user := utils.GetUser(ctx) + ip := ctx.RealIP() + + hostName := "" + hostNames := GetHostsForIP(ip) + + if len(hostNames) != 0 { + hostName = hostNames[0] + } // Log every request if user != nil { - request.Info(user.Nick, ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI()) + request.Info(user.Nick, ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) } else { - request.Info("[guest]", ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI()) + request.Info("[guest]", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) } // Log all requests that failed @@ -43,13 +51,13 @@ func Log() aero.Middleware { // Ok. default: - err.Error(http.StatusText(ctx.StatusCode), ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI()) + err.Error(http.StatusText(ctx.StatusCode), ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) } // Notify us about long requests. // However ignore requests under /auth/ because those depend on 3rd party servers. if responseTime >= 200*time.Millisecond && !strings.HasPrefix(ctx.URI(), "/auth/") { - err.Error("Long response time", ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI()) + err.Error("Long response time", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) } } } From 3cd8b403c5e7198c555158d6215c9db2a9b686b5 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 15:51:26 +0200 Subject: [PATCH 10/36] Improved logger --- middleware/IPToHost.go | 14 +++++-- middleware/Log.go | 84 +++++++++++++++++++++++------------------- 2 files changed, 57 insertions(+), 41 deletions(-) diff --git a/middleware/IPToHost.go b/middleware/IPToHost.go index f702bf3b..09eab148 100644 --- a/middleware/IPToHost.go +++ b/middleware/IPToHost.go @@ -23,7 +23,11 @@ func IPToHost() aero.Middleware { func GetHostsForIP(ip string) []string { hosts, found := ipToHosts.Get(ip) - if !found || hosts == nil { + if !found { + hosts = findHostsForIP(ip) + } + + if hosts == nil { return nil } @@ -31,17 +35,19 @@ func GetHostsForIP(ip string) []string { } // Finds all host names for the given IP -func findHostsForIP(ip string) { +func findHostsForIP(ip string) []string { hosts, err := net.LookupAddr(ip) if err != nil { - return + return nil } if len(hosts) == 0 { - return + return nil } // Cache host names ipToHosts.Set(ip, hosts, cache.DefaultExpiration) + + return hosts } diff --git a/middleware/Log.go b/middleware/Log.go index 84c53f4d..a010d22a 100644 --- a/middleware/Log.go +++ b/middleware/Log.go @@ -12,52 +12,62 @@ import ( "github.com/animenotifier/notify.moe/utils" ) -// Log middleware logs every request into logs/request.log and errors into logs/error.log. -func Log() aero.Middleware { - request := log.New() +var request = log.New() +var err = log.New() + +// Initialize log files +func init() { request.AddOutput(log.File("logs/request.log")) - err := log.New() err.AddOutput(log.File("logs/error.log")) err.AddOutput(os.Stderr) +} +// Log middleware logs every request into logs/request.log and errors into logs/error.log. +func Log() aero.Middleware { return func(ctx *aero.Context, next func()) { start := time.Now() next() responseTime := time.Since(start) - responseTimeString := strconv.Itoa(int(responseTime.Nanoseconds()/1000000)) + " ms" - responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString - user := utils.GetUser(ctx) - ip := ctx.RealIP() - - hostName := "" - hostNames := GetHostsForIP(ip) - - if len(hostNames) != 0 { - hostName = hostNames[0] - } - - // Log every request - if user != nil { - request.Info(user.Nick, ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) - } else { - request.Info("[guest]", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) - } - - // Log all requests that failed - switch ctx.StatusCode { - case http.StatusOK, http.StatusFound, http.StatusMovedPermanently, http.StatusPermanentRedirect, http.StatusTemporaryRedirect: - // Ok. - - default: - err.Error(http.StatusText(ctx.StatusCode), ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) - } - - // Notify us about long requests. - // However ignore requests under /auth/ because those depend on 3rd party servers. - if responseTime >= 200*time.Millisecond && !strings.HasPrefix(ctx.URI(), "/auth/") { - err.Error("Long response time", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) - } + go logRequest(ctx, responseTime) + } +} + +// Logs a single request +func logRequest(ctx *aero.Context, responseTime time.Duration) { + responseTimeString := strconv.Itoa(int(responseTime.Nanoseconds()/1000000)) + " ms" + responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString + + user := utils.GetUser(ctx) + ip := ctx.RealIP() + + hostName := "" + hostNames := GetHostsForIP(ip) + + if len(hostNames) != 0 { + hostName = hostNames[0] + } + + // Log every request + if user != nil { + request.Info(user.Nick, ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) + } else { + request.Info("[guest]", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) + } + + // Log all requests that failed + switch ctx.StatusCode { + case http.StatusOK, http.StatusFound, http.StatusMovedPermanently, http.StatusPermanentRedirect, http.StatusTemporaryRedirect: + // Ok. + + default: + err.Error(http.StatusText(ctx.StatusCode), ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) + } + + // Notify us about long requests. + // However ignore requests under /auth/ because those depend on 3rd party servers. + if responseTime >= 200*time.Millisecond && !strings.HasPrefix(ctx.URI(), "/auth/") { + err.Error("Long response time", ip, hostName, responseTimeString, ctx.StatusCode, ctx.URI()) } } From 0204958e818c91caad3a74e6395db5d22612d53d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 15:53:22 +0200 Subject: [PATCH 11/36] Minor changes --- middleware/Log.go | 1 + 1 file changed, 1 insertion(+) diff --git a/middleware/Log.go b/middleware/Log.go index a010d22a..1a9f9472 100644 --- a/middleware/Log.go +++ b/middleware/Log.go @@ -47,6 +47,7 @@ func logRequest(ctx *aero.Context, responseTime time.Duration) { if len(hostNames) != 0 { hostName = hostNames[0] + hostName = strings.TrimSuffix(hostName, ".") } // Log every request From 5476753520d02cc878f7aa85500bbb8141f2a4d8 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 15:57:58 +0200 Subject: [PATCH 12/36] Removed unnecessary middleware --- main.go | 1 - middleware/IPToHost.go | 10 ---------- 2 files changed, 11 deletions(-) diff --git a/main.go b/main.go index 2df2a9e9..4dfe6162 100644 --- a/main.go +++ b/main.go @@ -78,7 +78,6 @@ func configure(app *aero.Application) *aero.Application { app.Use(middleware.Log()) app.Use(middleware.Session()) app.Use(middleware.UserInfo()) - app.Use(middleware.IPToHost()) // API api := api.New("/api/", arn.DB) diff --git a/middleware/IPToHost.go b/middleware/IPToHost.go index 09eab148..e4273465 100644 --- a/middleware/IPToHost.go +++ b/middleware/IPToHost.go @@ -4,21 +4,11 @@ import ( "net" "time" - "github.com/aerogo/aero" cache "github.com/patrickmn/go-cache" ) var ipToHosts = cache.New(60*time.Minute, 30*time.Minute) -// IPToHost middleware tries to find domain names for the given IP. -func IPToHost() aero.Middleware { - return func(ctx *aero.Context, next func()) { - next() - - go findHostsForIP(ctx.RealIP()) - } -} - // GetHostsForIP returns all host names for the given IP (if cached). func GetHostsForIP(ip string) []string { hosts, found := ipToHosts.Get(ip) From 8c182f8a4e33819d000c6b7f642171d4bf31d63c Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 16:41:24 +0200 Subject: [PATCH 13/36] Sort user list by last seen users --- jobs/active-users/main.go | 2 +- pages/profile/profile.pixy | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/jobs/active-users/main.go b/jobs/active-users/main.go index 9e55d7c3..db0bfe96 100644 --- a/jobs/active-users/main.go +++ b/jobs/active-users/main.go @@ -24,7 +24,7 @@ func main() { // Sort sort.Slice(users, func(i, j int) bool { - return users[i].Registered < users[j].Registered + return users[i].LastSeen > users[j].LastSeen }) // Add users to list diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 1854238c..8a0b5130 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -83,3 +83,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .post-toolbar.active .spacer .post-likes= len(post.Likes) + + if user != nil && user.Role == "admin" + .side-note + a(href="/api/user/" + viewUser.ID)= "API: ", user.Nick \ No newline at end of file From 8bee2f4e102e47df3aa377550076af64079bf15d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 16:53:07 +0200 Subject: [PATCH 14/36] Improved user sort --- jobs/active-users/main.go | 10 +++++++++- patches/add-last-seen/main.go | 29 +++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 patches/add-last-seen/main.go diff --git a/jobs/active-users/main.go b/jobs/active-users/main.go index db0bfe96..8c520285 100644 --- a/jobs/active-users/main.go +++ b/jobs/active-users/main.go @@ -24,7 +24,15 @@ func main() { // Sort sort.Slice(users, func(i, j int) bool { - return users[i].LastSeen > users[j].LastSeen + if users[i].LastSeen < users[j].LastSeen { + return false + } + + if users[i].LastSeen > users[j].LastSeen { + return true + } + + return users[i].Registered > users[j].Registered }) // Add users to list diff --git a/patches/add-last-seen/main.go b/patches/add-last-seen/main.go new file mode 100644 index 00000000..ca8fcc3c --- /dev/null +++ b/patches/add-last-seen/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "github.com/animenotifier/arn" +) + +func main() { + // Get a stream of all users + allUsers, err := arn.AllUsers() + + if err != nil { + panic(err) + } + + // Iterate over the stream + for user := range allUsers { + if user.LastSeen != "" { + continue + } + + user.LastSeen = user.LastLogin + + if user.LastSeen == "" { + user.LastSeen = user.Registered + } + + user.Save() + } +} From 389dbae5b4f3917f5165ce0a053c3ee5996ad5f2 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 16:53:40 +0200 Subject: [PATCH 15/36] Fixed API link --- pages/profile/profile.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 8a0b5130..875a1761 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -86,4 +86,4 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, if user != nil && user.Role == "admin" .side-note - a(href="/api/user/" + viewUser.ID)= "API: ", user.Nick \ No newline at end of file + a(href="/api/user/" + viewUser.ID)= "API: ", viewUser.Nick \ No newline at end of file From 17b8282b5e197c7b9f482dd71debb317f38deba7 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 16:58:12 +0200 Subject: [PATCH 16/36] Fixed UserInfo middleware --- middleware/UserInfo.go | 88 ++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 42 deletions(-) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index d7704903..a06e47e1 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -31,25 +31,25 @@ func UserInfo() aero.Middleware { return func(ctx *aero.Context, next func()) { next() + // Ignore non-HTML requests + if strings.Index(ctx.GetRequestHeader("Accept"), "text/html") == -1 { + return + } + + user := utils.GetUser(ctx) + + // When there's no user logged in, nothing to update + if user == nil { + return + } + // This works asynchronously so it doesn't block the response - go updateUserInfo(ctx) + go updateUserInfo(ctx, user) } } // updateUserInfo is started asynchronously so it doesn't block the request -func updateUserInfo(ctx *aero.Context) { - user := utils.GetUser(ctx) - - // When there's no user logged in, nothing to update - if user == nil { - return - } - - // Ignore non-HTML requests - if strings.Index(ctx.GetRequestHeader("Accept"), "text/html") == -1 { - return - } - +func updateUserInfo(ctx *aero.Context, user *arn.User) { newIP := ctx.RealIP() newUserAgent := ctx.UserAgent() @@ -69,36 +69,40 @@ func updateUserInfo(ctx *aero.Context) { } if user.IP != newIP { - user.IP = newIP - locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json" - - response, data, err := gorequest.New().Get(locationAPI).EndBytes() - - if len(err) > 0 && err[0] != nil { - color.Red("Couldn't fetch location data | Error: %s | IP: %s", err[0].Error(), user.IP) - return - } - - if response.StatusCode != http.StatusOK { - color.Red("Couldn't fetch location data | Status: %d | IP: %s", response.StatusCode, user.IP) - return - } - - newLocation := arn.IPInfoDBLocation{} - json.Unmarshal(data, &newLocation) - - if newLocation.CountryName != "-" { - user.Location.CountryName = newLocation.CountryName - user.Location.CountryCode = newLocation.CountryCode - user.Location.Latitude, _ = strconv.ParseFloat(newLocation.Latitude, 64) - user.Location.Longitude, _ = strconv.ParseFloat(newLocation.Longitude, 64) - user.Location.CityName = newLocation.CityName - user.Location.RegionName = newLocation.RegionName - user.Location.TimeZone = newLocation.TimeZone - user.Location.ZipCode = newLocation.ZipCode - } + updateUserLocation(user, newIP) } user.LastSeen = arn.DateTimeUTC() user.Save() } + +func updateUserLocation(user *arn.User, newIP string) { + user.IP = newIP + locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json" + + response, data, err := gorequest.New().Get(locationAPI).EndBytes() + + if len(err) > 0 && err[0] != nil { + color.Red("Couldn't fetch location data | Error: %s | IP: %s", err[0].Error(), user.IP) + return + } + + if response.StatusCode != http.StatusOK { + color.Red("Couldn't fetch location data | Status: %d | IP: %s", response.StatusCode, user.IP) + return + } + + newLocation := arn.IPInfoDBLocation{} + json.Unmarshal(data, &newLocation) + + if newLocation.CountryName != "-" { + user.Location.CountryName = newLocation.CountryName + user.Location.CountryCode = newLocation.CountryCode + user.Location.Latitude, _ = strconv.ParseFloat(newLocation.Latitude, 64) + user.Location.Longitude, _ = strconv.ParseFloat(newLocation.Longitude, 64) + user.Location.CityName = newLocation.CityName + user.Location.RegionName = newLocation.RegionName + user.Location.TimeZone = newLocation.TimeZone + user.Location.ZipCode = newLocation.ZipCode + } +} From 776078d333afe08d243c8b12df8aa32a60aad75b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 17:00:41 +0200 Subject: [PATCH 17/36] Better comments --- middleware/UserInfo.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index a06e47e1..3867e8d6 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -48,7 +48,7 @@ func UserInfo() aero.Middleware { } } -// updateUserInfo is started asynchronously so it doesn't block the request +// Update browser and OS data func updateUserInfo(ctx *aero.Context, user *arn.User) { newIP := ctx.RealIP() newUserAgent := ctx.UserAgent() @@ -76,6 +76,7 @@ func updateUserInfo(ctx *aero.Context, user *arn.User) { user.Save() } +// Updates the location of the user. func updateUserLocation(user *arn.User, newIP string) { user.IP = newIP locationAPI := "https://api.ipinfodb.com/v3/ip-city/?key=" + apiKeys.IPInfoDB.ID + "&ip=" + user.IP + "&format=json" From 294e8db22c1e51151eb625b764235c3a2693572f Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 17:15:48 +0200 Subject: [PATCH 18/36] Updated tests --- tests.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests.go b/tests.go index a63318eb..53b1352e 100644 --- a/tests.go +++ b/tests.go @@ -10,6 +10,10 @@ var tests = map[string][]string{ "/+Akyoto/threads", }, + "/user/:nick/posts": []string{ + "/+Akyoto/posts", + }, + "/user/:nick/animelist": []string{ "/+Akyoto/animelist", }, @@ -56,6 +60,14 @@ var tests = map[string][]string{ "/api/animelist/4J6qpK1ve", }, + "/api/animelist/:id/get/:item": []string{ + "/api/animelist/4J6qpK1ve/get/7929", + }, + + "/api/animelist/:id/get/:item/:property": []string{ + "/api/animelist/4J6qpK1ve/get/7929/Episodes", + }, + "/api/settings/:id": []string{ "/api/settings/4J6qpK1ve", }, From 7269bbbaca96c2843da4c7305de824e4b48ccc72 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 18:22:48 +0200 Subject: [PATCH 19/36] General improvements --- auth/google.go | 2 +- config.json | 5 +---- jobs/search-index/main.go | 4 ++-- pages/profile/profile.go | 3 ++- styles/{forms.scarlet => input.scarlet} | 18 ++++++++++++------ 5 files changed, 18 insertions(+), 14 deletions(-) rename styles/{forms.scarlet => input.scarlet} (88%) diff --git a/auth/google.go b/auth/google.go index 5090d4af..ee90f0f7 100644 --- a/auth/google.go +++ b/auth/google.go @@ -166,7 +166,7 @@ func InstallGoogleAuth(app *aero.Application) { session.Set("userId", user.ID) // Log - authLog.Info("Registered new user", user.ID, ctx.RealIP(), user.Email, user.RealName()) + authLog.Info("Registered new user", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) // Redirect to frontpage return ctx.Redirect("/") diff --git a/config.json b/config.json index ab0a1d90..bebbdef8 100644 --- a/config.json +++ b/config.json @@ -14,12 +14,9 @@ "layout", "navigation", "headers", - "forms", + "input", "grid", - "colors", - "animelist", "forum", - "settings", "user", "video", "loading", diff --git a/jobs/search-index/main.go b/jobs/search-index/main.go index 214d4a2e..aa98d388 100644 --- a/jobs/search-index/main.go +++ b/jobs/search-index/main.go @@ -4,7 +4,7 @@ import ( "fmt" "strings" - "github.com/aerogo/aero" + "github.com/aerogo/flow" "github.com/animenotifier/arn" "github.com/fatih/color" ) @@ -12,7 +12,7 @@ import ( func main() { color.Yellow("Updating search index") - aero.Parallel(updateAnimeIndex, updateUserIndex) + flow.Parallel(updateAnimeIndex, updateUserIndex) color.Green("Finished.") } diff --git a/pages/profile/profile.go b/pages/profile/profile.go index 9e660b42..c146f2ac 100644 --- a/pages/profile/profile.go +++ b/pages/profile/profile.go @@ -2,6 +2,7 @@ package profile import ( "github.com/aerogo/aero" + "github.com/aerogo/flow" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" "github.com/animenotifier/notify.moe/utils" @@ -28,7 +29,7 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string { var animeList *arn.AnimeList var posts []*arn.Post - aero.Parallel(func() { + flow.Parallel(func() { user = utils.GetUser(ctx) }, func() { animeList = viewUser.AnimeList() diff --git a/styles/forms.scarlet b/styles/input.scarlet similarity index 88% rename from styles/forms.scarlet rename to styles/input.scarlet index a151dde3..8c272583 100644 --- a/styles/forms.scarlet +++ b/styles/input.scarlet @@ -1,3 +1,10 @@ +mixin input-focus + :focus + color black + border 1px solid main-color + // TODO: Replace with alpha(main-color, 20%) function + box-shadow 0 0 6px rgba(248, 165, 130, 0.2) + input, textarea, button, select font-family inherit font-size 1em @@ -9,16 +16,15 @@ input, textarea border ui-border background white box-shadow none - - :focus - color black - border 1px solid main-color - // TODO: Replace with alpha(main-color, 20%) function - box-shadow 0 0 6px rgba(248, 165, 130, 0.2) + input-focus :disabled ui-disabled +// We need this to have a selector with a higher priority than .widget-element:focus +input.widget-element + input-focus + button, .button ui-element horizontal From b5d2cc0465b59dfabc560d6b33329ac504f74c0d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 19:22:39 +0200 Subject: [PATCH 20/36] Refactor --- main.go | 3 +-- middleware/UserInfo.go | 14 +++++++++++++- utils/production.go | 17 ----------------- utils/user.go | 20 ++------------------ 4 files changed, 16 insertions(+), 38 deletions(-) delete mode 100644 utils/production.go diff --git a/main.go b/main.go index 4dfe6162..edc47ead 100644 --- a/main.go +++ b/main.go @@ -26,7 +26,6 @@ import ( "github.com/animenotifier/notify.moe/pages/user" "github.com/animenotifier/notify.moe/pages/users" "github.com/animenotifier/notify.moe/pages/webdev" - "github.com/animenotifier/notify.moe/utils" ) var app = aero.New() @@ -84,7 +83,7 @@ func configure(app *aero.Application) *aero.Application { api.Install(app) // Domain - if utils.IsDevelopment() { + if arn.IsDevelopment() { app.Config.Domain = "beta.notify.moe" } diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index 3867e8d6..9f1a8429 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -32,7 +32,14 @@ func UserInfo() aero.Middleware { next() // Ignore non-HTML requests - if strings.Index(ctx.GetRequestHeader("Accept"), "text/html") == -1 { + if ctx.IsMediaResponse() { + return + } + + // Ignore API requests + // Note that API requests can filter data (privacy) and we might accidentally save the filtered data. + // That's why it's very important to ignore all API requests and not call user.Save() in this context. + if strings.HasPrefix(ctx.URI(), "/api/") { return } @@ -43,6 +50,11 @@ func UserInfo() aero.Middleware { return } + // Let's be 100% sure we really do not accidentally save filtered data. + if user.Email == "" && user.IP == "" && user.FirstName == "" { + return + } + // This works asynchronously so it doesn't block the response go updateUserInfo(ctx, user) } diff --git a/utils/production.go b/utils/production.go deleted file mode 100644 index 334a0991..00000000 --- a/utils/production.go +++ /dev/null @@ -1,17 +0,0 @@ -package utils - -import ( - "os" - "strings" -) - -// IsProduction returns true if the hostname contains "arn". -func IsProduction() bool { - host, _ := os.Hostname() - return strings.Contains(host, "arn") -} - -// IsDevelopment returns true if the hostname does not contain "arn". -func IsDevelopment() bool { - return !IsProduction() -} diff --git a/utils/user.go b/utils/user.go index ff52a7df..3e665f7e 100644 --- a/utils/user.go +++ b/utils/user.go @@ -5,23 +5,7 @@ import ( "github.com/animenotifier/arn" ) -// GetUser ... +// GetUser returns the logged in user for the given context. func GetUser(ctx *aero.Context) *arn.User { - if !ctx.HasSession() { - return nil - } - - userID := ctx.Session().GetString("userId") - - if userID == "" { - return nil - } - - user, err := arn.GetUser(userID) - - if err != nil { - return nil - } - - return user + return arn.GetUserFromContext(ctx) } From 3286b3b7dbaa3303724a8506a6a6c3e5992d1e20 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 19:26:54 +0200 Subject: [PATCH 21/36] Removed incorrect comments --- middleware/UserInfo.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/middleware/UserInfo.go b/middleware/UserInfo.go index 9f1a8429..96aecab1 100644 --- a/middleware/UserInfo.go +++ b/middleware/UserInfo.go @@ -37,8 +37,6 @@ func UserInfo() aero.Middleware { } // Ignore API requests - // Note that API requests can filter data (privacy) and we might accidentally save the filtered data. - // That's why it's very important to ignore all API requests and not call user.Save() in this context. if strings.HasPrefix(ctx.URI(), "/api/") { return } @@ -50,11 +48,6 @@ func UserInfo() aero.Middleware { return } - // Let's be 100% sure we really do not accidentally save filtered data. - if user.Email == "" && user.IP == "" && user.FirstName == "" { - return - } - // This works asynchronously so it doesn't block the response go updateUserInfo(ctx, user) } From 4462f7c5ffd8b038237ca62e2e846259b550401e Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Fri, 23 Jun 2017 21:05:30 +0200 Subject: [PATCH 22/36] Renamed Search component to SearchResults --- pages/search/search.go | 2 +- pages/search/search.pixy | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pages/search/search.go b/pages/search/search.go index 34e6a19f..a7dd0516 100644 --- a/pages/search/search.go +++ b/pages/search/search.go @@ -14,5 +14,5 @@ func Get(ctx *aero.Context) string { term := ctx.Get("term") userResults, animeResults := arn.Search(term, maxUsers, maxAnime) - return ctx.HTML(components.Search(userResults, animeResults)) + return ctx.HTML(components.SearchResults(userResults, animeResults)) } diff --git a/pages/search/search.pixy b/pages/search/search.pixy index 312ded75..6e12b652 100644 --- a/pages/search/search.pixy +++ b/pages/search/search.pixy @@ -1,4 +1,4 @@ -component Search(users []*arn.User, animeResults []*arn.Anime) +component SearchResults(users []*arn.User, animeResults []*arn.Anime) .widgets .widget h3 Users From 3fcb1bc36e9df5846011a854add9b29de7e99af9 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 00:36:07 +0200 Subject: [PATCH 23/36] Cleanup --- jobs/popular-anime/main.go | 3 +-- pages/popularanime/old/popular.pixy | 16 ++++++++++++++++ pages/popularanime/old/popular.scarlet | 19 +++++++++++++++++++ pages/popularanime/popular.go | 23 +++-------------------- pages/popularanime/popular.pixy | 16 ---------------- pages/popularanime/popular.scarlet | 19 ------------------- 6 files changed, 39 insertions(+), 57 deletions(-) create mode 100644 pages/popularanime/old/popular.pixy create mode 100644 pages/popularanime/old/popular.scarlet delete mode 100644 pages/popularanime/popular.pixy delete mode 100644 pages/popularanime/popular.scarlet diff --git a/jobs/popular-anime/main.go b/jobs/popular-anime/main.go index 05eb5f55..c90015f2 100644 --- a/jobs/popular-anime/main.go +++ b/jobs/popular-anime/main.go @@ -7,7 +7,6 @@ import ( "github.com/fatih/color" ) - const maxPopularAnime = 10 // Note this is using the airing-anime as a template with modfications @@ -32,7 +31,7 @@ func main() { sort.Slice(animeList, func(i, j int) bool { return animeList[i].Rating.Overall > animeList[j].Rating.Overall }) - + // Change size of anime list to 10 animeList = animeList[:maxPopularAnime] diff --git a/pages/popularanime/old/popular.pixy b/pages/popularanime/old/popular.pixy new file mode 100644 index 00000000..4d650c83 --- /dev/null +++ b/pages/popularanime/old/popular.pixy @@ -0,0 +1,16 @@ +component OldPopularAnime(popularAnime []*arn.Anime, titleCount int, animeCount int) + //- h2 Anime + + //- #search-container + //- input#search(type="text", placeholder="Search...", onkeyup="$.searchAnime();", onfocus="this.select();", disabled="disabled", data-count=titleCount, data-anime-count=animeCount) + + //- #search-results-container + //- #search-results + + //- if popularAnime != nil + //- h3.popular-title Popular + + //- .popular-anime-list + //- each anime in popularAnime + //- a.popular-anime.ajax(href="/anime/" + toString(anime.ID), title=anime.Title.Romaji + " (" + arn.Plural(anime.Watching(), "user") + " watching)") + //- img.anime-image.popular-anime-image(src=anime.Image, alt=anime.Title.Romaji) \ No newline at end of file diff --git a/pages/popularanime/old/popular.scarlet b/pages/popularanime/old/popular.scarlet new file mode 100644 index 00000000..6ee9f2dd --- /dev/null +++ b/pages/popularanime/old/popular.scarlet @@ -0,0 +1,19 @@ +// .popular-title +// text-align center + +// .popular-anime-list +// display flex +// flex-flow row wrap +// justify-content center + +// .popular-anime +// padding 0.5em +// display block + +// .popular-anime-image +// width 100px !important +// height 141px !important +// border-radius 3px +// object-fit cover +// default-transition +// shadow-up \ No newline at end of file diff --git a/pages/popularanime/popular.go b/pages/popularanime/popular.go index 0d35d971..c7cd7e22 100644 --- a/pages/popularanime/popular.go +++ b/pages/popularanime/popular.go @@ -1,6 +1,8 @@ package popularanime import ( + "net/http" + "github.com/aerogo/aero" "github.com/animenotifier/arn" "github.com/animenotifier/notify.moe/components" @@ -8,29 +10,10 @@ import ( // Get search page. func Get(ctx *aero.Context) string { - // titleCount := 0 - // animeCount := 0 - - // // let info: any = await bluebird.props({ - // // popular: arn.db.get('Cache', 'popularAnime'), - // // stats: arn.db.get('Cache', 'animeStats') - // // }) - - // // return response.render({ - // // user, - // // popularAnime: info.popular.anime, - // // animeCount: info.stats.animeCount, - // // titleCount: info.stats.titleCount, - // // anime: null - // // }) - - // popular, _ := arn.GetPopularCache() - - // return ctx.HTML(components.Search(popular.Anime, titleCount, animeCount)) animeList, err := arn.GetPopularAnimeCached() if err != nil { - return ctx.HTML("There was a problem listing anime!") + return ctx.Error(http.StatusInternalServerError, "Error fetching popular anime", err) } return ctx.HTML(components.AnimeGrid(animeList)) diff --git a/pages/popularanime/popular.pixy b/pages/popularanime/popular.pixy deleted file mode 100644 index dc922971..00000000 --- a/pages/popularanime/popular.pixy +++ /dev/null @@ -1,16 +0,0 @@ -component Search(popularAnime []*arn.Anime, titleCount int, animeCount int) - h2 Anime - - #search-container - input#search(type="text", placeholder="Search...", onkeyup="$.searchAnime();", onfocus="this.select();", disabled="disabled", data-count=titleCount, data-anime-count=animeCount) - - #search-results-container - #search-results - - if popularAnime != nil - h3.popular-title Popular - - .popular-anime-list - each anime in popularAnime - a.popular-anime.ajax(href="/anime/" + toString(anime.ID), title=anime.Title.Romaji + " (" + arn.Plural(anime.Watching(), "user") + " watching)") - img.anime-image.popular-anime-image(src=anime.Image, alt=anime.Title.Romaji) \ No newline at end of file diff --git a/pages/popularanime/popular.scarlet b/pages/popularanime/popular.scarlet deleted file mode 100644 index fa3aa153..00000000 --- a/pages/popularanime/popular.scarlet +++ /dev/null @@ -1,19 +0,0 @@ -.popular-title - text-align center - -.popular-anime-list - display flex - flex-flow row wrap - justify-content center - -.popular-anime - padding 0.5em - display block - -.popular-anime-image - width 100px !important - height 141px !important - border-radius 3px - object-fit cover - default-transition - shadow-up \ No newline at end of file From 8d976488eed1a950012d692b784bf11eaf0d1eea Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 02:10:04 +0200 Subject: [PATCH 24/36] Allow editing of settings --- patches/user-references/main.go | 2 +- scripts/AnimeNotifier.ts | 7 ++++++- scripts/Diff.ts | 7 ++++++- scripts/actions.ts | 2 ++ 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/patches/user-references/main.go b/patches/user-references/main.go index 0dbc4a51..46ad2777 100644 --- a/patches/user-references/main.go +++ b/patches/user-references/main.go @@ -25,7 +25,7 @@ func main() { count++ println(count, user.Nick) - user.SetNick(user.Nick) + user.ForceSetNick(user.Nick) if user.Email != "" { user.SetEmail(user.Email) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 61622040..b3cbead9 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -30,6 +30,7 @@ export class AnimeNotifier { }) .then(response => response.text()) .then(html => Diff.innerHTML(this.app.content, html)) + .then(() => this.app.emit("DOMContentLoaded")) } loading(isLoading: boolean) { @@ -42,13 +43,17 @@ export class AnimeNotifier { updateActions() { for(let element of findAll("action")) { + if(element["action assigned"]) { + continue + } + let actionName = element.dataset.action element.addEventListener(element.dataset.trigger, e => { actions[actionName](this, element, e) }) - element.classList.remove("action") + element["action assigned"] = true } } diff --git a/scripts/Diff.ts b/scripts/Diff.ts index 394d06dd..fc59b7c2 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -48,6 +48,11 @@ export class Diff { a.setAttribute(attrib.name, b.getAttribute(attrib.name)) } } + + // Special case: Apply state of input elements + if(a !== document.activeElement && a instanceof HTMLInputElement && b instanceof HTMLInputElement) { + a.value = b.value + } } Diff.childNodes(a, b) @@ -57,7 +62,7 @@ export class Diff { static innerHTML(aRoot: HTMLElement, html: string) { let bRoot = document.createElement("main") bRoot.innerHTML = html - + Diff.childNodes(aRoot, bRoot) } } \ No newline at end of file diff --git a/scripts/actions.ts b/scripts/actions.ts index c6a8f702..a6e7cb5e 100644 --- a/scripts/actions.ts +++ b/scripts/actions.ts @@ -48,6 +48,8 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE .then(() => { arn.loading(false) input.disabled = false + + return arn.reloadContent() }) } From 79ba7ecf3b24dd81eadfe27b6b3c97f9e189bd62 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 02:31:49 +0200 Subject: [PATCH 25/36] Skip iframes in DOM diff --- scripts/Diff.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/Diff.ts b/scripts/Diff.ts index fc59b7c2..aa4ab348 100644 --- a/scripts/Diff.ts +++ b/scripts/Diff.ts @@ -25,6 +25,10 @@ export class Diff { } if(a.nodeType === Node.ELEMENT_NODE) { + if(a.tagName === "IFRAME") { + continue + } + let removeAttributes: Attr[] = [] for(let x = 0; x < a.attributes.length; x++) { From b6c4968c6c4100e50babd31ad7dd72a020ae181b Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 16:17:38 +0200 Subject: [PATCH 26/36] Lazy load images --- mixins/Input.pixy | 6 ++--- pages/anime/anime.pixy | 6 +++-- pages/anime/anime.scarlet | 5 ++-- pages/profile/profile.pixy | 6 ++--- pages/settings/settings.pixy | 16 +++++++++--- scripts/AnimeNotifier.ts | 49 +++++++++++++++++++++++++++++------- 6 files changed, 65 insertions(+), 23 deletions(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index 7da14926..e18d2a3f 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -1,14 +1,14 @@ component InputText(id string, value string, label string, placeholder string) .widget-input label(for=id)= label + ":" - input.widget-element.action(id=id, type="text", value=value, placeholder=placeholder, data-action="save", data-trigger="change") + input.widget-element.action(id=id, type="text", value=value, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") component InputTextArea(id string, value string, label string, placeholder string) .widget-input label(for=id)= label + ":" - textarea.widget-element.action(id=id, placeholder=placeholder, data-action="save", data-trigger="change")= value + textarea.widget-element.action(id=id, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change")= value component InputNumber(id string, value int, label string, placeholder string, min string, max string) .widget-input label(for=id)= label + ":" - input.widget-element.action(id=id, type="number", value=value, min=min, max=max, placeholder=placeholder, data-action="save", data-trigger="change") \ No newline at end of file + input.widget-element.action(id=id, type="number", value=value, min=min, max=max, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") \ No newline at end of file diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index d97034f1..ad838187 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -159,8 +159,10 @@ component Anime(anime *arn.Anime, user *arn.User) //- if providers.AnimePlanet //- a.light-button(href="http://www.anime-planet.com/anime/" + providers.AnimePlanet.providerId, target="_blank") AnimePlanet - .sources - p Powered by Kitsu. + .footer + a(href="/api/anime/" + anime.ID) Anime API + span | + span Powered by Kitsu. //- if descriptionSource //- span= " Summary by " + summarySource + "." //- //- diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index 15721b7f..d3de1f37 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -87,10 +87,11 @@ .anime-rating-categories vertical -.sources +.footer font-size 0.8rem - opacity 0.5 + opacity 0.7 margin-top 0.5rem + text-align center .relations horizontal-wrap diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 875a1761..2e9d344e 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -84,6 +84,6 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .spacer .post-likes= len(post.Likes) - if user != nil && user.Role == "admin" - .side-note - a(href="/api/user/" + viewUser.ID)= "API: ", viewUser.Nick \ No newline at end of file + + .footer + a(href="/api/user/" + viewUser.ID) User API \ No newline at end of file diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 3cd68590..3fb7b1aa 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -1,7 +1,7 @@ component Settings(user *arn.User) h2.page-title Settings - .widgets(data-api="/api/user/" + user.ID) - .widget.mountable + .widgets + .widget.mountable(data-api="/api/user/" + user.ID) h3.widget-title Icon("user") span Personal @@ -10,11 +10,19 @@ component Settings(user *arn.User) InputText("Tagline", user.Tagline, "Tagline", "Text that appears below your username") InputText("Website", user.Website, "Website", "Your homepage") - .widget.mountable + .widget.mountable(data-api="/api/user/" + user.ID) h3.widget-title Icon("cubes") span Accounts InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co") InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net") - InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") \ No newline at end of file + InputText("Accounts.AnimePlanet.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") + InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") + + .widget.mountable(data-api="/api/settings/" + user.ID) + h3.widget-title + Icon("cogs") + span Settings + + InputText("TitleLanguage", user.Settings().TitleLanguage, "Title language", "Language of anime titles") \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index b3cbead9..6a4ad029 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -5,9 +5,21 @@ import * as actions from "./actions" export class AnimeNotifier { app: Application + visibilityObserver: IntersectionObserver constructor(app: Application) { this.app = app + this.visibilityObserver = new IntersectionObserver( + entries => { + for(let entry of entries) { + if(entry.intersectionRatio > 0) { + entry.target["became visible"]() + this.visibilityObserver.unobserve(entry.target) + } + } + }, + {} + ) } onReadyStateChange() { @@ -24,6 +36,15 @@ export class AnimeNotifier { this.app.run() } + onContentLoaded() { + this.visibilityObserver.disconnect() + + // Update each of these asynchronously + Promise.resolve().then(() => this.updateMountables()) + Promise.resolve().then(() => this.updateActions()) + Promise.resolve().then(() => this.lazyLoadImages()) + } + reloadContent() { return fetch("/_" + this.app.currentPath, { credentials: "same-origin" @@ -57,9 +78,24 @@ export class AnimeNotifier { } } - updateAvatars() { + lazyLoadImages() { for(let element of findAll("user-image")) { - let img = element as HTMLImageElement + this.lazyLoadImage(element as HTMLImageElement) + } + + for(let element of findAll("anime-cover-image")) { + this.lazyLoadImage(element as HTMLImageElement) + } + } + + lazyLoadImage(img: HTMLImageElement) { + // Prevent browser from loading it instantly + img["original source"] = img.src + img.src = "" + + // Once the image becomes visible, load it + img["became visible"] = () => { + img.src = img["original source"] if(img.naturalWidth === 0) { img.onload = function() { @@ -73,6 +109,8 @@ export class AnimeNotifier { img.classList.add("user-image-found") } } + + this.visibilityObserver.observe(img) } updateMountables() { @@ -94,13 +132,6 @@ export class AnimeNotifier { } } - onContentLoaded() { - // Update each of these asynchronously - Promise.resolve().then(() => this.updateMountables()) - Promise.resolve().then(() => this.updateAvatars()) - Promise.resolve().then(() => this.updateActions()) - } - onPopState(e: PopStateEvent) { if(e.state) { this.app.load(e.state, { From 49a6d8b7fa2abeade12f135fa54cb46938447a5d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 16:28:16 +0200 Subject: [PATCH 27/36] Added minimal polyfill for IntersectionObserver --- scripts/AnimeNotifier.ts | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 6a4ad029..39f73a80 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -9,17 +9,30 @@ export class AnimeNotifier { constructor(app: Application) { this.app = app - this.visibilityObserver = new IntersectionObserver( - entries => { - for(let entry of entries) { - if(entry.intersectionRatio > 0) { - entry.target["became visible"]() - this.visibilityObserver.unobserve(entry.target) + + if("IntersectionObserver" in window) { + // Enable lazy load + this.visibilityObserver = new IntersectionObserver( + entries => { + for(let entry of entries) { + if(entry.intersectionRatio > 0) { + entry.target["became visible"]() + this.visibilityObserver.unobserve(entry.target) + } } - } - }, - {} - ) + }, + {} + ) + } else { + // Disable lazy load feature + this.visibilityObserver = { + disconnect: () => {}, + observe: (elem: HTMLElement) => { + elem["became visible"]() + }, + unobserve: (elem: HTMLElement) => {} + } as IntersectionObserver + } } onReadyStateChange() { From 4dfb14904a5665becc587fdc8027528f0b24b2bd Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 16:31:54 +0200 Subject: [PATCH 28/36] Disable certain settings --- pages/settings/settings.pixy | 10 +++++----- scripts/AnimeNotifier.ts | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 3fb7b1aa..8db80c9b 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -20,9 +20,9 @@ component Settings(user *arn.User) InputText("Accounts.AnimePlanet.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") - .widget.mountable(data-api="/api/settings/" + user.ID) - h3.widget-title - Icon("cogs") - span Settings + //- .widget.mountable(data-api="/api/settings/" + user.ID) + //- h3.widget-title + //- Icon("cogs") + //- span Settings - InputText("TitleLanguage", user.Settings().TitleLanguage, "Title language", "Language of anime titles") \ No newline at end of file + //- InputText("TitleLanguage", user.Settings().TitleLanguage, "Title language", "Language of anime titles") \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 39f73a80..2e647650 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -158,8 +158,8 @@ export class AnimeNotifier { } onKeyDown(e: KeyboardEvent) { - // Ctrl + Q = Search - if(e.ctrlKey && e.keyCode == 81) { + // F = Search + if(e.keyCode == 70) { let search = this.app.find("search") as HTMLInputElement search.focus() From 873be1849fb63fe8ef13015c934a63ee93170bf6 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 16:41:22 +0200 Subject: [PATCH 29/36] Changed search hotkey to F --- mixins/Navigation.pixy | 2 +- scripts/AnimeNotifier.ts | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mixins/Navigation.pixy b/mixins/Navigation.pixy index ab394798..0a6a55df 100644 --- a/mixins/Navigation.pixy +++ b/mixins/Navigation.pixy @@ -39,7 +39,7 @@ component LoggedInMenu(user *arn.User) NavigationButtonNoAJAX("Logout", "/logout", "sign-out") component FuzzySearch - input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: Ctrl + Q") + input#search.action(data-action="search", data-trigger="input", type="text", placeholder="Search...", title="Shortcut: F") component NavigationButton(name string, target string, icon string) a.navigation-link.ajax(href=target, aria-label=name, title=name) diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 2e647650..e5678955 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -158,6 +158,13 @@ export class AnimeNotifier { } onKeyDown(e: KeyboardEvent) { + // Ignore hotkeys on input elements + switch(document.activeElement.tagName) { + case "INPUT": + case "TEXTAREA": + return + } + // F = Search if(e.keyCode == 70) { let search = this.app.find("search") as HTMLInputElement From c6694aef8c2cd4746efff5d18cfa76962ead8aea Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 17:38:10 +0200 Subject: [PATCH 30/36] Improved image lazy loader --- mixins/AnimeGrid.pixy | 2 +- mixins/Avatar.pixy | 2 +- pages/profile/profile.pixy | 2 +- scripts/AnimeNotifier.ts | 21 ++++++++------------- styles/grid.scarlet | 1 + styles/images.scarlet | 11 +++++++++++ styles/user.scarlet | 9 +-------- 7 files changed, 24 insertions(+), 24 deletions(-) create mode 100644 styles/images.scarlet diff --git a/mixins/AnimeGrid.pixy b/mixins/AnimeGrid.pixy index 1581023c..43537328 100644 --- a/mixins/AnimeGrid.pixy +++ b/mixins/AnimeGrid.pixy @@ -2,4 +2,4 @@ component AnimeGrid(animeList []*arn.Anime) .anime-grid each anime in animeList a.anime-grid-cell.ajax(href="/anime/" + toString(anime.ID)) - img.anime-grid-image(src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")") \ No newline at end of file + img.anime-grid-image.lazy(data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")") \ No newline at end of file diff --git a/mixins/Avatar.pixy b/mixins/Avatar.pixy index c95f96f4..b3f9a103 100644 --- a/mixins/Avatar.pixy +++ b/mixins/Avatar.pixy @@ -4,7 +4,7 @@ component Avatar(user *arn.User) component AvatarNoLink(user *arn.User) if user.HasAvatar() - img.user-image(src=user.SmallAvatar(), alt=user.Nick) + img.user-image.lazy(data-src=user.SmallAvatar(), alt=user.Nick) else SVGAvatar diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 2e9d344e..36c12226 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -57,7 +57,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, else each item in animeList.Items a.profile-watching-list-item.ajax(href=item.Anime().Link(), title=item.Anime().Title.Canonical + " (" + toString(item.Episodes) + " / " + arn.EpisodesToString(item.Anime().EpisodeCount) + ")") - img.anime-cover-image.profile-watching-list-item-image(src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) + img.anime-cover-image.profile-watching-list-item-image.lazy(data-src=item.Anime().Image.Tiny, alt=item.Anime().Title.Canonical) .profile-category.mountable h3 diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index e5678955..d705e594 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -87,39 +87,34 @@ export class AnimeNotifier { actions[actionName](this, element, e) }) + // Use "action assigned" flag instead of removing the class. + // This will make sure that DOM diffs which restore the class name + // will not assign the action multiple times to the same element. element["action assigned"] = true } } lazyLoadImages() { - for(let element of findAll("user-image")) { - this.lazyLoadImage(element as HTMLImageElement) - } - - for(let element of findAll("anime-cover-image")) { + for(let element of findAll("lazy")) { this.lazyLoadImage(element as HTMLImageElement) } } lazyLoadImage(img: HTMLImageElement) { - // Prevent browser from loading it instantly - img["original source"] = img.src - img.src = "" - // Once the image becomes visible, load it img["became visible"] = () => { - img.src = img["original source"] + img.src = img.dataset.src if(img.naturalWidth === 0) { img.onload = function() { - this.classList.add("user-image-found") + this.classList.add("image-found") } img.onerror = function() { - this.classList.add("user-image-not-found") + this.classList.add("image-not-found") } } else { - img.classList.add("user-image-found") + img.classList.add("image-found") } } diff --git a/styles/grid.scarlet b/styles/grid.scarlet index 6d304d72..caa8ae2b 100644 --- a/styles/grid.scarlet +++ b/styles/grid.scarlet @@ -32,6 +32,7 @@ mixin grid-image height 100% border-radius 3px object-fit cover + default-transition mixin grid-icon font-size 2.5rem diff --git a/styles/images.scarlet b/styles/images.scarlet new file mode 100644 index 00000000..bf3ea806 --- /dev/null +++ b/styles/images.scarlet @@ -0,0 +1,11 @@ +.lazy + visibility hidden + opacity 0 + +.image-found + visibility visible + opacity 1 !important + +.image-not-found + visibility hidden + opacity 0 !important \ No newline at end of file diff --git a/styles/user.scarlet b/styles/user.scarlet index 21a16f54..5a39867f 100644 --- a/styles/user.scarlet +++ b/styles/user.scarlet @@ -4,13 +4,6 @@ height avatar-size border-radius 100% object-fit cover - opacity 0 default-transition :hover - box-shadow outline-shadow-heavy - -.user-image-found - opacity 1 !important - -.user-image-not-found - opacity 0 !important \ No newline at end of file + box-shadow outline-shadow-heavy \ No newline at end of file From ee9930cea3c02ed51fcd928a543473739d848934 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 18:15:39 +0200 Subject: [PATCH 31/36] Restored old footer --- pages/anime/anime.pixy | 5 +++-- pages/anime/anime.scarlet | 1 - pages/profile/profile.pixy | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pages/anime/anime.pixy b/pages/anime/anime.pixy index ad838187..2b770f46 100644 --- a/pages/anime/anime.pixy +++ b/pages/anime/anime.pixy @@ -160,8 +160,9 @@ component Anime(anime *arn.Anime, user *arn.User) //- a.light-button(href="http://www.anime-planet.com/anime/" + providers.AnimePlanet.providerId, target="_blank") AnimePlanet .footer - a(href="/api/anime/" + anime.ID) Anime API - span | + //- if user != nil && user.Role == "admin" + //- a(href="/api/anime/" + anime.ID) Anime API + //- span | span Powered by Kitsu. //- if descriptionSource //- span= " Summary by " + summarySource + "." diff --git a/pages/anime/anime.scarlet b/pages/anime/anime.scarlet index d3de1f37..b1eb03fb 100644 --- a/pages/anime/anime.scarlet +++ b/pages/anime/anime.scarlet @@ -91,7 +91,6 @@ font-size 0.8rem opacity 0.7 margin-top 0.5rem - text-align center .relations horizontal-wrap diff --git a/pages/profile/profile.pixy b/pages/profile/profile.pixy index 36c12226..a7abc4fe 100644 --- a/pages/profile/profile.pixy +++ b/pages/profile/profile.pixy @@ -84,6 +84,6 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList, .spacer .post-likes= len(post.Likes) - - .footer - a(href="/api/user/" + viewUser.ID) User API \ No newline at end of file + //- if user != nil && user.Role == "admin" + //- .footer + //- a(href="/api/user/" + viewUser.ID) User API \ No newline at end of file From 932ed7c16507da116af443c02ab9536d38541d47 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 19:52:10 +0200 Subject: [PATCH 32/36] Minor changes --- mixins/Input.pixy | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mixins/Input.pixy b/mixins/Input.pixy index e18d2a3f..b5aa3884 100644 --- a/mixins/Input.pixy +++ b/mixins/Input.pixy @@ -11,4 +11,9 @@ component InputTextArea(id string, value string, label string, placeholder strin component InputNumber(id string, value int, label string, placeholder string, min string, max string) .widget-input label(for=id)= label + ":" - input.widget-element.action(id=id, type="number", value=value, min=min, max=max, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") \ No newline at end of file + input.widget-element.action(id=id, type="number", value=value, min=min, max=max, placeholder=placeholder, title=placeholder, data-action="save", data-trigger="change") + +component InputSelection(id string, value string, label string, placeholder string) + .widget-input + label(for=id)= label + ":" + select.widget-element.action(id=id, value=value, title=placeholder, data-action="save", data-trigger="change") From 7e924097585fc30b4b92463779a783de89c3d1a1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 21:07:45 +0200 Subject: [PATCH 33/36] Basic browser extension support --- config.json | 3 ++- layout/layout.go | 2 +- layout/layout.pixy | 4 ++-- main.go | 2 ++ pages/animelist/animelist.go | 2 +- pages/embed/embed.go | 31 +++++++++++++++++++++++++++++++ pages/settings/settings.go | 2 +- styles/extension.scarlet | 10 ++++++++++ utils/allowembed.go | 10 ++++++++++ utils/container.go | 15 +++++++++++++++ 10 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 pages/embed/embed.go create mode 100644 styles/extension.scarlet create mode 100644 utils/allowembed.go create mode 100644 utils/container.go diff --git a/config.json b/config.json index bebbdef8..c4244c39 100644 --- a/config.json +++ b/config.json @@ -21,7 +21,8 @@ "video", "loading", "fade", - "mobile" + "mobile", + "extension" ], "scripts": { "main": "main" diff --git a/layout/layout.go b/layout/layout.go index 9e617c67..b963046f 100644 --- a/layout/layout.go +++ b/layout/layout.go @@ -9,5 +9,5 @@ import ( // Render layout. func Render(ctx *aero.Context, content string) string { user := utils.GetUser(ctx) - return components.Layout(ctx.App, user, content) + return components.Layout(ctx.App, ctx, user, content) } diff --git a/layout/layout.pixy b/layout/layout.pixy index 00629b93..86007cdb 100644 --- a/layout/layout.pixy +++ b/layout/layout.pixy @@ -1,4 +1,4 @@ -component Layout(app *aero.Application, user *arn.User, content string) +component Layout(app *aero.Application, ctx *aero.Context, user *arn.User, content string) html(lang="en") head title= app.Config.Title @@ -6,7 +6,7 @@ component Layout(app *aero.Application, user *arn.User, content string) meta(name="theme-color", content=app.Config.Manifest.ThemeColor) link(rel="manifest", href="/manifest.json") body - #container + #container(class=utils.GetContainerClass(ctx)) #header Navigation(user) #content-container diff --git a/main.go b/main.go index edc47ead..16c31cf2 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( "github.com/animenotifier/notify.moe/pages/animelist" "github.com/animenotifier/notify.moe/pages/animelistitem" "github.com/animenotifier/notify.moe/pages/dashboard" + "github.com/animenotifier/notify.moe/pages/embed" "github.com/animenotifier/notify.moe/pages/forum" "github.com/animenotifier/notify.moe/pages/forums" "github.com/animenotifier/notify.moe/pages/login" @@ -70,6 +71,7 @@ func configure(app *aero.Application) *aero.Application { app.Ajax("/login", login.Get) app.Ajax("/airing", airing.Get) app.Ajax("/webdev", webdev.Get) + app.Ajax("/extension/embed", embed.Get) // app.Ajax("/genres", genres.Get) // app.Ajax("/genres/:name", genre.Get) diff --git a/pages/animelist/animelist.go b/pages/animelist/animelist.go index 0982543f..d06f7227 100644 --- a/pages/animelist/animelist.go +++ b/pages/animelist/animelist.go @@ -21,7 +21,7 @@ func Get(ctx *aero.Context) string { animeList := viewUser.AnimeList() if animeList == nil { - return ctx.Error(http.StatusNotFound, "Anime list not found", err) + return ctx.Error(http.StatusNotFound, "Anime list not found", nil) } sort.Slice(animeList.Items, func(i, j int) bool { diff --git a/pages/embed/embed.go b/pages/embed/embed.go new file mode 100644 index 00000000..8c486ce2 --- /dev/null +++ b/pages/embed/embed.go @@ -0,0 +1,31 @@ +package embed + +import ( + "net/http" + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/components" + "github.com/animenotifier/notify.moe/utils" +) + +// Get anime list in the browser extension. +func Get(ctx *aero.Context) string { + user := utils.GetUser(ctx) + + if user == nil { + return ctx.Error(http.StatusUnauthorized, "Not logged in", nil) + } + + animeList := user.AnimeList() + + if animeList == nil { + return ctx.Error(http.StatusNotFound, "Anime list not found", nil) + } + + sort.Slice(animeList.Items, func(i, j int) bool { + return animeList.Items[i].FinalRating() < animeList.Items[j].FinalRating() + }) + + return utils.AllowEmbed(ctx, ctx.HTML(components.AnimeList(animeList))) +} diff --git a/pages/settings/settings.go b/pages/settings/settings.go index 978866bb..761cc9a5 100644 --- a/pages/settings/settings.go +++ b/pages/settings/settings.go @@ -16,5 +16,5 @@ func Get(ctx *aero.Context) string { return ctx.Error(http.StatusForbidden, "Not logged in", nil) } - return ctx.HTML(components.Settings(user)) + return utils.AllowEmbed(ctx, ctx.HTML(components.Settings(user))) } diff --git a/styles/extension.scarlet b/styles/extension.scarlet new file mode 100644 index 00000000..d22d4624 --- /dev/null +++ b/styles/extension.scarlet @@ -0,0 +1,10 @@ +.embedded + .anime-list + th + display none + + #content + // font-size 0.9em + + #header + display none \ No newline at end of file diff --git a/utils/allowembed.go b/utils/allowembed.go new file mode 100644 index 00000000..16761b8c --- /dev/null +++ b/utils/allowembed.go @@ -0,0 +1,10 @@ +package utils + +import "github.com/aerogo/aero" + +// AllowEmbed allows the page to be called by the browser extension. +func AllowEmbed(ctx *aero.Context, response string) string { + // This is a bit of a hack. + ctx.SetResponseHeader("X-Frame-Options", "ALLOW-FROM chrome-extension://hjfcooigdelogjmniiahfiilcefdlpha/options.html") + return response +} diff --git a/utils/container.go b/utils/container.go new file mode 100644 index 00000000..33343873 --- /dev/null +++ b/utils/container.go @@ -0,0 +1,15 @@ +package utils + +import ( + "github.com/aerogo/aero" +) + +// GetContainerClass returns the class for the "container" element. +// In the browser extension it will get the "embedded" class. +func GetContainerClass(ctx *aero.Context) string { + if ctx.URI() == "/extension/embed" { + return "embedded" + } + + return "" +} From a0843125e6c1932a70471a4a0aa737a7eda6100d Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 23:03:03 +0200 Subject: [PATCH 34/36] Improved embedded style --- pages/animelist/animelist.scarlet | 3 +++ styles/extension.scarlet | 28 ++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/pages/animelist/animelist.scarlet b/pages/animelist/animelist.scarlet index c67e96dc..e943c080 100644 --- a/pages/animelist/animelist.scarlet +++ b/pages/animelist/animelist.scarlet @@ -7,6 +7,9 @@ .anime-list-item-name flex 0.8 + white-space nowrap + text-overflow ellipsis + overflow hidden .anime-list-item-episodes flex 0.1 diff --git a/styles/extension.scarlet b/styles/extension.scarlet index d22d4624..1e4f5801 100644 --- a/styles/extension.scarlet +++ b/styles/extension.scarlet @@ -1,10 +1,26 @@ .embedded + flex-direction column-reverse !important + .anime-list - th + max-width 500px + margin -1.1rem + + thead + display none + + .anime-list-item + ui-element + margin-bottom 0.5rem + + .anime-list-item-name + flex 0.9 + + .anime-list-item-episodes + text-align right + white-space nowrap + + .anime-list-item-rating display none - #content - // font-size 0.9em - - #header - display none \ No newline at end of file + #navigation + font-size 0.8rem \ No newline at end of file From 6abe9814603863aa643808df0baa4dea5dafad54 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 23:05:07 +0200 Subject: [PATCH 35/36] Kitsu username fix --- pages/settings/settings.pixy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pages/settings/settings.pixy b/pages/settings/settings.pixy index 8db80c9b..d00e2e1c 100644 --- a/pages/settings/settings.pixy +++ b/pages/settings/settings.pixy @@ -17,7 +17,7 @@ component Settings(user *arn.User) InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co") InputText("Accounts.MyAnimeList.Nick", user.Accounts.MyAnimeList.Nick, "MyAnimeList", "Your username on myanimelist.net") - InputText("Accounts.AnimePlanet.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") + InputText("Accounts.Kitsu.Nick", user.Accounts.Kitsu.Nick, "Kitsu", "Your username on kitsu.io") InputText("Accounts.AnimePlanet.Nick", user.Accounts.AnimePlanet.Nick, "AnimePlanet", "Your username on anime-planet.com") //- .widget.mountable(data-api="/api/settings/" + user.ID) From 2b1fc3239c5c144a8af2e6485a5b458696d2da50 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Sat, 24 Jun 2017 23:24:15 +0200 Subject: [PATCH 36/36] Improved extension style --- auth/google.go | 4 ++-- pages/animelist/animelist.pixy | 5 ++++- styles/extension.scarlet | 22 ++++++++++++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/auth/google.go b/auth/google.go index ee90f0f7..d23048b5 100644 --- a/auth/google.go +++ b/auth/google.go @@ -111,7 +111,7 @@ func InstallGoogleAuth(app *aero.Application) { user, getErr = arn.GetUserFromTable("GoogleToUser", googleUser.Sub) if getErr == nil && user != nil { - authLog.Info("User logged in via Google ID", user.ID, ctx.RealIP(), user.Email, user.RealName()) + authLog.Info("User logged in via Google ID", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) user.LastLogin = arn.DateTimeUTC() user.Save() @@ -124,7 +124,7 @@ func InstallGoogleAuth(app *aero.Application) { user, getErr = arn.GetUserByEmail(googleUser.Email) if getErr == nil && user != nil { - authLog.Info("User logged in via Email", user.ID, ctx.RealIP(), user.Email, user.RealName()) + authLog.Info("User logged in via Email", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName()) user.LastLogin = arn.DateTimeUTC() user.Save() diff --git a/pages/animelist/animelist.pixy b/pages/animelist/animelist.pixy index da17b5c8..89092062 100644 --- a/pages/animelist/animelist.pixy +++ b/pages/animelist/animelist.pixy @@ -10,5 +10,8 @@ component AnimeList(animeList *arn.AnimeList) tr.anime-list-item.mountable(title=item.Notes) td.anime-list-item-name a.ajax(href=item.Anime().Link())= item.Anime().Title.Canonical - td.anime-list-item-episodes= toString(item.Episodes) + " / " + item.Anime().EpisodeCountString() + td.anime-list-item-episodes + span.anime-list-item-episodes-watched= item.Episodes + span.anime-list-item-episodes-separator / + span.anime-list-item-episodes-max= item.Anime().EpisodeCountString() td.anime-list-item-rating= item.FinalRating() \ No newline at end of file diff --git a/styles/extension.scarlet b/styles/extension.scarlet index 1e4f5801..50eb9d4e 100644 --- a/styles/extension.scarlet +++ b/styles/extension.scarlet @@ -9,18 +9,28 @@ display none .anime-list-item - ui-element - margin-bottom 0.5rem - - .anime-list-item-name - flex 0.9 + // ui-element + // margin-bottom 0.5rem .anime-list-item-episodes + horizontal text-align right white-space nowrap + flex 0.2 + + .anime-list-item-episodes-watched + flex 0.4 + + .anime-list-item-episodes-max + opacity 0.5 + flex 0.4 + + .anime-list-item-episodes-separator + opacity 0.5 + flex 0.2 .anime-list-item-rating display none #navigation - font-size 0.8rem \ No newline at end of file + font-size 0.9rem \ No newline at end of file