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
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ require (
github.com/kr/text v0.2.0 // indirect
github.com/mfridman/interpolate v0.0.2 // indirect
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a // indirect
github.com/motoki317/sc v1.8.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sethvargo/go-retry v0.3.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ github.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866
github.com/milvus-io/milvus-proto/go-api/v2 v2.4.10-0.20240819025435-512e3b98866a/go.mod h1:1OIl0v5PQeNxIJhCvY+K55CBUOYDZevw9g9380u1Wek=
github.com/milvus-io/milvus-sdk-go/v2 v2.4.2 h1:Xqf+S7iicElwYoS2Zly8Nf/zKHuZsNy1xQajfdtygVY=
github.com/milvus-io/milvus-sdk-go/v2 v2.4.2/go.mod h1:ulO1YUXKH0PGg50q27grw048GDY9ayB4FPmh7D+FFTA=
github.com/motoki317/sc v1.8.2 h1:JzhmFKl4ZS0VxuRYRBQ07o3DcGdvhn2NwqnUcPJmCjY=
github.com/motoki317/sc v1.8.2/go.mod h1:IwywgSXTlBxHV8a6lHNiQYmTBh7Dc4f9KjzXVdl8/Bk=
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/openai/openai-go/v2 v2.7.1 h1:/tfvTJhfv7hTSL8mWwc5VL4WLLSDL5yn9VqVykdu9r8=
Expand Down
25 changes: 25 additions & 0 deletions internal/bot/channel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package bot

import (
"context"
"time"

"github.com/motoki317/sc"
)

func getChannelPathInternal(ctx context.Context, channelID string) (string, error) {
bot := GetBot()

path, _, err := bot.API().ChannelAPI.GetChannelPath(ctx, channelID).Execute()
if err != nil {
return "", err
}

return path.Path, nil
}

var channelPathCache = sc.NewMust(getChannelPathInternal, time.Hour, time.Hour)

func GetChannelPath(channelID string) (string, error) {
return channelPathCache.Get(context.Background(), channelID)
}
Comment on lines +23 to +25
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verification script from review comment
set -euo pipefail

echo "=== Checking context.Background() usage ==="
rg -n 'context\.Background\(\)' internal/bot/channel.go internal/bot/user.go -C2 2>/dev/null || echo "No matches in specified files"

echo -e "\n=== Checking GetChannelPath function definition ==="
rg -n 'func\s+GetChannelPath\s*\(' internal/bot/channel.go -C2 2>/dev/null || echo "No matches found"

echo -e "\n=== Checking all GetChannelPath call sites ==="
rg -n '\bGetChannelPath\s*\(' --type=go -C2 2>/dev/null | head -50 || echo "No matches found"

Repository: traPtitech/BOT_GPT

Length of output: 1801


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking isChannelAllowingQuotes call sites ==="
rg -n '\bisChannelAllowingQuotes\s*\(' --type=go -B2 -A2

echo -e "\n=== Checking function signatures that call GetChannelPath or isChannelAllowingQuotes ==="
rg -n 'func.*GetChannelPath|func.*isChannelAllowingQuotes|func.*quotes' --type=go -A1 | head -30

echo -e "\n=== Checking cache.Get method signature ==="
rg -n 'type.*Cache|func.*\.Get\(' --type=go internal/pkg/cache -A2 2>/dev/null || echo "Cache package info not found"

echo -e "\n=== Looking for cache implementation ==="
fd -t f '.*cache.*' --type=go 2>/dev/null || echo "No cache files found"

Repository: traPtitech/BOT_GPT

Length of output: 1393


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Function containing isChannelAllowingQuotes call at line 86 ==="
sed -n '75,95p' internal/pkg/formatter/quotes.go | cat -n

echo -e "\n=== Full function signature around line 86 ==="
sed -n '70,100p' internal/pkg/formatter/quotes.go | cat -n

echo -e "\n=== Search for cache package in internal/pkg ==="
find internal/pkg -name '*cache*' -o -name '*Cache*' 2>/dev/null

echo -e "\n=== Search for 'sc.' or cache-like pattern ==="
rg -n 'import|sc\.' internal/bot/channel.go | head -20

echo -e "\n=== Check imports in channel.go to find cache package ==="
head -20 internal/bot/channel.go

Repository: traPtitech/BOT_GPT

Length of output: 2410


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Find the function containing line 86 (isChannelAllowingQuotes call) ==="
rg -B50 'isChannelAllowingQuotes\(message.ChannelId\)' internal/pkg/formatter/quotes.go | head -60 | tail -20

echo -e "\n=== Get full function signature ==="
rg -n 'func.*Format.*Quote|^func' internal/pkg/formatter/quotes.go

echo -e "\n=== Check the actual formatter function with context ==="
sed -n '50,75p' internal/pkg/formatter/quotes.go | cat -n

echo -e "\n=== Check motoki317/sc package documentation or usage ==="
rg -n 'sc\.' internal/ -A1 -B1 | head -30

Repository: traPtitech/BOT_GPT

Length of output: 2381


🏁 Script executed:

#!/bin/bash
set -eulo pipefail

echo "=== Find all calls to FormatQuotedMessage ==="
rg -n '\bFormatQuotedMessage\s*\(' --type=go -B2 -A2

echo -e "\n=== Check the function signatures that call FormatQuotedMessage ==="
rg -B20 'FormatQuotedMessage\(' internal/ --type=go | grep -E 'func |ctx|context' | head -20

echo -e "\n=== Check if there's a handler function calling FormatQuotedMessage ==="
rg -n 'Handler|HandleMessage' internal/ --type=go -A3 | head -40

Repository: traPtitech/BOT_GPT

Length of output: 3466


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Check full OnMessageCreated handler signature ==="
head -30 internal/handler/OnMessageCreated.go | cat -n

echo -e "\n=== Check full OnDirectMessageCreated handler signature ==="
head -30 internal/handler/OnDirectMessageCreated.go | cat -n

echo -e "\n=== Check where these handlers are registered/called ==="
rg -n 'MessageReceived|DirectMessageReceived' --type=go -B2 -A2 | grep -E '(main|func|Register|http|Handler)' | head -30

echo -e "\n=== Check if handlers are HTTP handlers or event handlers ==="
rg -n 'type.*Handler|func.*Handler' internal/handler/ --type=go

Repository: traPtitech/BOT_GPT

Length of output: 3610


context.Background() 固定では、外部API呼び出しのタイムアウト制御ができません。

Line 24 の実装は、呼び出し元がタイムアウトを設定しても反映できません。ネットワーク遅延時にハンドラが長時間ブロックされるリスクがあります。

ただし、修正には複数レイヤーの変更が必要です:

  • GetChannelPath(channelID string)GetChannelPath(ctx context.Context, channelID string)
  • isChannelAllowingQuotes(channelID string)isChannelAllowingQuotes(ctx context.Context, channelID string)
  • FormatQuotedMessage(userID, content string)FormatQuotedMessage(ctx context.Context, userID, content string)
  • WebSocket イベントハンドラ(MessageReceivedDirectMessageReceived)にも context を追加

現在のハンドラは WebSocket 接続に基づくイベントコールバックであり、context がないため、この変更は各レイヤーで実施が必要です。

💡 変更の概要
-func GetChannelPath(channelID string) (string, error) {
-	return channelPathCache.Get(context.Background(), channelID)
+func GetChannelPath(ctx context.Context, channelID string) (string, error) {
+	return channelPathCache.Get(ctx, channelID)
}

-func isChannelAllowingQuotes(channelID string) (bool, error) {
-	channelPath, err := bot.GetChannelPath(channelID)
+func isChannelAllowingQuotes(ctx context.Context, channelID string) (bool, error) {
+	channelPath, err := bot.GetChannelPath(ctx, channelID)
}

-func FormatQuotedMessage(userID string, content string) (string, error) {
+func FormatQuotedMessage(ctx context.Context, userID string, content string) (string, error) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/bot/channel.go` around lines 23 - 25, The current GetChannelPath
uses context.Background() which prevents callers from supplying timeouts; change
GetChannelPath(channelID string) to GetChannelPath(ctx context.Context,
channelID string) and use ctx when calling channelPathCache.Get; propagate the
context through isChannelAllowingQuotes(ctx context.Context, channelID string)
and FormatQuotedMessage(ctx context.Context, userID, content string) and update
all callers (including WebSocket event handlers MessageReceived and
DirectMessageReceived) to accept and forward the ctx so timeouts/cancellation
are honored; ensure channelPathCache.Get(ctx, channelID) is used and update any
tests and call sites accordingly.

26 changes: 26 additions & 0 deletions internal/bot/user.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package bot

import (
"context"
"time"

"github.com/motoki317/sc"
"github.com/traPtitech/go-traq"
)

func getUserInternal(ctx context.Context, userID string) (*traq.UserDetail, error) {
bot := GetBot()

user, _, err := bot.API().UserAPI.GetUser(ctx, userID).Execute()
if err != nil {
return nil, err
}

return user, nil
}

var userCache = sc.NewMust(getUserInternal, time.Hour, time.Hour)

func GetUser(userID string) (*traq.UserDetail, error) {
return userCache.Get(context.Background(), userID)
}
Comment on lines +24 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's check the structure and find the relevant files
find . -name "user.go" -o -name "channel.go" | grep -E "(user|channel)\.go"

Repository: traPtitech/BOT_GPT

Length of output: 110


🏁 Script executed:

# Now let's examine the GetUser function in user.go at lines 24-26
cat -n internal/bot/user.go | head -35

Repository: traPtitech/BOT_GPT

Length of output: 754


🏁 Script executed:

# Let's also check the channel.go file to understand the similar pattern
cat -n internal/bot/channel.go | head -35

Repository: traPtitech/BOT_GPT

Length of output: 755


🏁 Script executed:

# Search for all usages of GetUser to understand the impact
rg "GetUser\(" -A 2 -B 2

Repository: traPtitech/BOT_GPT

Length of output: 1135


GetUser は context を非伝播のため、API遅延時の制御ができません。

Line 25で context.Background() が固定されているため、呼び出し元のキャンセルやタイムアウトが機能しません。GetUser(ctx context.Context, userID string) に変更し、上流の context を渡すようにしてください。これにより、内部の getUserInternal() に context が正しく伝播されます。

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/bot/user.go` around lines 24 - 26, Change the GetUser function to
accept and propagate a caller-provided context instead of using
context.Background(): update the signature from GetUser(userID string) to
GetUser(ctx context.Context, userID string), pass that ctx into userCache.Get
and any internal helper like getUserInternal(), and ensure all callers are
adjusted to provide their upstream context so cancellations/timeouts propagate
correctly.

14 changes: 11 additions & 3 deletions internal/handler/OnDirectMessageCreated.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package handler

import (
"log"

"github.com/traPtitech/BOT_GPT/internal/bot"
"github.com/traPtitech/BOT_GPT/internal/pkg/formatter"
"github.com/traPtitech/traq-ws-bot/payload"
"log"
)

func (h *Handler) DirectMessageReceived() func(p *payload.DirectMessageCreated) {
Expand All @@ -16,12 +18,18 @@ func (h *Handler) DirectMessageReceived() func(p *payload.DirectMessageCreated)
return
}

plainTextWithoutMention := bot.RemoveFirstBotID(p.Message.PlainText)
textWithEmbed := formatter.FormatEmbeds(p.Message.Text)
textWithoutMention := bot.RemoveFirstBotID(textWithEmbed)
formattedMessage, err := formatter.FormatQuotedMessage(p.Message.User.ID, textWithoutMention)
if err != nil {
log.Printf("Error formatting quoted message: %v\n", err)
formattedMessage = textWithoutMention
}

if p.Message.User.Name != "pikachu" {
_ = bot.PostMessage(p.Message.ChannelID, "DMではあんまり沢山使わないでね。定期的な`/reset`を忘れない事。")
}

messageReceived(p.Message.Text, plainTextWithoutMention, p.Message.ChannelID)
messageReceived(p.Message.Text, formattedMessage, p.Message.ChannelID)
}
}
14 changes: 11 additions & 3 deletions internal/handler/OnMessageCreated.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package handler

import (
"log"

"github.com/traPtitech/BOT_GPT/internal/bot"
"github.com/traPtitech/BOT_GPT/internal/pkg/formatter"
"github.com/traPtitech/traq-ws-bot/payload"
"log"
)

func (h *Handler) MessageReceived() func(p *payload.MessageCreated) {
Expand All @@ -16,8 +18,14 @@ func (h *Handler) MessageReceived() func(p *payload.MessageCreated) {
return
}

plainTextWithoutMention := bot.RemoveFirstBotID(p.Message.PlainText)
textEmbedFormatted := formatter.FormatEmbeds(p.Message.Text)
textWithoutMention := bot.RemoveFirstBotID(textEmbedFormatted)
formattedMessage, err := formatter.FormatQuotedMessage(p.Message.User.ID, textWithoutMention)
if err != nil {
log.Printf("Error formatting quoted message: %v\n", err)
formattedMessage = textWithoutMention
}

messageReceived(p.Message.Text, plainTextWithoutMention, p.Message.ChannelID)
messageReceived(p.Message.Text, formattedMessage, p.Message.ChannelID)
}
}
9 changes: 9 additions & 0 deletions internal/pkg/formatter/embed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package formatter

import "regexp"

var embedRegex = regexp.MustCompile(`!\{"type":"(\w+?)","raw":"(.+?)","id":"[a-z0-9-]+?"\}`)

func FormatEmbeds(content string) string {
return embedRegex.ReplaceAllString(content, "$2")
}
36 changes: 36 additions & 0 deletions internal/pkg/formatter/embed_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package formatter

import "testing"

func TestFormatEmbeds(t *testing.T) {
tests := []struct {
name string
input string
expected string
}{
{
name: "No embeds",
input: "Hello, world!",
expected: "Hello, world!",
},
{
name: "Single embed",
input: `Hello, !{"type":"channel","raw":"#general","id":"04ad2c18-fdcb-4c43-beef-82e8ba26ac98"}, world`,
expected: "Hello, #general, world",
},
{
name: "Multiple embeds",
input: `!{"type":"user","raw":"@cp20","id":"be77174f-13c5-4464-8b15-7f45b96d5b18"}!{"type":"channel","raw":"#general","id":"04ad2c18-fdcb-4c43-beef-82e8ba26ac98"}`,
expected: "@cp20#general",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := FormatEmbeds(tt.input)
if result != tt.expected {
t.Errorf("FormatEmbeds(%q) = %q; expected %q", tt.input, result, tt.expected)
}
})
}
}
106 changes: 106 additions & 0 deletions internal/pkg/formatter/quotes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package formatter

