Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ Some of these were slightly altered if the license allowed it.
Treat each of the files in this repository with the same license terms as the
original file.

**It seems iconfinder was renamed and all the URLs aren't correct anymore. If you are the author of any of the icons and want me to attribute you differently, create an issue please.**

* Logo - All rights reserved, excluded from BSD-3 licensing
* Background - All rights reserved, excluded from BSD-3 licensing
* Favicon - All rights reserved, excluded from BSD-3 licensing
Expand All @@ -204,4 +206,5 @@ original file.
* [Trash Icon](https://www.iconfinder.com/icons/315225/trash_can_icon) - Made by [Yannick Lung](https://yannicklung.com)
* [Undo Icon](https://www.iconfinder.com/icons/308948/arrow_undo_icon) - Made by [Ivan Boyko](https://www.iconfinder.com/visualpharm)
* [Alarmclock Icon](https://www.iconfinder.com/icons/4280508/alarm_outlined_alert_clock_icon) - Made by [Kit of Parts](https://www.iconfinder.com/kitofparts)
* https://www.iconfinder.com/icons/808399/load_turn_turnaround_icon TODO
* https://www.iconfinder.com/icons/808399/load_turn_turnaround_icon - Made by Pixel Bazaar
*[Gallery Icon](https://www.svgrepo.com/svg/528992/gallery-wide) - Made by [Solar Icons](https://www.svgrepo.com/author/Solar%20Icons/)
27 changes: 27 additions & 0 deletions internal/api/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func (handler *V1Handler) SetupRoutes(rootPath string, register func(string, str
register("GET", path.Join(v1, "lobby"), handler.getLobbies)
register("POST", path.Join(v1, "lobby"), handler.postLobby)

register("GET", path.Join(v1, "lobby", "{lobby_id}", "gallery"), handler.getGallery)
register("PATCH", path.Join(v1, "lobby", "{lobby_id}"), handler.patchLobby)
// We support both path parameter and cookie.
register("PATCH", path.Join(v1, "lobby"), handler.patchLobby)
Expand Down Expand Up @@ -83,3 +84,29 @@ func GetIPAddressFromRequest(request *http.Request) string {

return remoteAddressToSimpleIP(request.RemoteAddr)
}

func SetDiscordCookie(
w http.ResponseWriter,
key, value string,
) {
http.SetCookie(w, &http.Cookie{
Name: key,
Value: value,
Domain: discordDomain,
Path: "/",
SameSite: http.SameSiteNoneMode,
Partitioned: true,
Secure: true,
})
}
func SetNormalCookie(
w http.ResponseWriter,
key, value string,
) {
http.SetCookie(w, &http.Cookie{
Name: key,
Value: value,
Path: "/",
SameSite: http.SameSiteStrictMode,
})
}
129 changes: 85 additions & 44 deletions internal/api/v1.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import (
"golang.org/x/text/language"
)

var ErrLobbyNotExistent = errors.New("the requested lobby doesn't exist")
var (
ErrLobbyNotExistent = errors.New("the requested lobby doesn't exist")
ErrSessionNotMatching = errors.New("session doesn't match any player")
)

type V1Handler struct {
cfg *config.Config
Expand All @@ -31,16 +34,20 @@ func NewHandler(cfg *config.Config) *V1Handler {
}
}

func writeJson(writer http.ResponseWriter, bytes []byte) error {
writer.Header().Set("Content-Type", "application/json")
writer.Header().Set("Content-Length", strconv.Itoa(len(bytes)))
_, err := writer.Write(bytes)
return err
}

func marshalToHTTPWriter(data any, writer http.ResponseWriter) (bool, error) {
bytes, err := json.Marshal(data)
if err != nil {
return false, err
}

writer.Header().Set("Content-Type", "application/json")
writer.Header().Set("Content-Length", strconv.Itoa(len(bytes)))
_, err = writer.Write(bytes)
return true, err
return true, writeJson(writer, bytes)
}

type LobbyEntries []*LobbyEntry
Expand Down Expand Up @@ -200,6 +207,73 @@ func (handler *V1Handler) postLobby(writer http.ResponseWriter, request *http.Re
state.AddLobby(lobby)
}

type Gallery []game.GalleryDrawing

func (handler *V1Handler) getGallery(writer http.ResponseWriter, request *http.Request) {
// Cached lobbies should simply run into an error if they try to update.
userSession, err := GetUserSession(request)
if err != nil {
log.Printf("error getting user session: %v", err)
http.Error(writer, "no valid usersession supplied", http.StatusBadRequest)
return
}

if userSession == uuid.Nil {
http.Error(writer, "no usersession supplied", http.StatusBadRequest)
return
}

if err := request.ParseForm(); err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
return
}

rawLocalCacheCount := request.FormValue("local_cache_count")
localCacheCount, err := strconv.Atoi(rawLocalCacheCount)
if err != nil {
http.Error(writer, err.Error(), http.StatusBadRequest)
return
}

lobby := state.GetLobby(GetLobbyId(request))
if lobby == nil {
http.Error(writer, ErrLobbyNotExistent.Error(), http.StatusNotFound)
return
}

var after func()
lobby.Synchronized(func() {
// FIXME Improve these.
if localCacheCount == len(lobby.Drawings) {
after = func() {
http.Error(writer, "drawings unchanged", http.StatusNoContent)
}
return
}

if lobby.GetPlayerBySession(userSession) == nil {
after = func() {
http.Error(writer, ErrSessionNotMatching.Error(), http.StatusForbidden)
}
return
}

encodedJson, err := json.Marshal(Gallery(lobby.Drawings))
after = func() {
if err != nil {
http.Error(writer, err.Error(), http.StatusInternalServerError)
return
}

if err := writeJson(writer, encodedJson); err != nil {
log.Println("Error responding to gallery request:", err)
return
}
}
})
after()
}

func (handler *V1Handler) postPlayer(writer http.ResponseWriter, request *http.Request) {
lobby := state.GetLobby(request.PathValue("lobby_id"))
if lobby == nil {
Expand Down Expand Up @@ -263,15 +337,7 @@ const discordDomain = "1320396325925163070.discordsays.com"
func SetDiscordCookies(w http.ResponseWriter, request *http.Request) {
discordInstanceId := GetDiscordInstanceId(request)
if discordInstanceId != "" {
http.SetCookie(w, &http.Cookie{
Name: "discord-instance-id",
Value: discordInstanceId,
Domain: discordDomain,
Path: "/",
SameSite: http.SameSiteNoneMode,
Partitioned: true,
Secure: true,
})
SetDiscordCookie(w, "discord-instance-id", discordInstanceId)
}
}

Expand All @@ -285,39 +351,14 @@ func SetGameplayCookies(
) {
discordInstanceId := GetDiscordInstanceId(request)
if discordInstanceId != "" {
http.SetCookie(w, &http.Cookie{
Name: "usersession",
Value: player.GetUserSession().String(),
Domain: discordDomain,
Path: "/",
SameSite: http.SameSiteNoneMode,
Partitioned: true,
Secure: true,
})
http.SetCookie(w, &http.Cookie{
Name: "lobby-id",
Value: lobby.LobbyID,
Domain: discordDomain,
Path: "/",
SameSite: http.SameSiteNoneMode,
Partitioned: true,
Secure: true,
})
SetDiscordCookie(w, "usersession", player.GetUserSession().String())
SetDiscordCookie(w, "lobby-id", lobby.LobbyID)
} else {
// FIXME This comment seems nonsensical, am i not getting something?
// For the discord case, we need both, as the discord specific cookies
// aren't available during the readirect from ssrCreate to ssrEnter.
http.SetCookie(w, &http.Cookie{
Name: "usersession",
Value: player.GetUserSession().String(),
Path: "/",
SameSite: http.SameSiteStrictMode,
})
http.SetCookie(w, &http.Cookie{
Name: "lobby-id",
Value: lobby.LobbyID,
Path: "/",
SameSite: http.SameSiteStrictMode,
})
SetNormalCookie(w, "usersession", player.GetUserSession().String())
SetNormalCookie(w, "lobby-id", lobby.LobbyID)
}
}

Expand Down
52 changes: 52 additions & 0 deletions internal/frontend/gallery.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package frontend

import (
"log"
"net/http"
"strings"

"github.com/scribble-rs/scribble.rs/internal/api"
"github.com/scribble-rs/scribble.rs/internal/translations"
)

type galleryPageData struct {
*BasePageConfig

LobbyID string
Translation *translations.Translation
Locale string
}

func (handler *SSRHandler) ssrGallery(writer http.ResponseWriter, request *http.Request) {
userAgent := strings.ToLower(request.UserAgent())
if !isHumanAgent(userAgent) {
translation, _ := determineTranslation(request)
writer.WriteHeader(http.StatusForbidden)
handler.userFacingError(writer, translation.Get("forbidden"), translation)
return
}

lobbyId := request.PathValue("lobby_id")

// FIXME Do we care about discord anymore?
api.SetNormalCookie(writer, "lobby-id", lobbyId)
api.SetNormalCookie(writer, "root-path", handler.basePageConfig.RootPath)

// Note that this lobby doesn't have to exist necessarily, as the user can still have
// the data cached locally.

translation, locale := determineTranslation(request)
pageData := &galleryPageData{
BasePageConfig: handler.basePageConfig,
LobbyID: lobbyId,
Translation: translation,
Locale: locale,
}

// If the pagedata isn't initialized, it means the synchronized block has exited.
// In this case we don't want to template the lobby, since an error has occurred
// and probably already has been handled.
if err := pageTemplates.ExecuteTemplate(writer, "gallery-page", pageData); err != nil {
log.Printf("Error templating lobby: %s\n", err)
}
}
1 change: 1 addition & 0 deletions internal/frontend/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ func (handler *SSRHandler) SetupRoutes(register func(string, string, http.Handle
registerWithCsp("GET", path.Join(handler.cfg.RootPath, "lobby.js"), handler.lobbyJs)
registerWithCsp("GET", path.Join(handler.cfg.RootPath, "index.js"), handler.indexJs)
registerWithCsp("GET", path.Join(handler.cfg.RootPath, "lobby", "{lobby_id}"), handler.ssrEnterLobby)
registerWithCsp("GET", path.Join(handler.cfg.RootPath, "lobby", "{lobby_id}", "gallery"), handler.ssrGallery)
registerWithCsp("POST", path.Join(handler.cfg.RootPath, "lobby"), handler.ssrCreateLobby)
}

Expand Down
1 change: 0 additions & 1 deletion internal/frontend/index.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@ func NewHandler(cfg *config.Config) (*SSRHandler, error) {
if err != nil {
return nil, fmt.Errorf("error parsing lobby js template: %w", err)
}

lobbyJsRawTemplate.AddParseTree("footer", pageTemplates.Tree)

entries, err := frontendResourcesFS.ReadDir("resources")
Expand Down
1 change: 1 addition & 0 deletions internal/frontend/lobby.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ func (handler *SSRHandler) lobbyJs(writer http.ResponseWriter, request *http.Req
if err := handler.lobbyJsRawTemplate.ExecuteTemplate(writer, "lobby-js", pageData); err != nil {
log.Printf("error templating JS: %s\n", err)
}

}

// ssrEnterLobby opens a lobby, either opening it directly or asking for a lobby.
Expand Down
7 changes: 7 additions & 0 deletions internal/frontend/lobby.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ String.prototype.format = function() {
};

const discordInstanceId = getCookie("discord-instance-id")
const lobbyId = getCookie("lobby-id");
const rootPath = `${discordInstanceId ? ".proxy/" : ""}{{.RootPath}}`

let socketIsConnecting = false;
Expand Down Expand Up @@ -346,6 +347,12 @@ function toggleFullscreen() {
}
document.getElementById("toggle-fullscreen-button").addEventListener("click", toggleFullscreen);

function showGallery() {
window.open(`${rootPath}/lobby/${lobbyId}/gallery`,"_blank" );
}

document.getElementById("show-gallery-button").addEventListener("click", showGallery);

function showLobbySettingsDialog() {
hideMenu();
lobbySettingsDialog.style.visibility = "visible";
Expand Down
40 changes: 40 additions & 0 deletions internal/frontend/resources/gallery.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
.app {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
box-sizing: border-box;
height: 100%;
}

#drawing-board {
border-radius: var(--component-border-radius);
min-height: 0;
max-width: 100%;
margin-left: auto;
margin-right: auto;
}

.top-bar {
display: flex;
flex-direction: row;
justify-content: space-between;
padding: 0.5rem;
background-color: var(--component-base-color);
border-radius: var(--component-border-radius);

* {
font-size: 1.5rem;
}
}

.prev,
.next {
border: 1px solid black;
}

.prev {
}

.next {
}
Loading