Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
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
8 changes: 7 additions & 1 deletion .docker/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Start with a base image containing golang runtime
FROM golang:1.20
FROM golang:1.23
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zond do you anticipate any problems with upgrading to go 1.23? It is required for the new FCM package, or some of its dependencies.


# Downloading gcloud package
RUN curl https://dl.google.com/dl/cloudsdk/release/google-cloud-sdk.tar.gz > /tmp/google-cloud-sdk.tar.gz
Expand All @@ -18,6 +18,12 @@ ENV PATH $PATH:/usr/local/gcloud/google-cloud-sdk/bin
# Set the working directory in the container
WORKDIR /go/src/app

# Copy the go.mod and go.sum files
COPY go.mod go.sum ./

# Download the dependencies
RUN go mod download

# Copy the current directory contents into the container at /go/src/app
COPY ../ .

Expand Down
8 changes: 8 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# OAuth Client Id from Google Cloud Project
OAUTH_CLIENT_ID=your-client-id-here

# OAuth Secret from Google Cloud Project
OAUTH_SECRET=your-secret-here

# Server key from FCM
FCM_SERVER_KEY=your-server-key-here
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,5 @@ game/game.debug
# Diff tool files
*.orig

.env
.env
service-account-key.json
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ To enable debugging the JSON output in a browser, adding the query parameter `ac

- Download Docker
- Navigate to the root directory of this project
- Create a `.env` file based on `.env.example` and fill in the required secrets
- Run `docker-compose up`
- The API is now available on your machine at `localhost:8080`
- The Admin server is now available on your machine at `localhost:8000`
- **Note** to get the Discord bot auth to work, you need to send a curl request
after the service is initialized: `curl -XPOST http://localhost:8080/_configure -d '{"DiscordBotCredentials": {"Username": "<username>", "Password": "<password>"}}'`
- The `diplicity-configuration` container will automatically configure the application using the secrets from the `.env` file after a short delay.

## Environment Variables

To configure the application, create a `.env` file in the root directory with the environment variables listed in `.env.example`.

## Running locally

Expand Down
2 changes: 1 addition & 1 deletion auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,7 @@ func (u *User) ID(ctx context.Context) *datastore.Key {
return UserID(ctx, u.Id)
}

