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
6 changes: 4 additions & 2 deletions docs/v3-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6068,8 +6068,10 @@ components:
type: string
description: FCMのデバイストークン
example: "bk3RNwTe3H0:CI2k_HHwgIpoDKCIZvvDMExUdFQ3P1"
required:
- token
fid:
type: string
description: FCMのInstallation ID
example: "cA7kP2mV9xQn4Lw8RjT0sB"
PostUserRequest:
title: PostUserRequest
type: object
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ toolchain go1.26.5

require (
cloud.google.com/go/profiler v0.6.0
firebase.google.com/go/v4 v4.20.0
firebase.google.com/go/v4 v4.21.0
github.com/MicahParks/jwkset v0.11.3
github.com/NYTimes/gziphandler v1.1.1
github.com/aws/aws-sdk-go-v2 v1.43.3
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
firebase.google.com/go/v4 v4.20.0 h1:ighpjeAC45rY/95cUQ+ojIKlKcTnz2YC0ldam56z2YU=
firebase.google.com/go/v4 v4.20.0/go.mod h1:hqhkQtZkThGH42TnaYi7A8EFR1E0FEuB5oHvJ1Q57t8=
firebase.google.com/go/v4 v4.21.0 h1:HBZV4jrLtFYj8EwWyqEZOuRLfkfkV2bpnfyyXHOhPxY=
firebase.google.com/go/v4 v4.21.0/go.mod h1:CDumIdA5oTiyDpLNVcQoW8ZrB5CTgyE2D45DuENIABg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
Expand Down
87 changes: 44 additions & 43 deletions migration/current.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,50 @@ import (
// 新たなマイグレーションを行う場合は、この配列の末尾に必ず追加すること
func Migrations() []*gormigrate.Migration {
return []*gormigrate.Migration{
v1(), // インデックスidx_messages_deleted_atの削除とidx_messages_channel_id_deleted_at_created_atの追加
v2(), // RBAC周りのリフォーム
v3(), // チャンネルイベント履歴
v4(), // Webhook, Bot外部キー
v5(), // Mute, 旧Clip削除
v6(), // ユーザーグループ拡張
v7(), // ファイルメタ拡張
v8(), // チャンネル購読拡張
v9(), // ユーザーテーブル拡張
v10(), // パーミッション周りの調整
v11(), // クリップ機能の追加
v12(), // カスタムスタンプパレットの追加
v13(), // パーミッション調整・インデックス付与
v14(), // パーミッション不足修正
v15(), // 外部ログイン機能追加
v16(), // パーミッション修正
v17(), // ユーザーホームチャンネル
v18(), // インデックス追加
v19(), // httpセッション管理テーブル変更
v20(), // パーミッション周りの調整
v21(), // OGPキャッシュ追加
v22(), // BOTへのWebRTCパーミッションの付与
v23(), // 複合インデックス追加
v24(), // ユーザー設定追加
v25(), // FileMetaにIsAnimatedImageを追加
v26(), // FileMetaからThumbnail情報を分離
v27(), // Gorm v2移行: FKの追加、FKのリネーム、一部フィールドのデータ型変更、idx_messages_channel_idの削除
v28(), // ユーザーグループにアイコンを追加
v29(), // BotにModeを追加、WebSocket Modeを追加
v30(), // bot_event_logsにresultを追加
v31(), // お気に入りスタンプパーミッション削除(削除忘れ)
v32(), // ユーザーの表示名上限を32文字に
v33(), // 未読テーブルにチャンネルIDカラムを追加 / インデックス類の更新 / 不要なレコードの削除
v34(), // 未読テーブルのcreated_atカラムをメッセージテーブルを元に更新 / カラム名を変更
v35(), // OIDC実装のため、openid, profileロール、get_oidc_userinfo権限を追加
v36(), // delete_my_stampパーミッションを追加
v37(), // サウンドボードアイテム追加
v38(), // v37で作ったサウンドボードアイテムのテーブル名変更
v39(), // OAuth Client Credentials Grantの対応のため、clientロールを追加
v40(), // delete_my_stampパーミッションを削除
v41(), // ユーザーグループ名受付規則変更に伴う既存ユーザーグループ名の更新
v42(), // get_my_stamp_recommendationsパーミッションの追加とmessages_stampsテーブルへの (user_id, updated_at) の複合インデックスの追加
v43(), // messages_stampsテーブルのインデックス (user_id, updated_at) を (user_id, updated_at, stamp_id) に変更
v1(), // インデックスidx_messages_deleted_atの削除とidx_messages_channel_id_deleted_at_created_atの追加
v2(), // RBAC周りのリフォーム
v3(), // チャンネルイベント履歴
v4(), // Webhook, Bot外部キー
v5(), // Mute, 旧Clip削除
v6(), // ユーザーグループ拡張
v7(), // ファイルメタ拡張
v8(), // チャンネル購読拡張
v9(), // ユーザーテーブル拡張
v10(), // パーミッション周りの調整
v11(), // クリップ機能の追加
v12(), // カスタムスタンプパレットの追加
v13(), // パーミッション調整・インデックス付与
v14(), // パーミッション不足修正
v15(), // 外部ログイン機能追加
v16(), // パーミッション修正
v17(), // ユーザーホームチャンネル
v18(), // インデックス追加
v19(), // httpセッション管理テーブル変更
v20(), // パーミッション周りの調整
v21(), // OGPキャッシュ追加
v22(), // BOTへのWebRTCパーミッションの付与
v23(), // 複合インデックス追加
v24(), // ユーザー設定追加
v25(), // FileMetaにIsAnimatedImageを追加
v26(), // FileMetaからThumbnail情報を分離
v27(), // Gorm v2移行: FKの追加、FKのリネーム、一部フィールドのデータ型変更、idx_messages_channel_idの削除
v28(), // ユーザーグループにアイコンを追加
v29(), // BotにModeを追加、WebSocket Modeを追加
v30(), // bot_event_logsにresultを追加
v31(), // お気に入りスタンプパーミッション削除(削除忘れ)
v32(), // ユーザーの表示名上限を32文字に
v33(), // 未読テーブルにチャンネルIDカラムを追加 / インデックス類の更新 / 不要なレコードの削除
v34(), // 未読テーブルのcreated_atカラムをメッセージテーブルを元に更新 / カラム名を変更
v35(), // OIDC実装のため、openid, profileロール、get_oidc_userinfo権限を追加
v36(), // delete_my_stampパーミッションを追加
v37(), // サウンドボードアイテム追加
v38(), // v37で作ったサウンドボードアイテムのテーブル名変更
v39(), // OAuth Client Credentials Grantの対応のため、clientロールを追加
v40(), // delete_my_stampパーミッションを削除
v41(), // ユーザーグループ名受付規則変更に伴う既存ユーザーグループ名の更新
v42(), // get_my_stamp_recommendationsパーミッションの追加とmessages_stampsテーブルへの (user_id, updated_at) の複合インデックスの追加
v43(), // messages_stampsテーブルのインデックス (user_id, updated_at) を (user_id, updated_at, stamp_id) に変更
v43_1(), // Firebase Installation ID に対応するため、TokenType カラムを追加
}
}

Expand Down
36 changes: 36 additions & 0 deletions migration/v43-1.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package migration

import (
"time"

"github.com/go-gormigrate/gormigrate/v2"
"github.com/gofrs/uuid"
"github.com/traPtitech/traQ/model"
"gorm.io/gorm"
)

// v43-1 Added TokenType column in order to handle the Firebase Installation ID
func v43_1() *gormigrate.Migration {
return &gormigrate.Migration{
ID: "43-1",
Migrate: func(db *gorm.DB) error {
if err := db.Exec("ALTER TABLE devices DROP PRIMARY KEY, ADD COLUMN token_type VARCHAR(16) DEFAULT 'token' NOT NULL AFTER token, ADD PRIMARY KEY (token, token_type)").Error; err != nil {
return err
}
return db.AutoMigrate(&v43_1Device{})
},
}
}

type v43_1Device struct {
Token string `gorm:"type:varchar(190);not null;primaryKey"`
TokenType model.DeviceTokenType `gorm:"type:varchar(16);not null;primaryKey"`
UserID uuid.UUID `gorm:"type:char(36);not null;index"`
CreatedAt time.Time `gorm:"precision:6"`

User *model.User `gorm:"constraint:devices_user_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE"`
}

func (*v43_1Device) TableName() string {
return "devices"
}
14 changes: 11 additions & 3 deletions model/devices.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,23 @@ import (
"github.com/gofrs/uuid"
)

type DeviceTokenType string

// Device 通知デバイスの構造体
type Device struct {
Token string `gorm:"type:varchar(190);not null;primaryKey"`
UserID uuid.UUID `gorm:"type:char(36);not null;index"`
CreatedAt time.Time `gorm:"precision:6"`
Token string `gorm:"type:varchar(190);not null;primaryKey"`
TokenType DeviceTokenType `gorm:"type:varchar(16);not null;primaryKey"`
UserID uuid.UUID `gorm:"type:char(36);not null;index"`
CreatedAt time.Time `gorm:"precision:6"`

User *User `gorm:"constraint:devices_user_id_users_id_foreign,OnUpdate:CASCADE,OnDelete:CASCADE"`
}

const (
DeviceTokenTypeToken DeviceTokenType = "token" // レガシーな Token
DeviceTokenTypeFID DeviceTokenType = "fid" // Firebase Installation ID
)

// TableName Device構造体のテーブル名
func (*Device) TableName() string {
return "devices"
Expand Down
15 changes: 13 additions & 2 deletions repository/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,20 @@ import (

"github.com/gofrs/uuid"

"github.com/traPtitech/traQ/model"
"github.com/traPtitech/traQ/utils/optional"
"github.com/traPtitech/traQ/utils/set"
)

type RegisterDeviceArgs struct {
Token optional.Of[string]
FID optional.Of[string]
}
type TokenEntry struct {
Token string
TokenType model.DeviceTokenType
}

// DeviceRepository FCMデバイスリポジトリ
type DeviceRepository interface {
// RegisterDevice FCMデバイスを登録します
Expand All @@ -17,12 +28,12 @@ type DeviceRepository interface {
// tokenが空文字列の場合、ArgumentErrorを返します。
// 登録しようとしたトークンが既に他のユーザーと関連づけられていた場合はArgumentErrorを返します。
// DBによるエラーを返すことがあります。
RegisterDevice(ctx context.Context, userID uuid.UUID, token string) error
RegisterDevice(ctx context.Context, userID uuid.UUID, args RegisterDeviceArgs) error
// GetDeviceTokens 指定したユーザーの全デバイストークンを取得します
//
// 成功した場合、デバイストークンの配列とnilを返します。
// DBによるエラーを返すことがあります。
GetDeviceTokens(ctx context.Context, userIDs set.UUID) (map[uuid.UUID][]string, error)
GetDeviceTokens(ctx context.Context, userIDs set.UUID) (map[uuid.UUID][]TokenEntry, error)
// DeleteDeviceTokens FCMデバイスの登録を解除します
//
// 成功した、或いは既に登録解除されていた場合にnilを返します。
Expand Down
34 changes: 25 additions & 9 deletions repository/gorm/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,32 @@ import (
)

// RegisterDevice implements DeviceRepository interface.
func (repo *Repository) RegisterDevice(ctx context.Context, userID uuid.UUID, token string) error {
func (repo *Repository) RegisterDevice(ctx context.Context, userID uuid.UUID, args repository.RegisterDeviceArgs) error {
if userID == uuid.Nil {
return repository.ErrNilID
}
if len(token) == 0 {
return repository.ArgError("Token", "token is empty")

token := args.FID.V
tokenType := model.DeviceTokenTypeFID
if !args.FID.Valid {
token = args.Token.V
tokenType = model.DeviceTokenTypeToken
if !args.Token.Valid {
return repository.ArgError("Token, FID", "token and fid are empty")
}
}

err := repo.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var d model.Device
if err := tx.First(&d, &model.Device{Token: token}).Error; err == nil {
if len(args.Token.ValueOrZero()) != 0 && len(args.FID.ValueOrZero()) != 0 {
if err := tx.Delete(&model.Device{Token: args.Token.ValueOrZero(), TokenType: model.DeviceTokenTypeToken}).Error; err != nil {
if err != gorm.ErrRecordNotFound {
return err
}
}
}

if err := tx.First(&d, &model.Device{Token: token, TokenType: tokenType}).Error; err == nil {
if d.UserID != userID {
return repository.ArgError("Token", "the Token has already been associated with other user")
}
Expand All @@ -32,23 +47,24 @@ func (repo *Repository) RegisterDevice(ctx context.Context, userID uuid.UUID, to
}

return tx.Create(&model.Device{
Token: token,
UserID: userID,
Token: token,
TokenType: tokenType,
UserID: userID,
}).Error
})
return err
}

// GetDeviceTokens implements DeviceRepository interface.
func (repo *Repository) GetDeviceTokens(ctx context.Context, userIDs set.UUID) (tokens map[uuid.UUID][]string, err error) {
func (repo *Repository) GetDeviceTokens(ctx context.Context, userIDs set.UUID) (tokens map[uuid.UUID][]repository.TokenEntry, err error) {
var tmp []*model.Device
if err := repo.db.WithContext(ctx).Where("user_id IN (?)", userIDs.StringArray()).Find(&tmp).Error; err != nil {
return nil, err
}

tokens = make(map[uuid.UUID][]string, len(userIDs))
tokens = make(map[uuid.UUID][]repository.TokenEntry, len(userIDs))
for _, device := range tmp {
tokens[device.UserID] = append(tokens[device.UserID], device.Token)
tokens[device.UserID] = append(tokens[device.UserID], repository.TokenEntry{Token: device.Token, TokenType: device.TokenType})
}
return tokens, nil
}
Expand Down
Loading
Loading