This commit is contained in:
Vicente Ferrari Smith 2026-05-10 16:42:37 +02:00
parent 334550cef9
commit c143311608
4 changed files with 125 additions and 2 deletions

View File

@ -12,6 +12,7 @@ confirm:
current_time = $(shell date -Iseconds) current_time = $(shell date -Iseconds)
git_description = $(shell git describe --always --dirty) git_description = $(shell git describe --always --dirty)
git_commit_count = $(shell git rev-list --count HEAD)
linker_flags = '-s -X main.buildTime=${current_time} -X main.version=${git_description}' linker_flags = '-s -X main.buildTime=${current_time} -X main.version=${git_description}'
production_host = 152.53.236.243 production_host = 152.53.236.243
@ -46,8 +47,8 @@ build/package:
cp ./deploy/DEBIAN/prerm /tmp/party-pkg/DEBIAN/prerm cp ./deploy/DEBIAN/prerm /tmp/party-pkg/DEBIAN/prerm
cp ./deploy/DEBIAN/postrm /tmp/party-pkg/DEBIAN/postrm cp ./deploy/DEBIAN/postrm /tmp/party-pkg/DEBIAN/postrm
chmod 755 /tmp/party-pkg/DEBIAN/postinst /tmp/party-pkg/DEBIAN/prerm /tmp/party-pkg/DEBIAN/postrm chmod 755 /tmp/party-pkg/DEBIAN/postinst /tmp/party-pkg/DEBIAN/prerm /tmp/party-pkg/DEBIAN/postrm
printf 'Package: party\nVersion: 0+%s\nArchitecture: amd64\nMaintainer: Vicente Ferrari Smith <vicenteferrarismith@gmail.com>\nDescription: Party\n' \ printf 'Package: party\nVersion: 0+%s.%s\nArchitecture: amd64\nMaintainer: Vicente Ferrari Smith <vicenteferrarismith@gmail.com>\nDescription: Party\n' \
'${git_description}' > /tmp/party-pkg/DEBIAN/control '${git_commit_count}' '${git_description}' > /tmp/party-pkg/DEBIAN/control
dpkg-deb --build --root-owner-group /tmp/party-pkg ./party.deb dpkg-deb --build --root-owner-group /tmp/party-pkg ./party.deb
@echo 'Package ready: ./party.deb' @echo 'Package ready: ./party.deb'

View File

@ -2,29 +2,47 @@ package api
import ( import (
"errors" "errors"
"fmt"
"net/http" "net/http"
) )
func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) { func (api *Api) RegisterDeviceToken(w http.ResponseWriter, r *http.Request) {
user := GetUser(r) user := GetUser(r)
api.App.Logger.PrintInfo("register device token: request", map[string]string{
"user_id": fmt.Sprint(user.ID),
})
var input struct { var input struct {
Token string `json:"token"` Token string `json:"token"`
} }
if err := api.readJSON(w, r, &input); err != nil { if err := api.readJSON(w, r, &input); err != nil {
api.App.Logger.PrintInfo("register device token: bad request", map[string]string{
"user_id": fmt.Sprint(user.ID),
"error": err.Error(),
})
api.BadRequestResponse(w, r, err) api.BadRequestResponse(w, r, err)
return return
} }
if input.Token == "" { if input.Token == "" {
api.App.Logger.PrintInfo("register device token: empty token", map[string]string{
"user_id": fmt.Sprint(user.ID),
})
api.BadRequestResponse(w, r, errors.New("token must be provided")) api.BadRequestResponse(w, r, errors.New("token must be provided"))
return return
} }
if err := api.App.Models.DeviceTokens.Insert(user.ID, input.Token); err != nil { if err := api.App.Models.DeviceTokens.Insert(user.ID, input.Token); err != nil {
api.App.Logger.PrintError(err, map[string]string{
"user_id": fmt.Sprint(user.ID),
})
api.ServerErrorResponse(w, r, err) api.ServerErrorResponse(w, r, err)
return return
} }
api.App.Logger.PrintInfo("register device token: ok", map[string]string{
"user_id": fmt.Sprint(user.ID),
})
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} }