import (
"regexp"
"strings"
"unicode/utf8"

"github.com/traPtitech/BOT_GPT/internal/bot"
"github.com/traPtitech/go-traq"
)

const quoteRegexStr = `\bhttps://q\.trap\.jp/messages/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\b`

var quoteRegex = regexp.MustCompile(quoteRegexStr)

var allowingPrefixes = []string{"event", "general", "random", "services", "team/SysAd"}

func isChannelAllowingQuotes(channelID string) (bool, error) {
channelPath, err := bot.GetChannelPath(channelID)
if err != nil {
return false, err
}

for _, prefix := range allowingPrefixes {
if strings.HasPrefix(channelPath, prefix) {
return true, nil
Comment on lines +16 to +26
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

許可チャンネル判定が広すぎ、意図しないチャンネルを通す可能性があります。

Line 25 の HasPrefix 判定だと、general / random / event / services が前方一致で通るため、要件外チャンネルまで展開対象になるリスクがあります。
少なくとも event|random|general|services は厳密一致、team/SysAd のみ team/SysAd または team/SysAd/ 配下一致に分けるのが安全です。

🔧 判定ロジックの例
-var allowingPrefixes = []string{"event", "general", "random", "services", "team/SysAd"}
+var exactAllowingChannels = map[string]struct{}{
+	"event":    {},
+	"general":  {},
+	"random":   {},
+	"services": {},
+}

 func isChannelAllowingQuotes(channelID string) (bool, error) {
 	channelPath, err := bot.GetChannelPath(channelID)
 	if err != nil {
 		return false, err
 	}

-	for _, prefix := range allowingPrefixes {
-		if strings.HasPrefix(channelPath, prefix) {
-			return true, nil
-		}
-	}
+	if _, ok := exactAllowingChannels[channelPath]; ok {
+		return true, nil
+	}
+	if channelPath == "team/SysAd" || strings.HasPrefix(channelPath, "team/SysAd/") {
+		return true, nil
+	}

 	return false, nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var allowingPrefixes = []string{"event", "general", "random", "services", "team/SysAd"}
func isChannelAllowingQuotes(channelID string) (bool, error) {
channelPath, err := bot.GetChannelPath(channelID)
if err != nil {
return false, err
}
for _, prefix := range allowingPrefixes {
if strings.HasPrefix(channelPath, prefix) {
return true, nil
var exactAllowingChannels = map[string]struct{}{
"event": {},
"general": {},
"random": {},
"services": {},
}
func isChannelAllowingQuotes(channelID string) (bool, error) {
channelPath, err := bot.GetChannelPath(channelID)
if err != nil {
return false, err
}
if _, ok := exactAllowingChannels[channelPath]; ok {
return true, nil
}
if channelPath == "team/SysAd" || strings.HasPrefix(channelPath, "team/SysAd/") {
return true, nil
}
return false, nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/pkg/formatter/quotes.go` around lines 16 - 26, The current
isChannelAllowingQuotes uses allowingPrefixes and strings.HasPrefix on
channelPath which is too permissive; update isChannelAllowingQuotes (and
allowingPrefixes) so that "event", "general", "random", and "services" are
matched by exact equality with channelPath, while "team/SysAd" is matched either
by exact equality or by HasPrefix on "team/SysAd/"; use bot.GetChannelPath to
obtain channelPath as before and only return true,nil when one of these stricter
conditions is met.

}
}

return false, nil
}

func isUserAllowingQuotes(userID string, messageUserID string) (bool, error) {
if userID == messageUserID {
return true, nil
}

messageUser, err := bot.GetUser(messageUserID)
if err != nil {
return false, err
}

if messageUser.Bot {
return true, nil
}

return false, nil
}

func getQuoteMarkdown(message *traq.Message) (string, error) {
user, err := bot.GetUser(message.UserId)
if err != nil {
return "", err
}

return "> " + user.Name + ":\n> " + strings.ReplaceAll(message.Content, "\n", "\n> "), nil
}

const maxQuoteLength = 10000

func FormatQuotedMessage(userID string, content string) (string, error) {
matches := quoteRegex.FindAllSubmatch([]byte(content), len(content))
messageIDs := make([]string, 0, len(matches))
for _, match := range matches {
if len(match) < 2 {
continue
}
messageID := string(match[1])
messageIDs = append(messageIDs, messageID)
}

var formattedContent strings.Builder
formattedContent.WriteString(quoteRegex.ReplaceAllString(content, ""))

for _, messageID := range messageIDs {
message := bot.GetMessage(messageID)
if message == nil {
continue
}

if utf8.RuneCountInString(message.Content) > maxQuoteLength {
runes := []rune(message.Content)
message.Content = string(runes[:maxQuoteLength]) + "(以下略)"
}

channelAllowed, err := isChannelAllowingQuotes(message.ChannelId)
if err != nil {
return "", err
}
userAllowed, err := isUserAllowingQuotes(userID, message.UserId)
if err != nil {
return "", err
}
if !channelAllowed && !userAllowed {
continue
}

quote, err := getQuoteMarkdown(message)
if err != nil {
return "", err
}
formattedContent.WriteString("\n\n" + quote)
}

return formattedContent.String(), nil
}
Loading