commit
facd70e094
@ -19,6 +19,11 @@ func init() {
|
|||||||
return js
|
return js
|
||||||
})
|
})
|
||||||
|
|
||||||
|
app.Get("/scripts.js", func(ctx *aero.Context) string {
|
||||||
|
ctx.SetResponseHeader("Content-Type", "application/javascript")
|
||||||
|
return js
|
||||||
|
})
|
||||||
|
|
||||||
// Web manifest
|
// Web manifest
|
||||||
app.Get("/manifest.json", func(ctx *aero.Context) string {
|
app.Get("/manifest.json", func(ctx *aero.Context) string {
|
||||||
return ctx.JSON(app.Config.Manifest)
|
return ctx.JSON(app.Config.Manifest)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import "github.com/aerogo/aero"
|
import "github.com/aerogo/aero"
|
||||||
|
import "github.com/animenotifier/notify.moe/utils"
|
||||||
|
|
||||||
// Install ...
|
// Install ...
|
||||||
func Install(app *aero.Application) {
|
func Install(app *aero.Application) {
|
||||||
@ -10,6 +11,12 @@ func Install(app *aero.Application) {
|
|||||||
// Logout
|
// Logout
|
||||||
app.Get("/logout", func(ctx *aero.Context) string {
|
app.Get("/logout", func(ctx *aero.Context) string {
|
||||||
if ctx.HasSession() {
|
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)
|
ctx.Session().Set("userId", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/aero"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
|
"github.com/animenotifier/notify.moe/utils"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
"golang.org/x/oauth2/google"
|
"golang.org/x/oauth2/google"
|
||||||
)
|
)
|
||||||
@ -88,22 +89,86 @@ func InstallGoogleAuth(app *aero.Application) {
|
|||||||
return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err)
|
return ctx.Error(http.StatusBadRequest, "Failed parsing user data (JSON)", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to find an existing user by the Google user ID
|
// Is this an existing user connecting another social account?
|
||||||
user, getErr := arn.GetUserFromTable("GoogleToUser", googleUser.Sub)
|
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 {
|
if getErr == nil && user != nil {
|
||||||
|
authLog.Info("User logged in via Google ID", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
|
||||||
|
|
||||||
|
user.LastLogin = arn.DateTimeUTC()
|
||||||
|
user.Save()
|
||||||
|
|
||||||
session.Set("userId", user.ID)
|
session.Set("userId", user.ID)
|
||||||
return ctx.Redirect("/")
|
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)
|
user, getErr = arn.GetUserByEmail(googleUser.Email)
|
||||||
|
|
||||||
if getErr == nil && user != nil {
|
if getErr == nil && user != nil {
|
||||||
|
authLog.Info("User logged in via Email", user.ID, user.Nick, ctx.RealIP(), user.Email, user.RealName())
|
||||||
|
|
||||||
|
user.LastLogin = arn.DateTimeUTC()
|
||||||
|
user.Save()
|
||||||
|
|
||||||
session.Set("userId", user.ID)
|
session.Set("userId", user.ID)
|
||||||
return ctx.Redirect("/")
|
return ctx.Redirect("/")
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.Error(http.StatusForbidden, "Account does not exist", getErr)
|
// 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.LastLogin = arn.DateTimeUTC()
|
||||||
|
|
||||||
|
// Save basic user info already to avoid data inconsistency problems
|
||||||
|
user.Save()
|
||||||
|
|
||||||
|
// 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, user.Nick, ctx.RealIP(), user.Email, user.RealName())
|
||||||
|
|
||||||
|
// Redirect to frontpage
|
||||||
|
return ctx.Redirect("/")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
14
auth/log.go
Normal file
14
auth/log.go
Normal file
@ -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"))
|
||||||
|
}
|
@ -14,17 +14,15 @@
|
|||||||
"layout",
|
"layout",
|
||||||
"navigation",
|
"navigation",
|
||||||
"headers",
|
"headers",
|
||||||
"forms",
|
"input",
|
||||||
"grid",
|
"grid",
|
||||||
"colors",
|
|
||||||
"animelist",
|
|
||||||
"forum",
|
"forum",
|
||||||
"settings",
|
|
||||||
"user",
|
"user",
|
||||||
"video",
|
"video",
|
||||||
"loading",
|
"loading",
|
||||||
"fade",
|
"fade",
|
||||||
"mobile"
|
"mobile",
|
||||||
|
"extension"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"main": "main"
|
"main": "main"
|
||||||
|
@ -24,7 +24,15 @@ func main() {
|
|||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
sort.Slice(users, func(i, j int) bool {
|
sort.Slice(users, func(i, j int) bool {
|
||||||
return users[i].Registered < users[j].Registered
|
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
|
// Add users to list
|
||||||
|
@ -7,7 +7,6 @@ import (
|
|||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
const maxPopularAnime = 10
|
const maxPopularAnime = 10
|
||||||
|
|
||||||
// Note this is using the airing-anime as a template with modfications
|
// Note this is using the airing-anime as a template with modfications
|
||||||
|
@ -4,7 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/flow"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
"github.com/fatih/color"
|
"github.com/fatih/color"
|
||||||
)
|
)
|
||||||
@ -12,7 +12,7 @@ import (
|
|||||||
func main() {
|
func main() {
|
||||||
color.Yellow("Updating search index")
|
color.Yellow("Updating search index")
|
||||||
|
|
||||||
aero.Parallel(updateAnimeIndex, updateUserIndex)
|
flow.Parallel(updateAnimeIndex, updateUserIndex)
|
||||||
|
|
||||||
color.Green("Finished.")
|
color.Green("Finished.")
|
||||||
}
|
}
|
||||||
|
@ -9,5 +9,5 @@ import (
|
|||||||
// Render layout.
|
// Render layout.
|
||||||
func Render(ctx *aero.Context, content string) string {
|
func Render(ctx *aero.Context, content string) string {
|
||||||
user := utils.GetUser(ctx)
|
user := utils.GetUser(ctx)
|
||||||
return components.Layout(ctx.App, user, content)
|
return components.Layout(ctx.App, ctx, user, content)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
html(lang="en")
|
||||||
head
|
head
|
||||||
title= app.Config.Title
|
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)
|
meta(name="theme-color", content=app.Config.Manifest.ThemeColor)
|
||||||
link(rel="manifest", href="/manifest.json")
|
link(rel="manifest", href="/manifest.json")
|
||||||
body
|
body
|
||||||
#container
|
#container(class=utils.GetContainerClass(ctx))
|
||||||
#header
|
#header
|
||||||
Navigation(user)
|
Navigation(user)
|
||||||
#content-container
|
#content-container
|
||||||
|
6
main.go
6
main.go
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/animenotifier/notify.moe/pages/animelist"
|
"github.com/animenotifier/notify.moe/pages/animelist"
|
||||||
"github.com/animenotifier/notify.moe/pages/animelistitem"
|
"github.com/animenotifier/notify.moe/pages/animelistitem"
|
||||||
"github.com/animenotifier/notify.moe/pages/dashboard"
|
"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/forum"
|
||||||
"github.com/animenotifier/notify.moe/pages/forums"
|
"github.com/animenotifier/notify.moe/pages/forums"
|
||||||
"github.com/animenotifier/notify.moe/pages/login"
|
"github.com/animenotifier/notify.moe/pages/login"
|
||||||
@ -26,7 +27,6 @@ import (
|
|||||||
"github.com/animenotifier/notify.moe/pages/user"
|
"github.com/animenotifier/notify.moe/pages/user"
|
||||||
"github.com/animenotifier/notify.moe/pages/users"
|
"github.com/animenotifier/notify.moe/pages/users"
|
||||||
"github.com/animenotifier/notify.moe/pages/webdev"
|
"github.com/animenotifier/notify.moe/pages/webdev"
|
||||||
"github.com/animenotifier/notify.moe/utils"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var app = aero.New()
|
var app = aero.New()
|
||||||
@ -71,19 +71,21 @@ func configure(app *aero.Application) *aero.Application {
|
|||||||
app.Ajax("/login", login.Get)
|
app.Ajax("/login", login.Get)
|
||||||
app.Ajax("/airing", airing.Get)
|
app.Ajax("/airing", airing.Get)
|
||||||
app.Ajax("/webdev", webdev.Get)
|
app.Ajax("/webdev", webdev.Get)
|
||||||
|
app.Ajax("/extension/embed", embed.Get)
|
||||||
// app.Ajax("/genres", genres.Get)
|
// app.Ajax("/genres", genres.Get)
|
||||||
// app.Ajax("/genres/:name", genre.Get)
|
// app.Ajax("/genres/:name", genre.Get)
|
||||||
|
|
||||||
// Middleware
|
// Middleware
|
||||||
app.Use(middleware.Log())
|
app.Use(middleware.Log())
|
||||||
app.Use(middleware.Session())
|
app.Use(middleware.Session())
|
||||||
|
app.Use(middleware.UserInfo())
|
||||||
|
|
||||||
// API
|
// API
|
||||||
api := api.New("/api/", arn.DB)
|
api := api.New("/api/", arn.DB)
|
||||||
api.Install(app)
|
api.Install(app)
|
||||||
|
|
||||||
// Domain
|
// Domain
|
||||||
if utils.IsDevelopment() {
|
if arn.IsDevelopment() {
|
||||||
app.Config.Domain = "beta.notify.moe"
|
app.Config.Domain = "beta.notify.moe"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
43
middleware/IPToHost.go
Normal file
43
middleware/IPToHost.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cache "github.com/patrickmn/go-cache"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ipToHosts = cache.New(60*time.Minute, 30*time.Minute)
|
||||||
|
|
||||||
|
// GetHostsForIP returns all host names for the given IP (if cached).
|
||||||
|
func GetHostsForIP(ip string) []string {
|
||||||
|
hosts, found := ipToHosts.Get(ip)
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
hosts = findHostsForIP(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hosts == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts.([]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finds all host names for the given IP
|
||||||
|
func findHostsForIP(ip string) []string {
|
||||||
|
hosts, err := net.LookupAddr(ip)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(hosts) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache host names
|
||||||
|
ipToHosts.Set(ip, hosts, cache.DefaultExpiration)
|
||||||
|
|
||||||
|
return hosts
|
||||||
|
}
|
@ -9,26 +9,53 @@ import (
|
|||||||
|
|
||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/aero"
|
||||||
"github.com/aerogo/log"
|
"github.com/aerogo/log"
|
||||||
|
"github.com/animenotifier/notify.moe/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var request = log.New()
|
||||||
|
var err = log.New()
|
||||||
|
|
||||||
|
// Initialize log files
|
||||||
|
func init() {
|
||||||
|
request.AddOutput(log.File("logs/request.log"))
|
||||||
|
|
||||||
|
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.
|
// Log middleware logs every request into logs/request.log and errors into logs/error.log.
|
||||||
func Log() aero.Middleware {
|
func Log() aero.Middleware {
|
||||||
request := log.New()
|
|
||||||
request.AddOutput(log.File("logs/request.log"))
|
|
||||||
|
|
||||||
err := log.New()
|
|
||||||
err.AddOutput(log.File("logs/error.log"))
|
|
||||||
err.AddOutput(os.Stderr)
|
|
||||||
|
|
||||||
return func(ctx *aero.Context, next func()) {
|
return func(ctx *aero.Context, next func()) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
next()
|
next()
|
||||||
responseTime := time.Since(start)
|
responseTime := time.Since(start)
|
||||||
|
|
||||||
|
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 := strconv.Itoa(int(responseTime.Nanoseconds()/1000000)) + " ms"
|
||||||
responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString
|
responseTimeString = strings.Repeat(" ", 8-len(responseTimeString)) + responseTimeString
|
||||||
|
|
||||||
|
user := utils.GetUser(ctx)
|
||||||
|
ip := ctx.RealIP()
|
||||||
|
|
||||||
|
hostName := "<unknown host>"
|
||||||
|
hostNames := GetHostsForIP(ip)
|
||||||
|
|
||||||
|
if len(hostNames) != 0 {
|
||||||
|
hostName = hostNames[0]
|
||||||
|
hostName = strings.TrimSuffix(hostName, ".")
|
||||||
|
}
|
||||||
|
|
||||||
// Log every request
|
// Log every request
|
||||||
request.Info(ctx.RealIP(), ctx.StatusCode, responseTimeString, ctx.URI())
|
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
|
// Log all requests that failed
|
||||||
switch ctx.StatusCode {
|
switch ctx.StatusCode {
|
||||||
@ -36,13 +63,12 @@ func Log() aero.Middleware {
|
|||||||
// Ok.
|
// Ok.
|
||||||
|
|
||||||
default:
|
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.
|
// Notify us about long requests.
|
||||||
// However ignore requests under /auth/ because those depend on 3rd party servers.
|
// However ignore requests under /auth/ because those depend on 3rd party servers.
|
||||||
if responseTime >= 200*time.Millisecond && !strings.HasPrefix(ctx.URI(), "/auth/") {
|
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())
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
114
middleware/UserInfo.go
Normal file
114
middleware/UserInfo.go
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"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()
|
||||||
|
|
||||||
|
// Ignore non-HTML requests
|
||||||
|
if ctx.IsMediaResponse() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore API requests
|
||||||
|
if strings.HasPrefix(ctx.URI(), "/api/") {
|
||||||
|
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, user)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update browser and OS data
|
||||||
|
func updateUserInfo(ctx *aero.Context, user *arn.User) {
|
||||||
|
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()
|
||||||
|
|
||||||
|
// OS
|
||||||
|
os := parsed.OSInfo()
|
||||||
|
user.OS.Name = os.Name
|
||||||
|
user.OS.Version = os.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.IP != newIP {
|
||||||
|
updateUserLocation(user, newIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
user.LastSeen = arn.DateTimeUTC()
|
||||||
|
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"
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
@ -2,4 +2,4 @@ component AnimeGrid(animeList []*arn.Anime)
|
|||||||
.anime-grid
|
.anime-grid
|
||||||
each anime in animeList
|
each anime in animeList
|
||||||
a.anime-grid-cell.ajax(href="/anime/" + toString(anime.ID))
|
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) + ")")
|
img.anime-grid-image.lazy(data-src=anime.Image.Small, alt=anime.Title.Romaji, title=anime.Title.Romaji + " (" + toString(anime.Rating.Overall) + ")")
|
@ -4,7 +4,7 @@ component Avatar(user *arn.User)
|
|||||||
|
|
||||||
component AvatarNoLink(user *arn.User)
|
component AvatarNoLink(user *arn.User)
|
||||||
if user.HasAvatar()
|
if user.HasAvatar()
|
||||||
img.user-image(src=user.SmallAvatar(), alt=user.Nick)
|
img.user-image.lazy(data-src=user.SmallAvatar(), alt=user.Nick)
|
||||||
else
|
else
|
||||||
SVGAvatar
|
SVGAvatar
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
component InputText(id string, value string, label string, placeholder string)
|
component InputText(id string, value string, label string, placeholder string)
|
||||||
.widget-input
|
.widget-input
|
||||||
label(for=id)= label + ":"
|
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)
|
component InputTextArea(id string, value string, label string, placeholder string)
|
||||||
.widget-input
|
.widget-input
|
||||||
label(for=id)= label + ":"
|
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)
|
component InputNumber(id string, value int, label string, placeholder string, min string, max string)
|
||||||
.widget-input
|
.widget-input
|
||||||
label(for=id)= label + ":"
|
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")
|
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")
|
||||||
|
@ -39,7 +39,7 @@ component LoggedInMenu(user *arn.User)
|
|||||||
NavigationButtonNoAJAX("Logout", "/logout", "sign-out")
|
NavigationButtonNoAJAX("Logout", "/logout", "sign-out")
|
||||||
|
|
||||||
component FuzzySearch
|
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)
|
component NavigationButton(name string, target string, icon string)
|
||||||
a.navigation-link.ajax(href=target, aria-label=name, title=name)
|
a.navigation-link.ajax(href=target, aria-label=name, title=name)
|
||||||
|
@ -159,8 +159,11 @@ component Anime(anime *arn.Anime, user *arn.User)
|
|||||||
//- if providers.AnimePlanet
|
//- if providers.AnimePlanet
|
||||||
//- a.light-button(href="http://www.anime-planet.com/anime/" + providers.AnimePlanet.providerId, target="_blank") AnimePlanet
|
//- a.light-button(href="http://www.anime-planet.com/anime/" + providers.AnimePlanet.providerId, target="_blank") AnimePlanet
|
||||||
|
|
||||||
.sources
|
.footer
|
||||||
p Powered by Kitsu.
|
//- if user != nil && user.Role == "admin"
|
||||||
|
//- a(href="/api/anime/" + anime.ID) Anime API
|
||||||
|
//- span |
|
||||||
|
span Powered by Kitsu.
|
||||||
//- if descriptionSource
|
//- if descriptionSource
|
||||||
//- span= " Summary by " + summarySource + "."
|
//- span= " Summary by " + summarySource + "."
|
||||||
//- //-
|
//- //-
|
||||||
|
@ -87,9 +87,9 @@
|
|||||||
.anime-rating-categories
|
.anime-rating-categories
|
||||||
vertical
|
vertical
|
||||||
|
|
||||||
.sources
|
.footer
|
||||||
font-size 0.8rem
|
font-size 0.8rem
|
||||||
opacity 0.5
|
opacity 0.7
|
||||||
margin-top 0.5rem
|
margin-top 0.5rem
|
||||||
|
|
||||||
.relations
|
.relations
|
||||||
|
@ -21,7 +21,7 @@ func Get(ctx *aero.Context) string {
|
|||||||
animeList := viewUser.AnimeList()
|
animeList := viewUser.AnimeList()
|
||||||
|
|
||||||
if animeList == nil {
|
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 {
|
sort.Slice(animeList.Items, func(i, j int) bool {
|
||||||
|
@ -10,5 +10,8 @@ component AnimeList(animeList *arn.AnimeList)
|
|||||||
tr.anime-list-item.mountable(title=item.Notes)
|
tr.anime-list-item.mountable(title=item.Notes)
|
||||||
td.anime-list-item-name
|
td.anime-list-item-name
|
||||||
a.ajax(href=item.Anime().Link())= item.Anime().Title.Canonical
|
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()
|
td.anime-list-item-rating= item.FinalRating()
|
@ -7,6 +7,9 @@
|
|||||||
|
|
||||||
.anime-list-item-name
|
.anime-list-item-name
|
||||||
flex 0.8
|
flex 0.8
|
||||||
|
white-space nowrap
|
||||||
|
text-overflow ellipsis
|
||||||
|
overflow hidden
|
||||||
|
|
||||||
.anime-list-item-episodes
|
.anime-list-item-episodes
|
||||||
flex 0.1
|
flex 0.1
|
||||||
|
31
pages/embed/embed.go
Normal file
31
pages/embed/embed.go
Normal file
@ -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)))
|
||||||
|
}
|
@ -11,5 +11,8 @@ component Forum(tag string, threads []*arn.Thread, threadsPerPage int)
|
|||||||
span Load more
|
span Load more
|
||||||
|
|
||||||
component ThreadList(threads []*arn.Thread)
|
component ThreadList(threads []*arn.Thread)
|
||||||
|
if len(threads) == 0
|
||||||
|
p No threads found.
|
||||||
|
else
|
||||||
each thread in threads
|
each thread in threads
|
||||||
ThreadLink(thread)
|
ThreadLink(thread)
|
16
pages/popularanime/old/popular.pixy
Normal file
16
pages/popularanime/old/popular.pixy
Normal file
@ -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)
|
19
pages/popularanime/old/popular.scarlet
Normal file
19
pages/popularanime/old/popular.scarlet
Normal file
@ -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
|
@ -1,6 +1,8 @@
|
|||||||
package popularanime
|
package popularanime
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/aero"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
"github.com/animenotifier/notify.moe/components"
|
"github.com/animenotifier/notify.moe/components"
|
||||||
@ -8,29 +10,10 @@ import (
|
|||||||
|
|
||||||
// Get search page.
|
// Get search page.
|
||||||
func Get(ctx *aero.Context) string {
|
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()
|
animeList, err := arn.GetPopularAnimeCached()
|
||||||
|
|
||||||
if err != nil {
|
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))
|
return ctx.HTML(components.AnimeGrid(animeList))
|
||||||
|
@ -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)
|
|
@ -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
|
|
@ -2,6 +2,7 @@ package profile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/aerogo/aero"
|
"github.com/aerogo/aero"
|
||||||
|
"github.com/aerogo/flow"
|
||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
"github.com/animenotifier/notify.moe/components"
|
"github.com/animenotifier/notify.moe/components"
|
||||||
"github.com/animenotifier/notify.moe/utils"
|
"github.com/animenotifier/notify.moe/utils"
|
||||||
@ -28,7 +29,7 @@ func Profile(ctx *aero.Context, viewUser *arn.User) string {
|
|||||||
var animeList *arn.AnimeList
|
var animeList *arn.AnimeList
|
||||||
var posts []*arn.Post
|
var posts []*arn.Post
|
||||||
|
|
||||||
aero.Parallel(func() {
|
flow.Parallel(func() {
|
||||||
user = utils.GetUser(ctx)
|
user = utils.GetUser(ctx)
|
||||||
}, func() {
|
}, func() {
|
||||||
animeList = viewUser.AnimeList()
|
animeList = viewUser.AnimeList()
|
||||||
|
@ -57,7 +57,7 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList,
|
|||||||
else
|
else
|
||||||
each item in animeList.Items
|
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) + ")")
|
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
|
.profile-category.mountable
|
||||||
h3
|
h3
|
||||||
@ -79,7 +79,11 @@ component Profile(viewUser *arn.User, user *arn.User, animeList *arn.AnimeList,
|
|||||||
.post-author
|
.post-author
|
||||||
Avatar(post.Author())
|
Avatar(post.Author())
|
||||||
.post-content
|
.post-content
|
||||||
.mountable!= aero.Markdown(post.Text)
|
div!= aero.Markdown(post.Text)
|
||||||
.post-toolbar.active
|
.post-toolbar.active
|
||||||
.spacer
|
.spacer
|
||||||
.post-likes= len(post.Likes)
|
.post-likes= len(post.Likes)
|
||||||
|
|
||||||
|
//- if user != nil && user.Role == "admin"
|
||||||
|
//- .footer
|
||||||
|
//- a(href="/api/user/" + viewUser.ID) User API
|
@ -7,3 +7,7 @@
|
|||||||
.profile-watching-list-item-image
|
.profile-watching-list-item-image
|
||||||
width 55px !important
|
width 55px !important
|
||||||
border-radius 2px
|
border-radius 2px
|
||||||
|
|
||||||
|
< 380px
|
||||||
|
.profile-watching-list
|
||||||
|
justify-content center
|
@ -14,5 +14,5 @@ func Get(ctx *aero.Context) string {
|
|||||||
term := ctx.Get("term")
|
term := ctx.Get("term")
|
||||||
|
|
||||||
userResults, animeResults := arn.Search(term, maxUsers, maxAnime)
|
userResults, animeResults := arn.Search(term, maxUsers, maxAnime)
|
||||||
return ctx.HTML(components.Search(userResults, animeResults))
|
return ctx.HTML(components.SearchResults(userResults, animeResults))
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
component Search(users []*arn.User, animeResults []*arn.Anime)
|
component SearchResults(users []*arn.User, animeResults []*arn.Anime)
|
||||||
.widgets
|
.widgets
|
||||||
.widget
|
.widget
|
||||||
h3 Users
|
h3 Users
|
||||||
|
@ -16,5 +16,5 @@ func Get(ctx *aero.Context) string {
|
|||||||
return ctx.Error(http.StatusForbidden, "Not logged in", nil)
|
return ctx.Error(http.StatusForbidden, "Not logged in", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.HTML(components.Settings(user))
|
return utils.AllowEmbed(ctx, ctx.HTML(components.Settings(user)))
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
component Settings(user *arn.User)
|
component Settings(user *arn.User)
|
||||||
h2.page-title Settings
|
h2.page-title Settings
|
||||||
.widgets(data-api="/api/user/" + user.ID)
|
.widgets
|
||||||
.widget.mountable
|
.widget.mountable(data-api="/api/user/" + user.ID)
|
||||||
h3.widget-title
|
h3.widget-title
|
||||||
Icon("user")
|
Icon("user")
|
||||||
span Personal
|
span Personal
|
||||||
@ -10,7 +10,7 @@ component Settings(user *arn.User)
|
|||||||
InputText("Tagline", user.Tagline, "Tagline", "Text that appears below your username")
|
InputText("Tagline", user.Tagline, "Tagline", "Text that appears below your username")
|
||||||
InputText("Website", user.Website, "Website", "Your homepage")
|
InputText("Website", user.Website, "Website", "Your homepage")
|
||||||
|
|
||||||
.widget.mountable
|
.widget.mountable(data-api="/api/user/" + user.ID)
|
||||||
h3.widget-title
|
h3.widget-title
|
||||||
Icon("cubes")
|
Icon("cubes")
|
||||||
span Accounts
|
span Accounts
|
||||||
@ -18,3 +18,11 @@ component Settings(user *arn.User)
|
|||||||
InputText("Accounts.AniList.Nick", user.Accounts.AniList.Nick, "AniList", "Your username on anilist.co")
|
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.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")
|
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)
|
||||||
|
//- h3.widget-title
|
||||||
|
//- Icon("cogs")
|
||||||
|
//- span Settings
|
||||||
|
|
||||||
|
//- InputText("TitleLanguage", user.Settings().TitleLanguage, "Title language", "Language of anime titles")
|
29
patches/add-last-seen/main.go
Normal file
29
patches/add-last-seen/main.go
Normal file
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -25,7 +25,7 @@ func main() {
|
|||||||
count++
|
count++
|
||||||
println(count, user.Nick)
|
println(count, user.Nick)
|
||||||
|
|
||||||
user.SetNick(user.Nick)
|
user.ForceSetNick(user.Nick)
|
||||||
|
|
||||||
if user.Email != "" {
|
if user.Email != "" {
|
||||||
user.SetEmail(user.Email)
|
user.SetEmail(user.Email)
|
||||||
|
@ -5,9 +5,34 @@ import * as actions from "./actions"
|
|||||||
|
|
||||||
export class AnimeNotifier {
|
export class AnimeNotifier {
|
||||||
app: Application
|
app: Application
|
||||||
|
visibilityObserver: IntersectionObserver
|
||||||
|
|
||||||
constructor(app: Application) {
|
constructor(app: Application) {
|
||||||
this.app = app
|
this.app = app
|
||||||
|
|
||||||
|
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() {
|
onReadyStateChange() {
|
||||||
@ -24,12 +49,22 @@ export class AnimeNotifier {
|
|||||||
this.app.run()
|
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() {
|
reloadContent() {
|
||||||
return fetch("/_" + this.app.currentPath, {
|
return fetch("/_" + this.app.currentPath, {
|
||||||
credentials: "same-origin"
|
credentials: "same-origin"
|
||||||
})
|
})
|
||||||
.then(response => response.text())
|
.then(response => response.text())
|
||||||
.then(html => Diff.innerHTML(this.app.content, html))
|
.then(html => Diff.innerHTML(this.app.content, html))
|
||||||
|
.then(() => this.app.emit("DOMContentLoaded"))
|
||||||
}
|
}
|
||||||
|
|
||||||
loading(isLoading: boolean) {
|
loading(isLoading: boolean) {
|
||||||
@ -42,32 +77,48 @@ export class AnimeNotifier {
|
|||||||
|
|
||||||
updateActions() {
|
updateActions() {
|
||||||
for(let element of findAll("action")) {
|
for(let element of findAll("action")) {
|
||||||
|
if(element["action assigned"]) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
let actionName = element.dataset.action
|
let actionName = element.dataset.action
|
||||||
|
|
||||||
element.addEventListener(element.dataset.trigger, e => {
|
element.addEventListener(element.dataset.trigger, e => {
|
||||||
actions[actionName](this, element, e)
|
actions[actionName](this, element, e)
|
||||||
})
|
})
|
||||||
|
|
||||||
element.classList.remove("action")
|
// 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateAvatars() {
|
lazyLoadImages() {
|
||||||
for(let element of findAll("user-image")) {
|
for(let element of findAll("lazy")) {
|
||||||
let img = element as HTMLImageElement
|
this.lazyLoadImage(element as HTMLImageElement)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazyLoadImage(img: HTMLImageElement) {
|
||||||
|
// Once the image becomes visible, load it
|
||||||
|
img["became visible"] = () => {
|
||||||
|
img.src = img.dataset.src
|
||||||
|
|
||||||
if(img.naturalWidth === 0) {
|
if(img.naturalWidth === 0) {
|
||||||
img.onload = function() {
|
img.onload = function() {
|
||||||
this.classList.add("user-image-found")
|
this.classList.add("image-found")
|
||||||
}
|
}
|
||||||
|
|
||||||
img.onerror = function() {
|
img.onerror = function() {
|
||||||
this.classList.add("user-image-not-found")
|
this.classList.add("image-not-found")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
img.classList.add("user-image-found")
|
img.classList.add("image-found")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.visibilityObserver.observe(img)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMountables() {
|
updateMountables() {
|
||||||
@ -89,13 +140,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) {
|
onPopState(e: PopStateEvent) {
|
||||||
if(e.state) {
|
if(e.state) {
|
||||||
this.app.load(e.state, {
|
this.app.load(e.state, {
|
||||||
@ -109,8 +153,15 @@ export class AnimeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown(e: KeyboardEvent) {
|
onKeyDown(e: KeyboardEvent) {
|
||||||
// Ctrl + Q = Search
|
// Ignore hotkeys on input elements
|
||||||
if(e.ctrlKey && e.keyCode == 81) {
|
switch(document.activeElement.tagName) {
|
||||||
|
case "INPUT":
|
||||||
|
case "TEXTAREA":
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// F = Search
|
||||||
|
if(e.keyCode == 70) {
|
||||||
let search = this.app.find("search") as HTMLInputElement
|
let search = this.app.find("search") as HTMLInputElement
|
||||||
|
|
||||||
search.focus()
|
search.focus()
|
||||||
|
@ -25,6 +25,10 @@ export class Diff {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if(a.nodeType === Node.ELEMENT_NODE) {
|
if(a.nodeType === Node.ELEMENT_NODE) {
|
||||||
|
if(a.tagName === "IFRAME") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
let removeAttributes: Attr[] = []
|
let removeAttributes: Attr[] = []
|
||||||
|
|
||||||
for(let x = 0; x < a.attributes.length; x++) {
|
for(let x = 0; x < a.attributes.length; x++) {
|
||||||
@ -48,6 +52,11 @@ export class Diff {
|
|||||||
a.setAttribute(attrib.name, b.getAttribute(attrib.name))
|
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)
|
Diff.childNodes(a, b)
|
||||||
|
@ -48,6 +48,8 @@ export function save(arn: AnimeNotifier, input: HTMLInputElement | HTMLTextAreaE
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
arn.loading(false)
|
arn.loading(false)
|
||||||
input.disabled = false
|
input.disabled = false
|
||||||
|
|
||||||
|
return arn.reloadContent()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
36
styles/extension.scarlet
Normal file
36
styles/extension.scarlet
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
.embedded
|
||||||
|
flex-direction column-reverse !important
|
||||||
|
|
||||||
|
.anime-list
|
||||||
|
max-width 500px
|
||||||
|
margin -1.1rem
|
||||||
|
|
||||||
|
thead
|
||||||
|
display none
|
||||||
|
|
||||||
|
.anime-list-item
|
||||||
|
// 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.9rem
|
@ -3,9 +3,19 @@
|
|||||||
// margin-bottom 1rem
|
// margin-bottom 1rem
|
||||||
|
|
||||||
.thread-link
|
.thread-link
|
||||||
horizontal
|
vertical
|
||||||
margin 0.25rem 0
|
margin 0.25rem 0
|
||||||
|
|
||||||
|
.post-author
|
||||||
|
margin-bottom 0.25rem
|
||||||
|
|
||||||
|
> 330px
|
||||||
|
.thread-link
|
||||||
|
horizontal
|
||||||
|
|
||||||
|
.post-author
|
||||||
|
margin-bottom 0
|
||||||
|
|
||||||
.post-content
|
.post-content
|
||||||
ui-element
|
ui-element
|
||||||
flex-grow 1
|
flex-grow 1
|
||||||
|
@ -32,6 +32,7 @@ mixin grid-image
|
|||||||
height 100%
|
height 100%
|
||||||
border-radius 3px
|
border-radius 3px
|
||||||
object-fit cover
|
object-fit cover
|
||||||
|
default-transition
|
||||||
|
|
||||||
mixin grid-icon
|
mixin grid-icon
|
||||||
font-size 2.5rem
|
font-size 2.5rem
|
||||||
|
11
styles/images.scarlet
Normal file
11
styles/images.scarlet
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.lazy
|
||||||
|
visibility hidden
|
||||||
|
opacity 0
|
||||||
|
|
||||||
|
.image-found
|
||||||
|
visibility visible
|
||||||
|
opacity 1 !important
|
||||||
|
|
||||||
|
.image-not-found
|
||||||
|
visibility hidden
|
||||||
|
opacity 0 !important
|
@ -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
|
input, textarea, button, select
|
||||||
font-family inherit
|
font-family inherit
|
||||||
font-size 1em
|
font-size 1em
|
||||||
@ -9,16 +16,15 @@ input, textarea
|
|||||||
border ui-border
|
border ui-border
|
||||||
background white
|
background white
|
||||||
box-shadow none
|
box-shadow none
|
||||||
|
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)
|
|
||||||
|
|
||||||
:disabled
|
:disabled
|
||||||
ui-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
|
button, .button
|
||||||
ui-element
|
ui-element
|
||||||
horizontal
|
horizontal
|
@ -4,13 +4,6 @@
|
|||||||
height avatar-size
|
height avatar-size
|
||||||
border-radius 100%
|
border-radius 100%
|
||||||
object-fit cover
|
object-fit cover
|
||||||
opacity 0
|
|
||||||
default-transition
|
default-transition
|
||||||
:hover
|
:hover
|
||||||
box-shadow outline-shadow-heavy
|
box-shadow outline-shadow-heavy
|
||||||
|
|
||||||
.user-image-found
|
|
||||||
opacity 1 !important
|
|
||||||
|
|
||||||
.user-image-not-found
|
|
||||||
opacity 0 !important
|
|
12
tests.go
12
tests.go
@ -10,6 +10,10 @@ var tests = map[string][]string{
|
|||||||
"/+Akyoto/threads",
|
"/+Akyoto/threads",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
"/user/:nick/posts": []string{
|
||||||
|
"/+Akyoto/posts",
|
||||||
|
},
|
||||||
|
|
||||||
"/user/:nick/animelist": []string{
|
"/user/:nick/animelist": []string{
|
||||||
"/+Akyoto/animelist",
|
"/+Akyoto/animelist",
|
||||||
},
|
},
|
||||||
@ -56,6 +60,14 @@ var tests = map[string][]string{
|
|||||||
"/api/animelist/4J6qpK1ve",
|
"/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/:id": []string{
|
||||||
"/api/settings/4J6qpK1ve",
|
"/api/settings/4J6qpK1ve",
|
||||||
},
|
},
|
||||||
|
10
utils/allowembed.go
Normal file
10
utils/allowembed.go
Normal file
@ -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
|
||||||
|
}
|
15
utils/container.go
Normal file
15
utils/container.go
Normal file
@ -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 ""
|
||||||
|
}
|
@ -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()
|
|
||||||
}
|
|
@ -5,23 +5,7 @@ import (
|
|||||||
"github.com/animenotifier/arn"
|
"github.com/animenotifier/arn"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetUser ...
|
// GetUser returns the logged in user for the given context.
|
||||||
func GetUser(ctx *aero.Context) *arn.User {
|
func GetUser(ctx *aero.Context) *arn.User {
|
||||||
if !ctx.HasSession() {
|
return arn.GetUserFromContext(ctx)
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := ctx.Session().GetString("userId")
|
|
||||||
|
|
||||||
if userID == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := arn.GetUser(userID)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return user
|
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user