mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 15:43:49 +01:00
Move ~39 inline SQL queries from six handler files into repo_guild.go, consolidating all guild-related DB access (posts, alliances, adventures, treasure hunts, meals, kill logs, scouts) behind GuildRepository methods. Handler files now contain only packet serialization, business logic, and ACK responses with no direct database calls.
911 lines
29 KiB
Go
911 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"`
|
|
}
|
|
|
|
// 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
|
|
}
|