diff --git a/server/common/common.go b/server/common/common.go
new file mode 100644
index 0000000..08e50a8
--- /dev/null
+++ b/server/common/common.go
@@ -0,0 +1,6 @@
+package common
+
+const (
+ // BotID is the id of bot.
+ BotID = 0
+)
diff --git a/server/route/api/v1/shortcut_service.go b/server/route/api/v1/shortcut_service.go
index c1a65f0..3f646b0 100644
--- a/server/route/api/v1/shortcut_service.go
+++ b/server/route/api/v1/shortcut_service.go
@@ -10,8 +10,6 @@ import (
"github.com/pkg/errors"
"golang.org/x/exp/slices"
"google.golang.org/grpc/codes"
- "google.golang.org/grpc/metadata"
- "google.golang.org/grpc/peer"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/types/known/emptypb"
@@ -110,11 +108,6 @@ func (s *APIV1Service) GetShortcutByName(ctx context.Context, request *v1pb.GetS
}
}
- // Create shortcut view activity.
- if err := s.createShortcutViewActivity(ctx, shortcut); err != nil {
- fmt.Printf("failed to create activity, err: %v", err)
- }
-
composedShortcut, err := s.convertShortcutFromStorepb(ctx, shortcut)
if err != nil {
return nil, status.Errorf(codes.Internal, "failed to convert shortcut, err: %v", err)
@@ -352,35 +345,6 @@ func mapToAnalyticsSlice(m map[string]int32) []*v1pb.GetShortcutAnalyticsRespons
return analyticsSlice
}
-func (s *APIV1Service) createShortcutViewActivity(ctx context.Context, shortcut *storepb.Shortcut) error {
- p, _ := peer.FromContext(ctx)
- headers, ok := metadata.FromIncomingContext(ctx)
- if !ok {
- return errors.New("Failed to get metadata from context")
- }
- payload := &storepb.ActivityShorcutViewPayload{
- ShortcutId: shortcut.Id,
- Ip: p.Addr.String(),
- Referer: headers.Get("referer")[0],
- UserAgent: headers.Get("user-agent")[0],
- }
- payloadStr, err := protojson.Marshal(payload)
- if err != nil {
- return errors.Wrap(err, "Failed to marshal activity payload")
- }
- activity := &store.Activity{
- CreatorID: BotID,
- Type: store.ActivityShortcutView,
- Level: store.ActivityInfo,
- Payload: string(payloadStr),
- }
- _, err = s.Store.CreateActivity(ctx, activity)
- if err != nil {
- return errors.Wrap(err, "Failed to create activity")
- }
- return nil
-}
-
func (s *APIV1Service) createShortcutCreateActivity(ctx context.Context, shortcut *storepb.Shortcut) error {
payload := &storepb.ActivityShorcutCreatePayload{
ShortcutId: shortcut.Id,
diff --git a/server/route/api/v1/user_service.go b/server/route/api/v1/user_service.go
index c34f95f..d423b88 100644
--- a/server/route/api/v1/user_service.go
+++ b/server/route/api/v1/user_service.go
@@ -18,11 +18,6 @@ import (
"github.com/yourselfhosted/slash/store"
)
-const (
- // BotID is the id of bot.
- BotID = 0
-)
-
func (s *APIV1Service) ListUsers(ctx context.Context, _ *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) {
users, err := s.Store.ListUsers(ctx, &store.FindUser{})
if err != nil {
diff --git a/server/route/frontend/frontend.go b/server/route/frontend/frontend.go
index 25b01ef..68672e6 100644
--- a/server/route/frontend/frontend.go
+++ b/server/route/frontend/frontend.go
@@ -5,14 +5,18 @@ import (
"embed"
"fmt"
"io/fs"
+ "log/slog"
"net/http"
"strings"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
+ "github.com/pkg/errors"
+ "google.golang.org/protobuf/encoding/protojson"
"github.com/yourselfhosted/slash/internal/util"
storepb "github.com/yourselfhosted/slash/proto/gen/store"
+ "github.com/yourselfhosted/slash/server/common"
"github.com/yourselfhosted/slash/server/metric"
"github.com/yourselfhosted/slash/server/profile"
"github.com/yourselfhosted/slash/store"
@@ -44,35 +48,34 @@ func (s *FrontendService) Serve(ctx context.Context, e *echo.Echo) {
HTML5: true,
Filesystem: getFileSystem("dist"),
Skipper: func(c echo.Context) bool {
- return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/robots.txt", "/sitemap.xml", "/s/:shortcutName", "/c/:collectionName")
+ return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/s/:shortcutName", "/c/:collectionName")
},
}))
- g := e.Group("assets")
+ assetsGroup := e.Group("assets")
// Use echo gzip middleware to compress the response.
// Reference: https://echo.labstack.com/docs/middleware/gzip
- g.Use(middleware.GzipWithConfig(middleware.GzipConfig{
+ assetsGroup.Use(middleware.GzipWithConfig(middleware.GzipConfig{
Skipper: func(c echo.Context) bool {
- return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/robots.txt", "/sitemap.xml", "/s/:shortcutName", "/c/:collectionName")
+ return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/s/:shortcutName", "/c/:collectionName")
},
Level: 5,
}))
- g.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
+ assetsGroup.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
c.Response().Header().Set(echo.HeaderCacheControl, "max-age=31536000, immutable")
return next(c)
}
})
- g.Use(middleware.StaticWithConfig(middleware.StaticConfig{
+ assetsGroup.Use(middleware.StaticWithConfig(middleware.StaticConfig{
HTML5: true,
Filesystem: getFileSystem("dist/assets"),
Skipper: func(c echo.Context) bool {
- return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/robots.txt", "/sitemap.xml", "/s/:shortcutName", "/c/:collectionName")
+ return util.HasPrefixes(c.Path(), "/api", "/slash.api.v1", "/s/:shortcutName", "/c/:collectionName")
},
}))
s.registerRoutes(e)
- s.registerFileRoutes(ctx, e)
}
func (s *FrontendService) registerRoutes(e *echo.Echo) {
@@ -84,21 +87,20 @@ func (s *FrontendService) registerRoutes(e *echo.Echo) {
shortcut, err := s.Store.GetShortcut(ctx, &store.FindShortcut{
Name: &shortcutName,
})
- if err != nil {
- return c.HTML(http.StatusOK, rawIndexHTML)
- }
- if shortcut == nil {
+ // If any error occurs or the shortcut is not found, return the raw `index.html`.
+ if err != nil || shortcut == nil {
return c.HTML(http.StatusOK, rawIndexHTML)
}
- metric.Enqueue("shortcut view")
- // Only set the `Location` header if the link is a valid URI.
- if util.ValidateURI(shortcut.Link) {
- c.Response().Header().Set("Location", shortcut.Link)
+ // Create shortcut view activity.
+ if err := s.createShortcutViewActivity(ctx, c.Request(), shortcut); err != nil {
+ slog.Warn("failed to create shortcut view activity", slog.String("error", err.Error()))
}
+ metric.Enqueue("shortcut view")
+
// Inject shortcut metadata into `index.html`.
indexHTML := strings.ReplaceAll(rawIndexHTML, headerMetadataPlaceholder, generateShortcutMetadata(shortcut).String())
- return c.HTML(http.StatusPermanentRedirect, indexHTML)
+ return c.HTML(http.StatusOK, indexHTML)
})
e.GET("/c/:collectionName", func(c echo.Context) error {
@@ -107,10 +109,8 @@ func (s *FrontendService) registerRoutes(e *echo.Echo) {
collection, err := s.Store.GetCollection(ctx, &store.FindCollection{
Name: &collectionName,
})
- if err != nil {
- return c.HTML(http.StatusOK, rawIndexHTML)
- }
- if collection == nil {
+ // If any error occurs or the collection is not found, return the raw `index.html`.
+ if err != nil || collection == nil {
return c.HTML(http.StatusOK, rawIndexHTML)
}
@@ -121,50 +121,42 @@ func (s *FrontendService) registerRoutes(e *echo.Echo) {
})
}
-func (s *FrontendService) registerFileRoutes(ctx context.Context, e *echo.Echo) {
- workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
+func (s *FrontendService) createShortcutViewActivity(ctx context.Context, request *http.Request, shortcut *storepb.Shortcut) error {
+ ip := getReadUserIP(request)
+ referer := request.Header.Get("Referer")
+ userAgent := request.Header.Get("User-Agent")
+ payload := &storepb.ActivityShorcutViewPayload{
+ ShortcutId: shortcut.Id,
+ Ip: ip,
+ Referer: referer,
+ UserAgent: userAgent,
+ }
+ payloadStr, err := protojson.Marshal(payload)
if err != nil {
- return
+ return errors.Wrap(err, "Failed to marshal activity payload")
}
- instanceURL := workspaceGeneralSetting.InstanceUrl
- if instanceURL == "" {
- return
+ activity := &store.Activity{
+ CreatorID: common.BotID,
+ Type: store.ActivityShortcutView,
+ Level: store.ActivityInfo,
+ Payload: string(payloadStr),
}
+ _, err = s.Store.CreateActivity(ctx, activity)
+ if err != nil {
+ return errors.Wrap(err, "Failed to create activity")
+ }
+ return nil
+}
- e.GET("/robots.txt", func(c echo.Context) error {
- robotsTxt := fmt.Sprintf(`User-agent: *
-Allow: /
-Host: %s
-Sitemap: %s/sitemap.xml`, instanceURL, instanceURL)
- return c.String(http.StatusOK, robotsTxt)
- })
-
- e.GET("/sitemap.xml", func(c echo.Context) error {
- urlsets := []string{}
- // Append shortcut list.
- shortcuts, err := s.Store.ListShortcuts(ctx, &store.FindShortcut{
- VisibilityList: []store.Visibility{store.VisibilityPublic},
- })
- if err != nil {
- return err
- }
- for _, shortcut := range shortcuts {
- urlsets = append(urlsets, fmt.Sprintf(`%s/s/%s`, instanceURL, shortcut.Name))
- }
- // Append collection list.
- collections, err := s.Store.ListCollections(ctx, &store.FindCollection{
- VisibilityList: []store.Visibility{store.VisibilityPublic},
- })
- if err != nil {
- return err
- }
- for _, collection := range collections {
- urlsets = append(urlsets, fmt.Sprintf(`%s/c/%s`, instanceURL, collection.Name))
- }
-
- sitemap := fmt.Sprintf(`%s`, strings.Join(urlsets, "\n"))
- return c.XMLBlob(http.StatusOK, []byte(sitemap))
- })
+func getReadUserIP(r *http.Request) string {
+ IPAddress := r.Header.Get("X-Real-Ip")
+ if IPAddress == "" {
+ IPAddress = r.Header.Get("X-Forwarded-For")
+ }
+ if IPAddress == "" {
+ IPAddress = r.RemoteAddr
+ }
+ return IPAddress
}
func getFileSystem(path string) http.FileSystem {