func infoToUser(ui *oauth2service.Userinfoplus) *User {
func infoToUser(ui *oauth2service.Userinfo) *User {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had to change this to get the service to start. I suspect it comes from some indirect dependency which was upgraded and the original type no longer exists?

u := &User{
Email: ui.Email,
FamilyName: ui.FamilyName,
Expand Down
17 changes: 15 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,24 @@ services:
- .:/go/src/app
networks:
- diplicity-net
env_file:
- .env
ports:
- "8080:8080"
- "8000:8000"
diplicity-configuration:
image: curlimages/curl:latest
container_name: diplicity-configuration
depends_on:
- diplicity-application
entrypoint: >
sh -c "
sleep 20 &&
curl -XPOST http://diplicity-application:8080/_configure
-d '{\"OAuth\": { \"ClientID\": \"${OAUTH_CLIENT_ID}\", \"Secret\": \"${OAUTH_SECRET}\" }, \"FCMConf\": { \"ServerKey\": \"${FCM_SERVER_KEY}\" }}'
"
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a helper container that comes up and sends the values from .env to the service. Saves having to do it manually every time. There's a few values missing like SendGrid, but I'll add them as I go

env_file:
- .env
networks:
- diplicity-net
networks:
diplicity-net:
name: diplicity-net
Expand Down
23 changes: 3 additions & 20 deletions game/chat.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import (
"strings"
"time"

"firebase.google.com/go/v4/messaging"
"github.com/aymerick/raymond"
"github.com/davecgh/go-spew/spew"
"github.com/kvannotten/mailstrip"
"github.com/zond/diplicity/auth"
"github.com/zond/enmime"
fcm "github.com/zond/go-fcm"
"github.com/zond/godip"
"github.com/zond/godip/variants"
"golang.org/x/net/context"
Expand Down Expand Up @@ -312,12 +312,6 @@ func sendMsgNotificationsToFCM(ctx context.Context, host string, gameID *datasto
return err
}

dataPayload, err := NewFCMData(msgContext.fcmData)
if err != nil {
log.Errorf(ctx, "Unable to encode FCM data payload %v: %v; fix NewFCMData", msgContext.fcmData, err)
return err
}

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've gotten rid of the data payload in notifications for now. I understand that they are used to power the click action on the client side.

I'm not convinced that it is the best approach, I'm going to re-implement the click actions later, and hopefully in a way that is a little more straight-forward. I was struggling a lot with the encoding and decoding of data. Felt like client knew too much about what was being sent to it. I reckon there's an easier way.

if len(msgContext.userConfig.FCMTokens) == 0 {
log.Infof(ctx, "%q hasn't registered any FCM tokens, will skip sending notifiations", userId)
return nil
Expand All @@ -336,24 +330,14 @@ func sendMsgNotificationsToFCM(ctx context.Context, host string, gameID *datasto
if runes := []rune(notificationBody); len(runes) > 512 {
notificationBody = string(runes[:512]) + "..."
}
notificationPayload := &fcm.NotificationPayload{
notificationPayload := &messaging.Notification{
Title: fmt.Sprintf(
"%s: %s => %s",
msgContext.game.DescFor(msgContext.member.Nation),
msgContext.game.AbbrNat(msgContext.message.Sender),
msgContext.game.AbbrNats(msgContext.message.ChannelMembers).String(),
),
Body: notificationBody,
Tag: "diplicity-engine-new-message",
ClickAction: fmt.Sprintf("%s://%s/Game/%s/Channel/%s/Messages", DefaultScheme, host, gameID.Encode(), channelMembers.String()),
}

fcmToken.MessageConfig.Customize(ctx, notificationPayload, msgContext.mailData)
if fcmToken.MessageConfig.DontSendData {
dataPayload = nil
}
if fcmToken.MessageConfig.DontSendNotification {
notificationPayload = nil
Body: notificationBody,
}

if err := datastore.RunInTransaction(ctx, func(ctx context.Context) error {
Expand All @@ -362,7 +346,6 @@ func sendMsgNotificationsToFCM(ctx context.Context, host string, gameID *datasto
0,
time.Duration(0),
notificationPayload,
dataPayload,
map[string][]string{
userId: []string{fcmToken.Value},
},
Expand Down
182 changes: 26 additions & 156 deletions game/fcm.go
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
package game

import (
"bytes"
"compress/zlib"
"encoding/json"
"fmt"
"net/http"
"strconv"
"sync"
"time"

"github.com/zond/diplicity/auth"
"github.com/zond/go-fcm"
"golang.org/x/net/context"
"google.golang.org/appengine/v2/datastore"
"google.golang.org/appengine/v2/log"
"google.golang.org/appengine/v2/urlfetch"

"firebase.google.com/go/v4/messaging"
newFcm "github.com/appleboy/go-fcm"

. "github.com/zond/goaeoas"
)
Expand Down Expand Up @@ -162,35 +159,8 @@ func manageFCMTokens(ctx context.Context, tokensToRemove, tokensToUpdate map[str
return nil
}

type FCMData struct {
DiplicityJSON []byte
}

func NewFCMData(payload interface{}) (*FCMData, error) {
b, err := json.Marshal(payload)
if err != nil {
return nil, err
}
buf := &bytes.Buffer{}
w := zlib.NewWriter(buf)
w.Write(b)
w.Close()
return &FCMData{
DiplicityJSON: buf.Bytes(),
}, nil
}

func nestPut(m1 map[string]map[string]string, k1, k2, v string) {
m2, found := m1[k1]
if !found {
m2 = map[string]string{}
}
m2[k2] = v
m1[k1] = m2
}

func fcmSendToTokens(ctx context.Context, lastDelay time.Duration, notif *fcm.NotificationPayload, data *FCMData, tokens map[string][]string) error {
log.Infof(ctx, "fcmSendToTokens(..., %v, %v, %+v)", PP(notif), PP(data), tokens)
func fcmSendToTokens(ctx context.Context, lastDelay time.Duration, notification *messaging.Notification, tokens map[string][]string) error {
log.Infof(ctx, "fcmSendToTokens called")

tokenStrings := []string{}
userByToken := map[string]string{}
Expand All @@ -210,138 +180,38 @@ func fcmSendToTokens(ctx context.Context, lastDelay time.Duration, notif *fcm.No
return nil
}

fcmConf, err := getFCMConf(ctx)
log.Infof(ctx, "Creating new FCM client...")
newClient, err := newFcm.NewClient(
ctx,
newFcm.WithCredentialsFile("service-account-key.json"),
)
if err != nil {
// Safe to retry, nothing got sent.
log.Errorf(ctx, "Unable to get FCMConf: %v; fix getFCMConf or hope datastore gets fixed", err)
return err
log.Errorf(ctx, "Unable to create FCM client: %v", err)
}
log.Infof(ctx, "New FCM client created!")

client := fcm.NewFcmClient(fcmConf.ServerKey)
client.SetHTTPClient(urlfetch.Client(ctx))
client.AppendDevices(tokenStrings)
if notif != nil {
client.SetNotificationPayload(notif)
}
if data != nil {
client.SetMsgData(data)
msg := &messaging.MulticastMessage{
Tokens: tokenStrings,
Notification: notification,
}

resp, err := client.Send()
log.Infof(ctx, "Sending FCM message...")
newResp, err := newClient.SendMulticast(ctx, msg)
if err != nil {
// Safe to retry, nothing got sent probably.
log.Errorf(ctx, "%v unable to send: %v", PP(client), err)
return err
}

log.Infof(ctx, "Sent %v, received %v, %v in response", PP(client), PP(resp), err)

if resp.StatusCode == 401 {
// Safe to retry, we will just keep delaying incrementally until the auth gets fixed.
msg := fmt.Sprintf("%v unable to send due to 401: %v; fix your authentication", PP(client), PP(resp))
log.Errorf(ctx, msg)
return fmt.Errorf(msg)
}

if resp.StatusCode == 400 {
// Can't retry, our payload is fucked up.
log.Errorf(ctx, "%v unable to send due to 400: %v; unable to recover", PP(client), PP(resp))
return nil
log.Errorf(ctx, "Unable to send FCM message: %v", err)
}
log.Infof(ctx, "%d messages were sent successfully", newResp.SuccessCount)

idsToRetry := tokens
if resp.StatusCode > 199 && resp.StatusCode < 299 {
// Now we have to take care what we retry - retries might lead to duplicates.
idsToUpdate := map[string]map[string]string{}
idsToRemove := map[string]map[string]string{}
idsToRetry = map[string][]string{}

failures := 0
successes := 0
for i, result := range resp.Results {
token := tokenStrings[i]
uid := userByToken[token]
if newID, found := result["registration_id"]; found {
nestPut(idsToUpdate, uid, token, newID)
}
if errMsg, found := result["error"]; found {
switch errMsg {
case "InvalidRegistration":
fallthrough
case "NotRegistered":
fallthrough
case "MismatchSenderId":
log.Warningf(ctx, "Token %q got %q, will remove it.", token, errMsg)
nestPut(idsToRemove, uid, token, errMsg)
case "Unavailable":
// Can be retried, it's supposed to be.
fallthrough
case "InternalServerError":
// Can be retried, it's supposed to be.
log.Errorf(ctx, "Token %q got %q, will retry.", token, errMsg)
idsToRetry[uid] = append(idsToRetry[uid], token)
case "DeviceMessageRateExceeded":
fallthrough
case "TopicsMessageRateExceeded":
fallthrough
case "MissingRegistration":
fallthrough
case "InvalidTtl":
fallthrough
case "InvalidPackageName":
log.Errorf(ctx, "Token %q got %q, wtf?", token, errMsg)
case "InvalidParameters":
fallthrough
case "MessageTooBig":
log.Errorf(ctx, "Token %q got %q, SEND SMALLER MESSAGES DAMNIT!", token, errMsg)
case "InvalidDataKey":
log.Errorf(ctx, "Token %q got %q, SEND CORRECT MESSAGES DAMNIT!", token, errMsg)
default:
log.Errorf(ctx, "Token %q got %q, wtf?", token, errMsg)
}
failures++
} else {
successes++
}
}
if successes != resp.Success {
log.Errorf(ctx, "Reported successes %v != nr of non failure results %v", resp.Success, successes)
}
if failures != resp.Fail {
log.Errorf(ctx, "Reported failures %v != nr of failure results %v", resp.Fail, failures)
}
if len(idsToRemove) > 0 || len(idsToUpdate) > 0 {
if err := manageFCMTokensFunc.EnqueueIn(ctx, 0, idsToRemove, idsToUpdate); err != nil {
log.Errorf(ctx, "Unable to schedule repair of FCM tokens (to remove: %v, to update: %v): %v; hope that datastore gets fixed", PP(idsToRemove), PP(idsToUpdate), err)
if newResp.FailureCount > 0 {
var failedTokens []string
for i, resp := range newResp.Responses {
if !resp.Success {
failedTokens = append(failedTokens, tokenStrings[i])
log.Infof(ctx, "Failed to send message to token %s: %v", tokenStrings[i], resp.Error)
}
}
log.Infof(ctx, "Failed tokens: %v", failedTokens)
}

if len(idsToRetry) > 0 {
if lastDelay < time.Hour*8 {
// Right, we still have something to retry, but might also have a Retry-After header.
// First, assume we just double the old delay (or 1 sec).
delay := lastDelay * 2
if delay < time.Second {
delay = time.Second
}
// Then, try to honor the Retry-After header.
if n, err := strconv.ParseInt(resp.RetryAfter, 10, 64); err == nil {
delay = time.Duration(n) * time.Minute
} else if at, err := time.Parse(time.RFC1123, resp.RetryAfter); err == nil {
delay = at.Sub(time.Now())
}
// Finally, try to schedule again. If we can't then fuckall we'll try again with the entire payload.
if err := FCMSendToTokensFunc.EnqueueIn(ctx, delay, delay, notif, data, idsToRetry); err != nil {
log.Errorf(ctx, "Unable to schedule retry of %v, %v to %+v in %v: %v", PP(notif), PP(data), idsToRetry, delay, err)
return err
}
} else {
log.Errorf(ctx, "Still have %+v to retry, but last delay was %v, so I'm giving up", idsToRetry, lastDelay)
}
}

log.Infof(ctx, "fcmSendToTokens(..., %v, %v, %+v) *** SUCCESS ***", PP(notif), PP(data), tokens)

return nil
}
Loading
Loading