Files
Erupe/server/channelserver/repo_guild.go
Houmgaor d640bec8af refactor(channelserver): extract StampRepository, DistributionRepository, and SessionRepository
Eliminate 18 direct s.server.db calls from handlers_items.go,
handlers_distitem.go, and handlers_session.go by moving queries into
dedicated repository types.

New repositories:
- StampRepository (7 methods, stamps table)
- DistributionRepository (4 methods, distribution/distribution_items)
- SessionRepository (4 methods, sign_sessions/servers)

Also adds ClearTreasureHunt and InsertKillLog to GuildRepository,
which already owns those tables for read operations.
2026-02-21 00:06:23 +01:00

923 lines
29 KiB
Go

package channelserver
import (
"database/sql"
"errors"
"fmt"
"time"
"erupe-ce/common/stringsupport"
"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,
(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 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 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 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.Begin()
if err != nil {
return 0, err
}
var guildID int32
err = tx.QueryRow(
"INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id",
guildName, leaderCharID,
).Scan(&guildID)
if err != nil {
_ = tx.Rollback()
return 0, err
}
_, err = tx.Exec(`INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)`, guildID, leaderCharID)
if err != nil {
_ = tx.Rollback()
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.Begin()
if err != nil {
return err
}
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 {
_ = tx.Rollback()
return err
}
}
if _, err := tx.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guildID); err != nil {
_ = tx.Rollback()
return err
}
if _, err := tx.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guildID); err != nil {
_ = tx.Rollback()
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.Begin()
if err != nil {
return err
}
if _, err := tx.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID); err != nil {
_ = tx.Rollback()
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 {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
// CreateApplication inserts a guild application or invitation.
// If tx is non-nil, the operation participates in the given transaction.
func (r *GuildRepository) CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType, tx *sql.Tx) error {
query := `INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) VALUES ($1, $2, $3, $4)`
if tx != nil {
_, err := tx.Exec(query, guildID, charID, actorID, appType)
return err
}
_, err := r.db.Exec(query, guildID, charID, actorID, appType)
return err
}
// 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.Begin()
if err != nil {
return err
}
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 {
_ = tx.Rollback()
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 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 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
}
// AddMemberDailyRP adds RP to a member's daily total.
func (r *GuildRepository) AddMemberDailyRP(charID uint32, amount uint16) error {
_, err := r.db.Exec(`UPDATE guild_characters SET rp_today=rp_today+$1 WHERE character_id=$2`, amount, charID)
return err
}
// ExchangeEventRP subtracts RP from a guild's event pool and returns the new balance.
func (r *GuildRepository) ExchangeEventRP(guildID uint32, amount uint16) (uint32, error) {
var balance uint32
err := r.db.QueryRow(`UPDATE guilds SET event_rp=event_rp-$1 WHERE id=$2 RETURNING event_rp`, amount, guildID).Scan(&balance)
return balance, err
}
// AddRankRP adds RP to a guild's rank total.
func (r *GuildRepository) AddRankRP(guildID uint32, amount uint16) error {
_, err := r.db.Exec(`UPDATE guilds SET rank_rp = rank_rp + $1 WHERE id = $2`, amount, guildID)
return err
}
// AddEventRP adds RP to a guild's event total.
func (r *GuildRepository) AddEventRP(guildID uint32, amount uint16) error {
_, err := r.db.Exec(`UPDATE guilds SET event_rp = event_rp + $1 WHERE id = $2`, amount, guildID)
return err
}
// GetRoomRP returns the current room RP for a guild.
func (r *GuildRepository) GetRoomRP(guildID uint32) (uint16, error) {
var rp uint16
err := r.db.QueryRow(`SELECT room_rp FROM guilds WHERE id = $1`, guildID).Scan(&rp)
return rp, err
}
// SetRoomRP sets the room RP for a guild.
func (r *GuildRepository) SetRoomRP(guildID uint32, rp uint16) error {
_, err := r.db.Exec(`UPDATE guilds SET room_rp = $1 WHERE id = $2`, rp, guildID)
return err
}
// AddRoomRP atomically adds RP to a guild's room total.
func (r *GuildRepository) AddRoomRP(guildID uint32, amount uint16) error {
_, err := r.db.Exec(`UPDATE guilds SET room_rp = room_rp + $1 WHERE id = $2`, amount, guildID)
return err
}
// SetRoomExpiry sets the room expiry time for a guild.
func (r *GuildRepository) SetRoomExpiry(guildID uint32, expiry time.Time) error {
_, err := r.db.Exec(`UPDATE guilds SET room_expiry = $1 WHERE id = $2`, expiry, guildID)
return err
}
// --- Guild Posts ---
// ListPosts returns active guild posts of the given type, ordered by newest first.
func (r *GuildRepository) ListPosts(guildID uint32, postType int) ([]*MessageBoardPost, error) {
rows, err := r.db.Queryx(
`SELECT id, stamp_id, title, body, author_id, created_at, liked_by
FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
ORDER BY created_at DESC`, guildID, postType)
if err != nil {
return nil, err
}
defer rows.Close()
var posts []*MessageBoardPost
for rows.Next() {
post := &MessageBoardPost{}
if err := rows.StructScan(post); err != nil {
continue
}
posts = append(posts, post)
}
return posts, nil
}
// CreatePost inserts a new guild post and soft-deletes excess posts beyond maxPosts.
func (r *GuildRepository) CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error {
if _, err := r.db.Exec(
`INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)`,
guildID, authorID, stampID, postType, title, body); err != nil {
return err
}
_, err := r.db.Exec(`UPDATE guild_posts SET deleted = true WHERE id IN (
SELECT id FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
ORDER BY created_at DESC OFFSET $3
)`, guildID, postType, maxPosts)
return err
}
// DeletePost soft-deletes a guild post by ID.
func (r *GuildRepository) DeletePost(postID uint32) error {
_, err := r.db.Exec("UPDATE guild_posts SET deleted = true WHERE id = $1", postID)
return err
}
// UpdatePost updates the title and body of a guild post.
func (r *GuildRepository) UpdatePost(postID uint32, title, body string) error {
_, err := r.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE id = $3", title, body, postID)
return err
}
// UpdatePostStamp updates the stamp of a guild post.
func (r *GuildRepository) UpdatePostStamp(postID, stampID uint32) error {
_, err := r.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE id = $2", stampID, postID)
return err
}
// GetPostLikedBy returns the liked_by CSV string for a guild post.
func (r *GuildRepository) GetPostLikedBy(postID uint32) (string, error) {
var likedBy string
err := r.db.QueryRow("SELECT liked_by FROM guild_posts WHERE id = $1", postID).Scan(&likedBy)
return likedBy, err
}
// SetPostLikedBy updates the liked_by CSV string for a guild post.
func (r *GuildRepository) SetPostLikedBy(postID uint32, likedBy string) error {
_, err := r.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, postID)
return err
}
// CountNewPosts returns the count of non-deleted posts created after the given time.
func (r *GuildRepository) CountNewPosts(guildID uint32, since time.Time) (int, error) {
var count int
err := r.db.QueryRow(
`SELECT COUNT(*) FROM guild_posts WHERE guild_id = $1 AND deleted = false AND (EXTRACT(epoch FROM created_at)::int) > $2`,
guildID, since.Unix()).Scan(&count)
return count, err
}
// --- Guild Alliances ---
const allianceInfoSelectSQL = `
SELECT
ga.id,
ga.name,
created_at,
parent_id,
CASE
WHEN sub1_id IS NULL THEN 0
ELSE sub1_id
END,
CASE
WHEN sub2_id IS NULL THEN 0
ELSE sub2_id
END
FROM guild_alliances ga
`
// GetAllianceByID loads alliance data including parent and sub guilds.
func (r *GuildRepository) GetAllianceByID(allianceID uint32) (*GuildAlliance, error) {
rows, err := r.db.Queryx(fmt.Sprintf(`%s WHERE ga.id = $1`, allianceInfoSelectSQL), allianceID)
if err != nil {
return nil, err
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
return r.scanAllianceWithGuilds(rows)
}
// ListAlliances returns all alliances with their guild data populated.
func (r *GuildRepository) ListAlliances() ([]*GuildAlliance, error) {
rows, err := r.db.Queryx(allianceInfoSelectSQL)
if err != nil {
return nil, err
}
defer rows.Close()
var alliances []*GuildAlliance
for rows.Next() {
alliance, err := r.scanAllianceWithGuilds(rows)
if err != nil {
continue
}
alliances = append(alliances, alliance)
}
return alliances, nil
}
// CreateAlliance creates a new guild alliance with the given parent guild.
func (r *GuildRepository) CreateAlliance(name string, parentGuildID uint32) error {
_, err := r.db.Exec("INSERT INTO guild_alliances (name, parent_id) VALUES ($1, $2)", name, parentGuildID)
return err
}
// DeleteAlliance removes an alliance by ID.
func (r *GuildRepository) DeleteAlliance(allianceID uint32) error {
_, err := r.db.Exec("DELETE FROM guild_alliances WHERE id=$1", allianceID)
return err
}
// RemoveGuildFromAlliance removes a guild from its alliance, shifting sub2 into sub1's slot if needed.
func (r *GuildRepository) RemoveGuildFromAlliance(allianceID, guildID, subGuild1ID, subGuild2ID uint32) error {
if guildID == subGuild1ID && subGuild2ID > 0 {
_, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = sub2_id, sub2_id = NULL WHERE id = $1`, allianceID)
return err
} else if guildID == subGuild1ID {
_, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = NULL WHERE id = $1`, allianceID)
return err
}
_, err := r.db.Exec(`UPDATE guild_alliances SET sub2_id = NULL WHERE id = $1`, allianceID)
return err
}
// scanAllianceWithGuilds scans an alliance row and populates its guild data.
func (r *GuildRepository) scanAllianceWithGuilds(rows *sqlx.Rows) (*GuildAlliance, error) {
alliance := &GuildAlliance{}
if err := rows.StructScan(alliance); err != nil {
return nil, err
}
parentGuild, err := r.GetByID(alliance.ParentGuildID)
if err != nil {
return nil, err
}
alliance.ParentGuild = *parentGuild
alliance.TotalMembers += parentGuild.MemberCount
if alliance.SubGuild1ID > 0 {
subGuild1, err := r.GetByID(alliance.SubGuild1ID)
if err != nil {
return nil, err
}
alliance.SubGuild1 = *subGuild1
alliance.TotalMembers += subGuild1.MemberCount
}
if alliance.SubGuild2ID > 0 {
subGuild2, err := r.GetByID(alliance.SubGuild2ID)
if err != nil {
return nil, err
}
alliance.SubGuild2 = *subGuild2
alliance.TotalMembers += subGuild2.MemberCount
}
return alliance, nil
}
// --- Guild Adventures ---
// ListAdventures returns all adventures for a guild.
func (r *GuildRepository) ListAdventures(guildID uint32) ([]*GuildAdventure, error) {
rows, err := r.db.Queryx(
"SELECT id, destination, charge, depart, return, collected_by FROM guild_adventures WHERE guild_id = $1", guildID)
if err != nil {
return nil, err
}
defer rows.Close()
var adventures []*GuildAdventure
for rows.Next() {
adv := &GuildAdventure{}
if err := rows.StructScan(adv); err != nil {
continue
}
adventures = append(adventures, adv)
}
return adventures, nil
}
// CreateAdventure inserts a new guild adventure.
func (r *GuildRepository) CreateAdventure(guildID, destination uint32, depart, returnTime int64) error {
_, err := r.db.Exec(
"INSERT INTO guild_adventures (guild_id, destination, depart, return) VALUES ($1, $2, $3, $4)",
guildID, destination, depart, returnTime)
return err
}
// CreateAdventureWithCharge inserts a new guild adventure with an initial charge (Diva variant).
func (r *GuildRepository) CreateAdventureWithCharge(guildID, destination, charge uint32, depart, returnTime int64) error {
_, err := r.db.Exec(
"INSERT INTO guild_adventures (guild_id, destination, charge, depart, return) VALUES ($1, $2, $3, $4, $5)",
guildID, destination, charge, depart, returnTime)
return err
}
// CollectAdventure marks an adventure as collected by the given character (CSV append).
func (r *GuildRepository) CollectAdventure(adventureID uint32, charID uint32) error {
var collectedBy string
err := r.db.QueryRow("SELECT collected_by FROM guild_adventures WHERE id = $1", adventureID).Scan(&collectedBy)
if err != nil {
return err
}
collectedBy = stringsupport.CSVAdd(collectedBy, int(charID))
_, err = r.db.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID)
return err
}
// ChargeAdventure adds charge to a guild adventure.
func (r *GuildRepository) ChargeAdventure(adventureID uint32, amount uint32) error {
_, err := r.db.Exec("UPDATE guild_adventures SET charge = charge + $1 WHERE id = $2", amount, adventureID)
return err
}
// --- Guild Treasure Hunts ---
// GetPendingHunt returns the pending (unacquired) hunt for a character, or nil if none.
func (r *GuildRepository) GetPendingHunt(charID uint32) (*TreasureHunt, error) {
hunt := &TreasureHunt{}
err := r.db.QueryRowx(
`SELECT id, host_id, destination, level, start, hunt_data FROM guild_hunts WHERE host_id=$1 AND acquired=FALSE`,
charID).StructScan(hunt)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return hunt, nil
}
// ListGuildHunts returns acquired level-2 hunts for a guild, with hunter counts and claim status.
func (r *GuildRepository) ListGuildHunts(guildID, charID uint32) ([]*TreasureHunt, error) {
rows, err := r.db.Queryx(`SELECT gh.id, gh.host_id, gh.destination, gh.level, gh.start, gh.collected, gh.hunt_data,
(SELECT COUNT(*) FROM guild_characters gc WHERE gc.treasure_hunt = gh.id AND gc.character_id <> $1) AS hunters,
CASE
WHEN ghc.character_id IS NOT NULL THEN true
ELSE false
END AS claimed
FROM guild_hunts gh
LEFT JOIN guild_hunts_claimed ghc ON gh.id = ghc.hunt_id AND ghc.character_id = $1
WHERE gh.guild_id=$2 AND gh.level=2 AND gh.acquired=TRUE
`, charID, guildID)
if err != nil {
return nil, err
}
defer rows.Close()
var hunts []*TreasureHunt
for rows.Next() {
hunt := &TreasureHunt{}
if err := rows.StructScan(hunt); err != nil {
continue
}
hunts = append(hunts, hunt)
}
return hunts, nil
}
// CreateHunt inserts a new guild treasure hunt.
func (r *GuildRepository) CreateHunt(guildID, hostID, destination, level uint32, huntData []byte, catsUsed string) error {
_, err := r.db.Exec(
`INSERT INTO guild_hunts (guild_id, host_id, destination, level, hunt_data, cats_used) VALUES ($1, $2, $3, $4, $5, $6)`,
guildID, hostID, destination, level, huntData, catsUsed)
return err
}
// AcquireHunt marks a treasure hunt as acquired.
func (r *GuildRepository) AcquireHunt(huntID uint32) error {
_, err := r.db.Exec(`UPDATE guild_hunts SET acquired=true WHERE id=$1`, huntID)
return err
}
// RegisterHuntReport sets a character's active treasure hunt.
func (r *GuildRepository) RegisterHuntReport(huntID, charID uint32) error {
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=$1 WHERE character_id=$2`, huntID, charID)
return err
}
// CollectHunt marks a hunt as collected and clears all characters' treasure_hunt references.
func (r *GuildRepository) CollectHunt(huntID uint32) error {
if _, err := r.db.Exec(`UPDATE guild_hunts SET collected=true WHERE id=$1`, huntID); err != nil {
return err
}
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE treasure_hunt=$1`, huntID)
return err
}
// ClaimHuntReward records that a character has claimed a treasure hunt reward.
func (r *GuildRepository) ClaimHuntReward(huntID, charID uint32) error {
_, err := r.db.Exec(`INSERT INTO guild_hunts_claimed VALUES ($1, $2)`, huntID, charID)
return err
}
// --- Guild Cooking/Meals ---
// ListMeals returns all meals for a guild.
func (r *GuildRepository) ListMeals(guildID uint32) ([]*GuildMeal, error) {
rows, err := r.db.Queryx("SELECT id, meal_id, level, created_at FROM guild_meals WHERE guild_id = $1", guildID)
if err != nil {
return nil, err
}
defer rows.Close()
var meals []*GuildMeal
for rows.Next() {
meal := &GuildMeal{}
if err := rows.StructScan(meal); err != nil {
continue
}
meals = append(meals, meal)
}
return meals, nil
}
// CreateMeal inserts a new guild meal and returns the new ID.
func (r *GuildRepository) CreateMeal(guildID, mealID, level uint32, createdAt time.Time) (uint32, error) {
var id uint32
err := r.db.QueryRow(
"INSERT INTO guild_meals (guild_id, meal_id, level, created_at) VALUES ($1, $2, $3, $4) RETURNING id",
guildID, mealID, level, createdAt).Scan(&id)
return id, err
}
// UpdateMeal updates an existing guild meal's fields.
func (r *GuildRepository) UpdateMeal(mealID, newMealID, level uint32, createdAt time.Time) error {
_, err := r.db.Exec("UPDATE guild_meals SET meal_id = $1, level = $2, created_at = $3 WHERE id = $4",
newMealID, level, createdAt, mealID)
return err
}
// ClaimHuntBox updates the box_claimed timestamp for a guild character.
func (r *GuildRepository) ClaimHuntBox(charID uint32, claimedAt time.Time) error {
_, err := r.db.Exec(`UPDATE guild_characters SET box_claimed=$1 WHERE character_id=$2`, claimedAt, charID)
return err
}
// GuildKill represents a kill log entry for guild hunt data.
type GuildKill struct {
ID uint32 `db:"id"`
Monster uint32 `db:"monster"`
}
// ListGuildKills returns kill log entries for guild members since the character's last box claim.
func (r *GuildRepository) ListGuildKills(guildID, charID uint32) ([]*GuildKill, error) {
rows, err := r.db.Queryx(`SELECT kl.id, kl.monster FROM kill_logs kl
INNER JOIN guild_characters gc ON kl.character_id = gc.character_id
WHERE gc.guild_id=$1
AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2)
`, guildID, charID)
if err != nil {
return nil, err
}
defer rows.Close()
var kills []*GuildKill
for rows.Next() {
kill := &GuildKill{}
if err := rows.StructScan(kill); err != nil {
continue
}
kills = append(kills, kill)
}
return kills, nil
}
// CountGuildKills returns the count of kill log entries for guild members since the character's last box claim.
func (r *GuildRepository) CountGuildKills(guildID, charID uint32) (int, error) {
var count int
err := r.db.QueryRow(`SELECT COUNT(*) FROM kill_logs kl
INNER JOIN guild_characters gc ON kl.character_id = gc.character_id
WHERE gc.guild_id=$1
AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2)
`, guildID, charID).Scan(&count)
return count, err
}
// --- Guild Scouts ---
// 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"`
}
// ClearTreasureHunt clears the treasure_hunt field for a character on logout.
func (r *GuildRepository) ClearTreasureHunt(charID uint32) error {
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE character_id=$1`, charID)
return err
}
// InsertKillLog records a monster kill log entry for a character.
func (r *GuildRepository) InsertKillLog(charID uint32, monster int, quantity uint8, timestamp time.Time) error {
_, err := r.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, charID, monster, quantity, timestamp)
return err
}
// 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 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
}