View File

@ -45,6 +45,10 @@ func main() {
runCreateAdmin(os.Args[2:]) runCreateAdmin(os.Args[2:])
return return
} }
if len(os.Args) > 1 && os.Args[1] == "send-test-notification" {
runSendTestNotification(os.Args[2:])
return
}
var cfg common.Config var cfg common.Config

View File

@ -0,0 +1,100 @@
package main
import (
"flag"
"fmt"
"os"
apns "github.com/sideshow/apns2"
"github.com/sideshow/apns2/token"
"party.at/party/cmd/party/common"
"party.at/party/internal/data"
)
func runSendTestNotification(args []string) {
fs := flag.NewFlagSet("send-test-notification", flag.ExitOnError)
email := fs.String("email", "", "Send to all device tokens for this user")
deviceToken := fs.String("token", "", "Send to this specific device token")
message := fs.String("message", "Test notification", "Notification body")
fs.Parse(args)
if *email == "" && *deviceToken == "" {
fmt.Fprintln(os.Stderr, "usage: party send-test-notification [-email EMAIL | -token TOKEN] [-message TEXT]")
os.Exit(1)
}
keyPath := os.Getenv("APNS_KEY_PATH")
keyID := os.Getenv("APNS_KEY_ID")
teamID := os.Getenv("APNS_TEAM_ID")
bundleID := os.Getenv("APNS_BUNDLE_ID")
if keyPath == "" || keyID == "" || teamID == "" || bundleID == "" {
fmt.Fprintln(os.Stderr, "missing APNS_KEY_PATH, APNS_KEY_ID, APNS_TEAM_ID or APNS_BUNDLE_ID")
os.Exit(1)
}
authKey, err := token.AuthKeyFromFile(keyPath)
if err != nil {
fmt.Fprintf(os.Stderr, "apns key: %v\n", err)
os.Exit(1)
}
client := apns.NewTokenClient(&token.Token{
AuthKey: authKey,
KeyID: keyID,
TeamID: teamID,
}).Production()
var targets []string
if *deviceToken != "" {
targets = []string{*deviceToken}
} else {
var cfg common.Config
cfg.DB.DSN = os.Getenv("PARTY_DB_DSN")
cfg.DB.MaxOpenConns = 5
cfg.DB.MaxIdleConns = 5
cfg.DB.MaxIdleTime = "15m"
db, err := openDB(cfg)
if err != nil {
fmt.Fprintf(os.Stderr, "db: %v\n", err)
os.Exit(1)
}
defer db.Close()
models := data.NewModels(db)
user, err := models.Users.GetByEmail(*email)
if err != nil {
fmt.Fprintf(os.Stderr, "user: %v\n", err)
os.Exit(1)
}
deviceTokens, err := models.DeviceTokens.GetForUser(user.ID)
if err != nil {
fmt.Fprintf(os.Stderr, "device tokens: %v\n", err)
os.Exit(1)
}
if len(deviceTokens) == 0 {
fmt.Fprintln(os.Stderr, "no device tokens registered for this user")
os.Exit(1)
}
for _, dt := range deviceTokens {
targets = append(targets, dt.Token)
}
}
payload := fmt.Sprintf(`{"aps":{"alert":%q,"sound":"default"}}`, *message)
for _, t := range targets {
notification := &apns.Notification{
DeviceToken: t,
Topic: bundleID,
Payload: []byte(payload),
}
resp, err := client.Push(notification)
if err != nil {
fmt.Fprintf(os.Stderr, "push failed for %s: %v\n", t, err)
continue
}
fmt.Printf("token=%s status=%d apns_id=%s\n", t, resp.StatusCode, resp.ApnsID)
}
}