package main
import (
"context"
"encoding/json"
"errors"
"net/http"
"strings"
)
func writeJSON(w http.ResponseWriter, status int, body any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(body)
}
func writeError(w http.ResponseWriter, status int, msg string) {
writeJSON(w, status, map[string]string{"error": msg})
}
func withCORS(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
h(w, r)
}
}
type tokenVerifier interface {
Verify(ctx context.Context, token string) (string, error)
}
type App struct {
verifier tokenVerifier
store *Store
contacts *Contacts
defaultJitsiURL string
}
func (a *App) Routes(mux *http.ServeMux) {
mux.HandleFunc("/me", withCORS(a.requireAuth(a.handleMe)))
mux.HandleFunc("/register", withCORS(a.requireAuth(a.handleRegister)))
mux.HandleFunc("/poll", withCORS(a.requireAuth(a.handlePoll)))
mux.HandleFunc("/call", withCORS(a.requireAuth(a.handleCall)))
mux.HandleFunc("/contacts", withCORS(a.requireAuth(a.handleContacts)))
mux.HandleFunc("/contacts/{username}", withCORS(a.requireAuth(a.handleContact)))
}
type authedHandler func(w http.ResponseWriter, r *http.Request, me string)
func (a *App) requireAuth(h authedHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token, ok := strings.CutPrefix(r.Header.Get("Authorization"), "Bearer ")
if !ok {
w.Header().Set("WWW-Authenticate", "Bearer")
writeError(w, http.StatusUnauthorized, "sign in required")
return
}
username, err := a.verifier.Verify(r.Context(), token)
if errors.Is(err, errIdentityUnavailable) {
writeError(w, http.StatusServiceUnavailable, err.Error())
return
}
if err != nil {
w.Header().Set("WWW-Authenticate", `Bearer error="invalid_token"`)
writeError(w, http.StatusUnauthorized, "invalid or expired token")
return
}
h(w, r, username)
}
}
func (a *App) handleMe(w http.ResponseWriter, r *http.Request, me string) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "GET only")
return
}
writeJSON(w, http.StatusOK, map[string]string{"username": me, "defaultJitsiUrl": a.defaultJitsiURL})
}
func (a *App) handleRegister(w http.ResponseWriter, r *http.Request, me string) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "POST only")
return
}
a.store.Touch(me)
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}
func (a *App) handlePoll(w http.ResponseWriter, r *http.Request, me string) {
if r.Method != http.MethodGet {
writeError(w, http.StatusMethodNotAllowed, "GET only")
return
}
writeJSON(w, http.StatusOK, map[string][]Invite{"invites": a.store.DrainInvites(me)})
}
func (a *App) handleCall(w http.ResponseWriter, r *http.Request, me string) {
if r.Method != http.MethodPost {
writeError(w, http.StatusMethodNotAllowed, "POST only")
return
}
var body struct {
To string `json:"to"`
JitsiURL string `json:"jitsiUrl"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
to, err := normalizeUsername(body.To)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if to == me {
writeError(w, http.StatusBadRequest, "you can't call yourself")
return
}
jitsiURL := a.defaultJitsiURL
if body.JitsiURL != "" {
if jitsiURL, err = normalizeJitsiURL(body.JitsiURL); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
}
a.store.Touch(me)
inv := a.store.PlaceCall(to, Invite{From: me, MeetingID: newMeetingID(), JitsiURL: jitsiURL})
writeJSON(w, http.StatusOK, map[string]string{"meetingId": inv.MeetingID, "jitsiUrl": jitsiURL})
}
type contact struct {
Username string `json:"username"`
Online bool `json:"online"`
}
func (a *App) handleContacts(w http.ResponseWriter, r *http.Request, me string) {
switch r.Method {
case http.MethodGet:
list := []contact{}
for _, name := range a.contacts.List(me) {
list = append(list, contact{Username: name, Online: a.store.IsOnline(name)})
}
writeJSON(w, http.StatusOK, map[string][]contact{"contacts": list})
case http.MethodPost:
var body struct {
Username string `json:"username"`
}
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
writeError(w, http.StatusBadRequest, "invalid JSON body")
return
}
name, err := normalizeUsername(body.Username)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if name == me {
writeError(w, http.StatusBadRequest, "that's you")
return
}
if err := a.contacts.Add(me, name); err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
writeJSON(w, http.StatusOK, contact{Username: name, Online: a.store.IsOnline(name)})
default:
writeError(w, http.StatusMethodNotAllowed, "GET or POST only")
}
}
func (a *App) handleContact(w http.ResponseWriter, r *http.Request, me string) {
if r.Method != http.MethodDelete {
writeError(w, http.StatusMethodNotAllowed, "DELETE only")
return
}
name, err := normalizeUsername(r.PathValue("username"))
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
if err := a.contacts.Remove(me, name); err != nil {
writeError(w, http.StatusInternalServerError, "could not save contacts")
return
}
writeJSON(w, http.StatusOK, map[string]bool{"ok": true})
}