Skip to content

Commit 9cc58b5

Browse files
Add spaces-go-sdk
1 parent b86fda7 commit 9cc58b5

8 files changed

Lines changed: 334 additions & 0 deletions

File tree

go.work

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ go 1.26
33
use (
44
integrations/analytics-go-sdk
55
integrations/idp-go-sdk
6+
integrations/spaces-go-sdk
67
integrations/storage-go-sdk
78
services/core
89
services/storage

integrations/spaces-go-sdk/LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Oliver Schlüter
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Spaces Go SDK
2+
3+
Go documentation: https://pkg.go.dev/github.com/fancyinnovations/fancyspaces/integrations/spaces-go-sdk

integrations/spaces-go-sdk/go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module github.com/fancyinnovations/fancyspaces/integrations/spaces-go-sdk
2+
3+
go 1.26
4+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package spaces
2+
3+
import (
4+
"errors"
5+
)
6+
7+
var (
8+
ErrSlugTooLong = errors.New("slug exceeds maximum length of 20 characters")
9+
ErrSlugTooShort = errors.New("slug must be at least 3 characters long")
10+
ErrTitleTooLong = errors.New("title exceeds maximum length of 100 characters")
11+
ErrTitleTooShort = errors.New("title must be at least 3 characters long")
12+
ErrDescriptionTooLong = errors.New("description exceeds maximum length of 500 characters")
13+
ErrSpaceNotFound = errors.New("space not found")
14+
)
Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
package spaces
2+
3+
import (
4+
"time"
5+
6+
"github.com/fancyinnovations/fancyspaces/integrations/idp-go-sdk/idp"
7+
)
8+
9+
type Space struct {
10+
ID string `json:"id"`
11+
Slug string `json:"slug"`
12+
Title string `json:"title"`
13+
Summary string `json:"summary"`
14+
Description string `json:"description"`
15+
Categories []Category `json:"categories"`
16+
Links []Link `json:"links"`
17+
IconURL string `json:"icon_url"`
18+
Status Status `json:"status"`
19+
CreatedAt time.Time `json:"created_at"`
20+
Creator string `json:"creator"` // UserID of the creator
21+
Members []Member `json:"members"`
22+
23+
IssueSettings IssueSettings `json:"issue_settings"`
24+
ReleaseSettings ReleaseSettings `json:"release_settings"`
25+
MavenRepositorySettings MavenRepositorySettings `json:"maven_repository_settings"`
26+
StorageSettings StorageSettings `json:"storage_settings"`
27+
AnalyticsSettings AnalyticsSettings `json:"analytics_settings"`
28+
}
29+
30+
// InternalSpace is the internal representation of a Space, containing additional fields that are not exposed to clients.
31+
type InternalSpace struct {
32+
Space
33+
34+
AnalyticsWriteKey string `json:"analytics_write_key"`
35+
}
36+
37+
type IssueSettings struct {
38+
Enabled bool `json:"enabled"`
39+
40+
GitHubSync bool `json:"github_sync"`
41+
GitHubSyncOwner string `json:"github_sync_owner"` // GitHub username or organization (required if GitHubSync is true)
42+
GitHubSyncRepo string `json:"github_sync_repo"` // GitHub repository name (required if GitHubSync is true)
43+
GitHubSyncLabel string `json:"github_sync_label"` // only sync issues with this label (optional)
44+
}
45+
46+
type ReleaseSettings struct {
47+
Enabled bool `json:"enabled"`
48+
49+
// TODO: Implement these settings in the future
50+
//DiscordNotifications bool `json:"discord_notifications"`
51+
//DiscordNotificationWebhookURL string `json:"discord_notification_webhook_url"`
52+
}
53+
54+
type MavenRepositorySettings struct {
55+
Enabled bool `json:"enabled"`
56+
}
57+
58+
type StorageSettings struct {
59+
Enabled bool `json:"enabled"`
60+
}
61+
62+
type AnalyticsSettings struct {
63+
Enabled bool `json:"enabled"`
64+
RequireWriteKey bool `json:"require_write_key"`
65+
WriteKey string `json:"-"`
66+
}
67+
68+
type Link struct {
69+
Name string `json:"name"`
70+
URL string `json:"url"`
71+
}
72+
73+
type Member struct {
74+
UserID string `json:"user_id"`
75+
Role Role `json:"role"`
76+
}
77+
78+
type Role string
79+
80+
const (
81+
RoleAdmin Role = "admin"
82+
RoleMember Role = "member"
83+
)
84+
85+
type Status string
86+
87+
const (
88+
StatusDraft Status = "draft"
89+
StatusReview Status = "review"
90+
StatusApproved Status = "approved"
91+
StatusPrivate Status = "private"
92+
StatusArchived Status = "archived"
93+
StatusRejected Status = "rejected"
94+
StatusBanned Status = "banned"
95+
)
96+
97+
type Category string
98+
99+
const (
100+
CategoryMinecraftPlugin Category = "minecraft_plugin"
101+
CategoryMinecraftServer Category = "minecraft_server"
102+
CategoryMinecraftMod Category = "minecraft_mod"
103+
CategoryHytalePlugin Category = "hytale_plugin"
104+
CategoryWebApp Category = "web_app"
105+
CategoryMobileApp Category = "mobile_app"
106+
CategoryOther Category = "other"
107+
)
108+
109+
func (s *Space) IsMember(u *idp.User) bool {
110+
if !idp.IsUserValid(u) {
111+
return false
112+
}
113+
114+
if s.Creator == u.ID {
115+
return true
116+
}
117+
118+
for _, m := range s.Members {
119+
if m.UserID == u.ID {
120+
return true
121+
}
122+
}
123+
124+
return false
125+
}
126+
127+
func (s *Space) IsOwner(u *idp.User) bool {
128+
if !idp.IsUserValid(u) {
129+
return false
130+
}
131+
132+
return s.Creator == u.ID
133+
}
134+
135+
func (s *Space) HasFullAccess(u *idp.User) bool {
136+
if !idp.IsUserValid(u) {
137+
return false
138+
}
139+
140+
if s.Creator == u.ID {
141+
return true
142+
}
143+
144+
for _, m := range s.Members {
145+
if m.UserID == u.ID {
146+
return m.Role == RoleAdmin
147+
}
148+
}
149+
150+
return false
151+
}
152+
153+
func (s *Space) HasWriteAccess(u *idp.User) bool {
154+
if !idp.IsUserValid(u) {
155+
return false
156+
}
157+
158+
if s.Creator == u.ID {
159+
return true
160+
}
161+
162+
for _, m := range s.Members {
163+
if m.UserID == u.ID {
164+
return m.Role == RoleAdmin || m.Role == RoleMember
165+
}
166+
}
167+
168+
return false
169+
}
170+
171+
func (s *Space) Validate() error {
172+
if len(s.Slug) < 3 {
173+
return ErrSlugTooShort
174+
}
175+
if len(s.Slug) > 20 {
176+
return ErrSlugTooLong
177+
}
178+
179+
if len(s.Title) > 100 {
180+
return ErrTitleTooLong
181+
}
182+
if len(s.Title) < 3 {
183+
return ErrTitleTooShort
184+
}
185+
186+
if len(s.Description) > 500 {
187+
return ErrDescriptionTooLong
188+
}
189+
190+
return nil
191+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package spaces
2+
3+
import (
4+
"encoding/json"
5+
6+
"github.com/OliverSchlueter/goutils/broker"
7+
)
8+
9+
type Service struct {
10+
broker broker.Broker
11+
spacesCache *spacesCache
12+
}
13+
14+
type Configuration struct {
15+
Broker broker.Broker
16+
}
17+
18+
func NewService(cfg Configuration) *Service {
19+
return &Service{
20+
broker: cfg.Broker,
21+
spacesCache: newSpacesCache(),
22+
}
23+
}
24+
25+
func (s *Service) GetSpace(id string) (*InternalSpace, error) {
26+
spaceFromCache, err := s.spacesCache.GetByID(id)
27+
if err == nil {
28+
return spaceFromCache, nil
29+
}
30+
31+
resp, err := s.broker.Request("fancyspaces.space.get", []byte(id))
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
var space InternalSpace
37+
if err := json.Unmarshal(resp.Data, &space); err != nil {
38+
return nil, err
39+
}
40+
41+
s.spacesCache.UpsertSpace(&space)
42+
43+
return &space, nil
44+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package spaces
2+
3+
import (
4+
"log/slog"
5+
"reflect"
6+
"time"
7+
8+
"github.com/OliverSchlueter/goutils/sloki"
9+
"github.com/dgraph-io/ristretto/v2"
10+
)
11+
12+
var (
13+
sizeOfUser = int64(reflect.TypeFor[InternalSpace]().Size())
14+
ttl = 30 * time.Minute
15+
)
16+
17+
type spacesCache struct {
18+
cache *ristretto.Cache[string, *InternalSpace]
19+
}
20+
21+
func newSpacesCache() *spacesCache {
22+
cache, err := ristretto.NewCache(&ristretto.Config[string, *InternalSpace]{
23+
NumCounters: 100 * 10, // x10 of expected number of elements when full
24+
MaxCost: 64 * 1024 * 1024, // 64 MB
25+
BufferItems: 64, // keep 64
26+
})
27+
if err != nil {
28+
slog.Error("Failed to create space cache", sloki.WrapError(err))
29+
panic(err)
30+
}
31+
32+
return &spacesCache{
33+
cache: cache,
34+
}
35+
}
36+
37+
func (c *spacesCache) GetByID(id string) (*InternalSpace, error) {
38+
if id == "" {
39+
return nil, ErrSpaceNotFound
40+
}
41+
42+
user, found := c.cache.Get(id)
43+
if !found {
44+
return nil, ErrSpaceNotFound
45+
}
46+
47+
return user, nil
48+
}
49+
50+
func (c *spacesCache) UpsertSpace(s *InternalSpace) {
51+
c.cache.SetWithTTL(s.ID, s, sizeOfUser, ttl)
52+
}
53+
54+
func (c *spacesCache) Invalidate(s *InternalSpace) {
55+
c.cache.Del(s.ID)
56+
}

0 commit comments

Comments
 (0)