Skip to content
Merged
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
22 changes: 12 additions & 10 deletions internal/summary/collector.go → internal/discord/collector.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package summary
package discord

import (
"context"
Expand All @@ -10,6 +10,8 @@ import (
"time"

"github.com/bwmarrin/discordgo"

"yukan/internal/summary"
)

// Collector fetches recent messages from Discord.
Expand All @@ -26,8 +28,8 @@ const (
)

// Collect gathers messages from the target guild within the configured lookback window.
func (c *Collector) Collect(ctx context.Context, guildID, botID string) (CollectorResult, error) {
var empty CollectorResult
func (c *Collector) Collect(ctx context.Context, guildID, botID string) (summary.CollectorResult, error) {
var empty summary.CollectorResult

if guildID == "" {
return empty, fmt.Errorf("guild id is required")
Expand Down Expand Up @@ -102,7 +104,7 @@ func (c *Collector) Collect(ctx context.Context, guildID, botID string) (Collect
ctx, cancel := context.WithCancel(ctx)
defer cancel()

results := make([]*ChannelDigest, len(channels))
results := make([]*summary.ChannelDigest, len(channels))
var wg sync.WaitGroup
var firstErr error
var errMu sync.Mutex
Expand Down Expand Up @@ -160,7 +162,7 @@ func (c *Collector) Collect(ctx context.Context, guildID, botID string) (Collect
}

var (
digests []ChannelDigest
digests []summary.ChannelDigest
totalCollectedMsgs int
)

Expand All @@ -174,7 +176,7 @@ func (c *Collector) Collect(ctx context.Context, guildID, botID string) (Collect

fallback := fallbackMessage(totalCollectedMsgs)

return CollectorResult{
return summary.CollectorResult{
Digests: digests,
FallbackText: fallback,
TotalMessages: totalCollectedMsgs,
Expand All @@ -189,10 +191,10 @@ func fallbackMessage(total int) string {
return ""
}

func (c *Collector) collectChannel(ctx context.Context, guildID, botID string, cutoff time.Time, ch *discordgo.Channel, parentNames map[string]string, memberNames *nameCache) (*ChannelDigest, bool, error) {
func (c *Collector) collectChannel(ctx context.Context, guildID, botID string, cutoff time.Time, ch *discordgo.Channel, parentNames map[string]string, memberNames *nameCache) (*summary.ChannelDigest, bool, error) {
displayName := channelDisplayName(ch, parentNames)

collected := make([]MessageDigest, 0, c.FetchLimit)
collected := make([]summary.MessageDigest, 0, c.FetchLimit)
beforeID := ""
failedFetch := false

Expand Down Expand Up @@ -223,7 +225,7 @@ func (c *Collector) collectChannel(ctx context.Context, guildID, botID string, c
continue
}

digest := MessageDigest{
digest := summary.MessageDigest{
Author: resolveAuthor(c.Session, guildID, msg, memberNames),
AvatarURL: userAvatarURL(msg.Author),
Content: extractContent(msg),
Expand All @@ -246,7 +248,7 @@ func (c *Collector) collectChannel(ctx context.Context, guildID, botID string, c
return nil, failedFetch, nil
}

return &ChannelDigest{Name: displayName, Messages: collected}, false, nil
return &summary.ChannelDigest{Name: displayName, Messages: collected}, false, nil
}

func channelDisplayName(ch *discordgo.Channel, parentNames map[string]string) string {
Expand Down
96 changes: 96 additions & 0 deletions internal/discord/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package discord

import (
"fmt"
"strings"

"github.com/bwmarrin/discordgo"
)

func sanitizeContent(input string) string {
cleaned := strings.ReplaceAll(input, "\n", " ")
cleaned = strings.ReplaceAll(cleaned, "\r", " ")
cleaned = strings.TrimSpace(cleaned)
if cleaned == "" {
return "(内容なし)"
}
return cleaned
}

func buildMessageLink(guildID, channelID, messageID string) string {
if guildID == "" || channelID == "" || messageID == "" {
return ""
}
return fmt.Sprintf("https://discord.com/channels/%s/%s/%s", guildID, channelID, messageID)
}

func isThreadChannel(channelType discordgo.ChannelType) bool {
switch channelType {
case discordgo.ChannelTypeGuildPublicThread,
discordgo.ChannelTypeGuildPrivateThread,
discordgo.ChannelTypeGuildNewsThread:
return true
default:
return false
}
}

func isThreadParentChannel(channelType discordgo.ChannelType) bool {
switch channelType {
case discordgo.ChannelTypeGuildText,
discordgo.ChannelTypeGuildNews,
discordgo.ChannelTypeGuildForum:
return true
default:
return false
}
}

func isTextChannel(channelType discordgo.ChannelType) bool {
switch channelType {
case discordgo.ChannelTypeGuildText,
discordgo.ChannelTypeGuildNews,
discordgo.ChannelTypeGuildForum,
discordgo.ChannelTypeGuildPublicThread,
discordgo.ChannelTypeGuildPrivateThread,
discordgo.ChannelTypeGuildNewsThread:
return true
default:
return false
}
}

func displayNameFromMember(member *discordgo.Member) string {
if member == nil {
return ""
}
if nick := strings.TrimSpace(member.Nick); nick != "" {
return nick
}
return displayNameFromUser(member.User)
}

func displayNameFromUser(user *discordgo.User) string {
if user == nil {
return ""
}
if global := strings.TrimSpace(user.GlobalName); global != "" {
return global
}
username := strings.TrimSpace(user.Username)
if username == "" {
return ""
}
discriminator := strings.TrimSpace(user.Discriminator)
if discriminator != "" && discriminator != "0" {
return fmt.Sprintf("%s#%s", username, discriminator)
}
return username
}

func userAvatarURL(user *discordgo.User) string {
if user == nil {
return ""
}
return user.AvatarURL("")
}
73 changes: 73 additions & 0 deletions internal/discord/helpers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package discord

import (
"testing"

"github.com/bwmarrin/discordgo"
)

func TestSanitizeContent(t *testing.T) {
tests := []struct {
name string
input string
want string
}{
{"plain", "hello", "hello"},
{"newlines", "a\nb\r\nc", "a b c"},
{"whitespace only", " \n ", "(内容なし)"},
{"empty", "", "(内容なし)"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := sanitizeContent(tt.input); got != tt.want {
t.Errorf("sanitizeContent(%q) = %q, want %q", tt.input, got, tt.want)
}
})
}
}

func TestBuildMessageLink(t *testing.T) {
if got := buildMessageLink("g", "c", "m"); got != "https://discord.com/channels/g/c/m" {
t.Errorf("buildMessageLink = %q", got)
}
for _, args := range [][3]string{{"", "c", "m"}, {"g", "", "m"}, {"g", "c", ""}} {
if got := buildMessageLink(args[0], args[1], args[2]); got != "" {
t.Errorf("buildMessageLink(%v) = %q, want empty", args, got)
}
}
}

func TestDisplayNameFromUser(t *testing.T) {
tests := []struct {
name string
user *discordgo.User
want string
}{
{"nil", nil, ""},
{"global name wins", &discordgo.User{GlobalName: "グローバル", Username: "user1"}, "グローバル"},
{"username without discriminator", &discordgo.User{Username: "user1", Discriminator: "0"}, "user1"},
{"legacy discriminator", &discordgo.User{Username: "user1", Discriminator: "1234"}, "user1#1234"},
{"empty", &discordgo.User{}, ""},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := displayNameFromUser(tt.user); got != tt.want {
t.Errorf("displayNameFromUser = %q, want %q", got, tt.want)
}
})
}
}

func TestDisplayNameFromMember(t *testing.T) {
member := &discordgo.Member{Nick: "ニック", User: &discordgo.User{GlobalName: "グローバル"}}
if got := displayNameFromMember(member); got != "ニック" {
t.Errorf("nick should win, got %q", got)
}
member = &discordgo.Member{User: &discordgo.User{GlobalName: "グローバル"}}
if got := displayNameFromMember(member); got != "グローバル" {
t.Errorf("fallback to user, got %q", got)
}
if got := displayNameFromMember(nil); got != "" {
t.Errorf("nil member should return empty, got %q", got)
}
}
92 changes: 0 additions & 92 deletions internal/summary/helpers.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package summary

import (
"fmt"
"strings"
"unicode/utf8"

"github.com/bwmarrin/discordgo"
)

func truncateRunes(input string, limit int) string {
Expand All @@ -17,91 +13,3 @@ func truncateRunes(input string, limit int) string {
}
return string([]rune(input)[:limit]) + "…"
}

func sanitizeContent(input string) string {
cleaned := strings.ReplaceAll(input, "\n", " ")
cleaned = strings.ReplaceAll(cleaned, "\r", " ")
cleaned = strings.TrimSpace(cleaned)
if cleaned == "" {
return "(内容なし)"
}
return cleaned
}

func buildMessageLink(guildID, channelID, messageID string) string {
if guildID == "" || channelID == "" || messageID == "" {
return ""
}
return fmt.Sprintf("https://discord.com/channels/%s/%s/%s", guildID, channelID, messageID)
}

func isThreadChannel(channelType discordgo.ChannelType) bool {
switch channelType {
case discordgo.ChannelTypeGuildPublicThread,
discordgo.ChannelTypeGuildPrivateThread,
discordgo.ChannelTypeGuildNewsThread:
return true
default:
return false
}
}

func isThreadParentChannel(channelType discordgo.ChannelType) bool {
switch channelType {
case discordgo.ChannelTypeGuildText,
discordgo.ChannelTypeGuildNews,
discordgo.ChannelTypeGuildForum:
return true
default:
return false
}
}

func isTextChannel(channelType discordgo.ChannelType) bool {
switch channelType {
case discordgo.ChannelTypeGuildText,
discordgo.ChannelTypeGuildNews,
discordgo.ChannelTypeGuildForum,
discordgo.ChannelTypeGuildPublicThread,
discordgo.ChannelTypeGuildPrivateThread,
discordgo.ChannelTypeGuildNewsThread:
return true
default:
return false
}
}

func displayNameFromMember(member *discordgo.Member) string {
if member == nil {
return ""
}
if nick := strings.TrimSpace(member.Nick); nick != "" {
return nick
}
return displayNameFromUser(member.User)
}

func displayNameFromUser(user *discordgo.User) string {
if user == nil {
return ""
}
if global := strings.TrimSpace(user.GlobalName); global != "" {
return global
}
username := strings.TrimSpace(user.Username)
if username == "" {
return ""
}
discriminator := strings.TrimSpace(user.Discriminator)
if discriminator != "" && discriminator != "0" {
return fmt.Sprintf("%s#%s", username, discriminator)
}
return username
}

func userAvatarURL(user *discordgo.User) string {
if user == nil {
return ""
}
return user.AvatarURL("")
}
Loading