mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
Replace db.Begin() with db.BeginTxx(context.Background(), nil) across all 8 remaining call sites in repo_guild.go, repo_guild_rp.go, repo_festa.go, and repo_event.go. Use deferred Rollback() instead of explicit rollback at each error return, eliminating 15 manual rollback calls.
467 lines
14 KiB
Go
467 lines
14 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// GuildRepository centralizes all database access for guild-related tables
|
|
// (guilds, guild_characters, guild_applications).
|
|
type GuildRepository struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// NewGuildRepository creates a new GuildRepository.
|
|
func NewGuildRepository(db *sqlx.DB) *GuildRepository {
|
|
return &GuildRepository{db: db}
|
|
}
|
|
|
|
const guildInfoSelectSQL = `
|
|
SELECT
|
|
g.id,
|
|
g.name,
|
|
rank_rp,
|
|
event_rp,
|
|
room_rp,
|
|
COALESCE(room_expiry, '1970-01-01') AS room_expiry,
|
|
main_motto,
|
|
sub_motto,
|
|
created_at,
|
|
leader_id,
|
|
c.name AS leader_name,
|
|
comment,
|
|
COALESCE(pugi_name_1, '') AS pugi_name_1,
|
|
COALESCE(pugi_name_2, '') AS pugi_name_2,
|
|
COALESCE(pugi_name_3, '') AS pugi_name_3,
|
|
pugi_outfit_1,
|
|
pugi_outfit_2,
|
|
pugi_outfit_3,
|
|
pugi_outfits,
|
|
recruiting,
|
|
COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_color,
|
|
COALESCE((SELECT SUM(fs.souls) FROM festa_submissions fs WHERE fs.guild_id=g.id), 0) AS souls,
|
|
COALESCE((
|
|
SELECT id FROM guild_alliances ga WHERE
|
|
ga.parent_id = g.id OR
|
|
ga.sub1_id = g.id OR
|
|
ga.sub2_id = g.id
|
|
), 0) AS alliance_id,
|
|
icon,
|
|
COALESCE(rp_reset_at, '2000-01-01'::timestamptz) AS rp_reset_at,
|
|
(SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count
|
|
FROM guilds g
|
|
JOIN guild_characters gc ON gc.character_id = leader_id
|
|
JOIN characters c on leader_id = c.id
|
|
`
|
|
|
|
const guildMembersSelectSQL = `
|
|
SELECT
|
|
COALESCE(g.id, 0) AS guild_id,
|
|
joined_at,
|
|
COALESCE((SELECT SUM(souls) FROM festa_submissions fs WHERE fs.character_id=c.id), 0) AS souls,
|
|
COALESCE(rp_today, 0) AS rp_today,
|
|
COALESCE(rp_yesterday, 0) AS rp_yesterday,
|
|
c.name,
|
|
c.id AS character_id,
|
|
COALESCE(order_index, 0) AS order_index,
|
|
c.last_login,
|
|
COALESCE(recruiter, false) AS recruiter,
|
|
COALESCE(avoid_leadership, false) AS avoid_leadership,
|
|
c.hr,
|
|
c.gr,
|
|
c.weapon_id,
|
|
c.weapon_type,
|
|
CASE WHEN g.leader_id = c.id THEN true ELSE false END AS is_leader,
|
|
character.is_applicant
|
|
FROM (
|
|
SELECT character_id, true as is_applicant, guild_id
|
|
FROM guild_applications ga
|
|
WHERE ga.application_type = 'applied'
|
|
UNION
|
|
SELECT character_id, false as is_applicant, guild_id
|
|
FROM guild_characters gc
|
|
) character
|
|
JOIN characters c on character.character_id = c.id
|
|
LEFT JOIN guild_characters gc ON gc.character_id = character.character_id
|
|
LEFT JOIN guilds g ON g.id = gc.guild_id
|
|
`
|
|
|
|
func scanGuild(rows *sqlx.Rows) (*Guild, error) {
|
|
guild := &Guild{}
|
|
if err := rows.StructScan(guild); err != nil {
|
|
return nil, err
|
|
}
|
|
return guild, nil
|
|
}
|
|
|
|
func scanGuildMember(rows *sqlx.Rows) (*GuildMember, error) {
|
|
member := &GuildMember{}
|
|
if err := rows.StructScan(member); err != nil {
|
|
return nil, err
|
|
}
|
|
return member, nil
|
|
}
|
|
|
|
// GetByID retrieves guild info by guild ID, returning nil if not found.
|
|
func (r *GuildRepository) GetByID(guildID uint32) (*Guild, error) {
|
|
rows, err := r.db.Queryx(fmt.Sprintf(`%s WHERE g.id = $1 LIMIT 1`, guildInfoSelectSQL), guildID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
if !rows.Next() {
|
|
return nil, nil
|
|
}
|
|
return scanGuild(rows)
|
|
}
|
|
|
|
// GetByCharID retrieves guild info for a character, including applied guilds.
|
|
func (r *GuildRepository) GetByCharID(charID uint32) (*Guild, error) {
|
|
rows, err := r.db.Queryx(fmt.Sprintf(`
|
|
%s
|
|
WHERE EXISTS(
|
|
SELECT 1
|
|
FROM guild_characters gc1
|
|
WHERE gc1.character_id = $1
|
|
AND gc1.guild_id = g.id
|
|
)
|
|
OR EXISTS(
|
|
SELECT 1
|
|
FROM guild_applications ga
|
|
WHERE ga.character_id = $1
|
|
AND ga.guild_id = g.id
|
|
AND ga.application_type = 'applied'
|
|
)
|
|
LIMIT 1
|
|
`, guildInfoSelectSQL), charID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
if !rows.Next() {
|
|
return nil, nil
|
|
}
|
|
return scanGuild(rows)
|
|
}
|
|
|
|
// ListAll returns all guilds. Used for guild enumeration/search.
|
|
func (r *GuildRepository) ListAll() ([]*Guild, error) {
|
|
rows, err := r.db.Queryx(guildInfoSelectSQL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
var guilds []*Guild
|
|
for rows.Next() {
|
|
guild, err := scanGuild(rows)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
guilds = append(guilds, guild)
|
|
}
|
|
return guilds, nil
|
|
}
|
|
|
|
// Create creates a new guild and adds the leader as its first member.
|
|
func (r *GuildRepository) Create(leaderCharID uint32, guildName string) (int32, error) {
|
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
var guildID int32
|
|
err = tx.QueryRow(
|
|
"INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id",
|
|
guildName, leaderCharID,
|
|
).Scan(&guildID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
_, err = tx.Exec(`INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)`, guildID, leaderCharID)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
if err := tx.Commit(); err != nil {
|
|
return 0, err
|
|
}
|
|
return guildID, nil
|
|
}
|
|
|
|
// Save persists guild metadata changes.
|
|
func (r *GuildRepository) Save(guild *Guild) error {
|
|
_, err := r.db.Exec(`
|
|
UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7,
|
|
pugi_outfit_1=$8, pugi_outfit_2=$9, pugi_outfit_3=$10, pugi_outfits=$11, icon=$12, leader_id=$13 WHERE id=$1
|
|
`, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3,
|
|
guild.PugiOutfit1, guild.PugiOutfit2, guild.PugiOutfit3, guild.PugiOutfits, guild.Icon, guild.LeaderCharID)
|
|
return err
|
|
}
|
|
|
|
// Disband removes a guild, its members, and cleans up alliance references.
|
|
func (r *GuildRepository) Disband(guildID uint32) error {
|
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
stmts := []string{
|
|
"DELETE FROM guild_characters WHERE guild_id = $1",
|
|
"DELETE FROM guilds WHERE id = $1",
|
|
"DELETE FROM guild_alliances WHERE parent_id=$1",
|
|
}
|
|
for _, stmt := range stmts {
|
|
if _, err := tx.Exec(stmt, guildID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if _, err := tx.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guildID); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guildID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// RemoveCharacter removes a character from their guild.
|
|
func (r *GuildRepository) RemoveCharacter(charID uint32) error {
|
|
_, err := r.db.Exec("DELETE FROM guild_characters WHERE character_id=$1", charID)
|
|
return err
|
|
}
|
|
|
|
// AcceptApplication deletes the application and adds the character to the guild.
|
|
func (r *GuildRepository) AcceptApplication(guildID, charID uint32) error {
|
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if _, err := tx.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := tx.Exec(`
|
|
INSERT INTO guild_characters (guild_id, character_id, order_index)
|
|
VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1))
|
|
`, guildID, charID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// CreateApplication inserts a guild application or invitation.
|
|
func (r *GuildRepository) CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType) error {
|
|
_, err := r.db.Exec(
|
|
`INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) VALUES ($1, $2, $3, $4)`,
|
|
guildID, charID, actorID, appType)
|
|
return err
|
|
}
|
|
|
|
// CreateApplicationWithMail atomically creates an application and sends a notification mail.
|
|
func (r *GuildRepository) CreateApplicationWithMail(guildID, charID, actorID uint32, appType GuildApplicationType, mailSenderID, mailRecipientID uint32, mailSubject, mailBody string) error {
|
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
if _, err := tx.Exec(
|
|
`INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) VALUES ($1, $2, $3, $4)`,
|
|
guildID, charID, actorID, appType); err != nil {
|
|
return err
|
|
}
|
|
if _, err := tx.Exec(mailInsertQuery, mailSenderID, mailRecipientID, mailSubject, mailBody, 0, 0, true, false); err != nil {
|
|
return err
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
// CancelInvitation removes an invitation for a character.
|
|
func (r *GuildRepository) CancelInvitation(guildID, charID uint32) error {
|
|
_, err := r.db.Exec(
|
|
`DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`,
|
|
charID, guildID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// RejectApplication removes an applied application for a character.
|
|
func (r *GuildRepository) RejectApplication(guildID, charID uint32) error {
|
|
_, err := r.db.Exec(
|
|
`DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'applied'`,
|
|
charID, guildID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// ArrangeCharacters reorders guild members by updating their order_index values.
|
|
func (r *GuildRepository) ArrangeCharacters(charIDs []uint32) error {
|
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
|
|
for i, id := range charIDs {
|
|
if _, err := tx.Exec("UPDATE guild_characters SET order_index = $1 WHERE character_id = $2", 2+i, id); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// GetApplication retrieves a specific application by character, guild, and type.
|
|
// Returns nil, nil if not found.
|
|
func (r *GuildRepository) GetApplication(guildID, charID uint32, appType GuildApplicationType) (*GuildApplication, error) {
|
|
app := &GuildApplication{}
|
|
err := r.db.QueryRowx(`
|
|
SELECT * from guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = $3
|
|
`, charID, guildID, appType).StructScan(app)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return app, nil
|
|
}
|
|
|
|
// HasApplication checks whether any application exists for the character in the guild.
|
|
func (r *GuildRepository) HasApplication(guildID, charID uint32) (bool, error) {
|
|
var n int
|
|
err := r.db.QueryRow(`SELECT 1 from guild_applications WHERE character_id = $1 AND guild_id = $2`, charID, guildID).Scan(&n)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// GetItemBox returns the raw item_box bytes for a guild.
|
|
func (r *GuildRepository) GetItemBox(guildID uint32) ([]byte, error) {
|
|
var data []byte
|
|
err := r.db.QueryRow(`SELECT item_box FROM guilds WHERE id=$1`, guildID).Scan(&data)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil
|
|
}
|
|
return data, err
|
|
}
|
|
|
|
// SaveItemBox writes the serialized item box data for a guild.
|
|
func (r *GuildRepository) SaveItemBox(guildID uint32, data []byte) error {
|
|
_, err := r.db.Exec(`UPDATE guilds SET item_box=$1 WHERE id=$2`, data, guildID)
|
|
return err
|
|
}
|
|
|
|
// GetMembers loads all members (or applicants) of a guild.
|
|
func (r *GuildRepository) GetMembers(guildID uint32, applicants bool) ([]*GuildMember, error) {
|
|
rows, err := r.db.Queryx(fmt.Sprintf(`
|
|
%s
|
|
WHERE character.guild_id = $1 AND is_applicant = $2
|
|
`, guildMembersSelectSQL), guildID, applicants)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
members := make([]*GuildMember, 0)
|
|
for rows.Next() {
|
|
member, err := scanGuildMember(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
members = append(members, member)
|
|
}
|
|
return members, nil
|
|
}
|
|
|
|
// GetCharacterMembership loads a character's guild membership data.
|
|
// Returns nil, nil if the character is not in any guild.
|
|
func (r *GuildRepository) GetCharacterMembership(charID uint32) (*GuildMember, error) {
|
|
rows, err := r.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
|
|
if !rows.Next() {
|
|
return nil, nil
|
|
}
|
|
return scanGuildMember(rows)
|
|
}
|
|
|
|
// SaveMember persists guild member changes (avoid_leadership and order_index).
|
|
func (r *GuildRepository) SaveMember(member *GuildMember) error {
|
|
_, err := r.db.Exec(
|
|
"UPDATE guild_characters SET avoid_leadership=$1, order_index=$2 WHERE character_id=$3",
|
|
member.AvoidLeadership, member.OrderIndex, member.CharID,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// SetRecruiting updates whether a guild is accepting applications.
|
|
func (r *GuildRepository) SetRecruiting(guildID uint32, recruiting bool) error {
|
|
_, err := r.db.Exec("UPDATE guilds SET recruiting=$1 WHERE id=$2", recruiting, guildID)
|
|
return err
|
|
}
|
|
|
|
// SetPugiOutfits updates the unlocked pugi outfit bitmask.
|
|
func (r *GuildRepository) SetPugiOutfits(guildID uint32, outfits uint32) error {
|
|
_, err := r.db.Exec(`UPDATE guilds SET pugi_outfits=$1 WHERE id=$2`, outfits, guildID)
|
|
return err
|
|
}
|
|
|
|
// SetRecruiter updates whether a character has recruiter rights.
|
|
func (r *GuildRepository) SetRecruiter(charID uint32, allowed bool) error {
|
|
_, err := r.db.Exec("UPDATE guild_characters SET recruiter=$1 WHERE character_id=$2", allowed, charID)
|
|
return err
|
|
}
|
|
|
|
// ScoutedCharacter represents an invited character in the scout list.
|
|
type ScoutedCharacter struct {
|
|
CharID uint32 `db:"id"`
|
|
Name string `db:"name"`
|
|
HR uint16 `db:"hr"`
|
|
GR uint16 `db:"gr"`
|
|
ActorID uint32 `db:"actor_id"`
|
|
}
|
|
|
|
// ListInvitedCharacters returns all characters with pending guild invitations.
|
|
func (r *GuildRepository) ListInvitedCharacters(guildID uint32) ([]*ScoutedCharacter, error) {
|
|
rows, err := r.db.Queryx(`
|
|
SELECT c.id, c.name, c.hr, c.gr, ga.actor_id
|
|
FROM guild_applications ga
|
|
JOIN characters c ON c.id = ga.character_id
|
|
WHERE ga.guild_id = $1 AND ga.application_type = 'invited'
|
|
`, guildID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
var chars []*ScoutedCharacter
|
|
for rows.Next() {
|
|
sc := &ScoutedCharacter{}
|
|
if err := rows.StructScan(sc); err != nil {
|
|
continue
|
|
}
|
|
chars = append(chars, sc)
|
|
}
|
|
return chars, nil
|
|
}
|