mirror of
https://github.com/diamondburned/cchat-discord.git
synced 2024-11-22 06:02:59 +00:00
WIP tweaks
This commit is contained in:
parent
1155ccac34
commit
6c1a11706f
3
go.mod
3
go.mod
|
@ -4,10 +4,11 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/diamondburned/arikawa v1.3.6
|
||||
github.com/diamondburned/cchat v0.3.12
|
||||
github.com/diamondburned/cchat v0.3.15
|
||||
github.com/diamondburned/ningen v0.2.1-0.20201023061015-ce64ffb0bb12
|
||||
github.com/dustin/go-humanize v1.0.0
|
||||
github.com/go-test/deep v1.0.7
|
||||
github.com/lithammer/fuzzysearch v1.1.1
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||
github.com/yuin/goldmark v1.1.30
|
||||
|
|
9
go.sum
9
go.sum
|
@ -105,6 +105,12 @@ github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJ
|
|||
github.com/diamondburned/cchat v0.3.11/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||
github.com/diamondburned/cchat v0.3.12 h1:mew54lsDrwrJs4U2FtdbNFl/wAZcueIgZCsImHQzVL4=
|
||||
github.com/diamondburned/cchat v0.3.12/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||
github.com/diamondburned/cchat v0.3.13 h1:qSAo8FJDvEi9o8kLQJ5Mbo4jrfb+sd1Muo1sx8ruBo8=
|
||||
github.com/diamondburned/cchat v0.3.13/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||
github.com/diamondburned/cchat v0.3.14 h1:nDr9DJ1EW3kab4gieE+DLhpvHRws+umWpw5XrOmlNyc=
|
||||
github.com/diamondburned/cchat v0.3.14/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||
github.com/diamondburned/cchat v0.3.15 h1:BJf8ZiRtDWTGMtQ3QqjNU0H+784WSrkJEpFGkKY5gEw=
|
||||
github.com/diamondburned/cchat v0.3.15/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249 h1:yP7kJ+xCGpDz6XbcfACJcju4SH1XDPwlrvbofz3lP8I=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
||||
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=
|
||||
|
@ -204,6 +210,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
|
|||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lithammer/fuzzysearch v1.1.1 h1:8F9OAV2xPuYblToVohjanztdnPjbtA0MLgMvDKQ0Z08=
|
||||
github.com/lithammer/fuzzysearch v1.1.1/go.mod h1:H2bng+w5gsR7NlfIJM8ElGZI0sX6C/9uzGqicVXGU6c=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
|
@ -295,6 +303,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
|
|
@ -53,7 +53,7 @@ func (ch Channel) ID() cchat.ID {
|
|||
func (ch Channel) Name() text.Rich {
|
||||
c, err := ch.Self()
|
||||
if err != nil {
|
||||
return text.Rich{Content: ch.Channel.ID.String()}
|
||||
return text.Rich{Content: ch.ID()}
|
||||
}
|
||||
|
||||
if c.NSFW {
|
||||
|
|
|
@ -12,13 +12,13 @@ import (
|
|||
|
||||
type Commander struct {
|
||||
shared.Channel
|
||||
msgCompl complete.Completer
|
||||
msgCompl complete.ChannelCompleter
|
||||
}
|
||||
|
||||
func NewCommander(ch shared.Channel) cchat.Commander {
|
||||
return Commander{
|
||||
Channel: ch,
|
||||
msgCompl: complete.Completer{
|
||||
msgCompl: complete.ChannelCompleter{
|
||||
Channel: ch,
|
||||
},
|
||||
}
|
||||
|
|
|
@ -28,11 +28,11 @@ type Messenger struct {
|
|||
|
||||
var _ cchat.Messenger = (*Messenger)(nil)
|
||||
|
||||
func New(ch shared.Channel) Messenger {
|
||||
return Messenger{Channel: ch}
|
||||
func New(ch shared.Channel) *Messenger {
|
||||
return &Messenger{Channel: ch}
|
||||
}
|
||||
|
||||
func (msgr Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
||||
func (msgr *Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
||||
state := msgr.State.WithContext(ctx)
|
||||
|
||||
m, err := state.Messages(msgr.ID)
|
||||
|
@ -112,7 +112,7 @@ func (msgr Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer
|
|||
addcancel(
|
||||
msgr.State.AddHandler(func(m *gateway.MessageCreateEvent) {
|
||||
if m.ChannelID == msgr.ID {
|
||||
ct.CreateMessage(message.NewMessageCreate(m, msgr.State))
|
||||
ct.CreateMessage(message.NewGuildMessageCreate(m, msgr.State))
|
||||
msgr.State.ReadState.MarkRead(msgr.ID, m.ID)
|
||||
}
|
||||
}),
|
||||
|
@ -132,7 +132,7 @@ func (msgr Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer
|
|||
return funcutil.JoinCancels(addcancel()...), nil
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsSender() cchat.Sender {
|
||||
func (msgr *Messenger) AsSender() cchat.Sender {
|
||||
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
||||
return nil
|
||||
}
|
||||
|
@ -140,7 +140,7 @@ func (msgr Messenger) AsSender() cchat.Sender {
|
|||
return send.New(msgr.Channel)
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsEditor() cchat.Editor {
|
||||
func (msgr *Messenger) AsEditor() cchat.Editor {
|
||||
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
||||
return nil
|
||||
}
|
||||
|
@ -148,22 +148,22 @@ func (msgr Messenger) AsEditor() cchat.Editor {
|
|||
return edit.New(msgr.Channel)
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsActioner() cchat.Actioner {
|
||||
func (msgr *Messenger) AsActioner() cchat.Actioner {
|
||||
return action.New(msgr.Channel)
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsNicknamer() cchat.Nicknamer {
|
||||
func (msgr *Messenger) AsNicknamer() cchat.Nicknamer {
|
||||
return nickname.New(msgr.Channel)
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsMemberLister() cchat.MemberLister {
|
||||
func (msgr *Messenger) AsMemberLister() cchat.MemberLister {
|
||||
if !msgr.GuildID.IsValid() {
|
||||
return nil
|
||||
}
|
||||
return memberlist.New(msgr.Channel)
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsBacklogger() cchat.Backlogger {
|
||||
func (msgr *Messenger) AsBacklogger() cchat.Backlogger {
|
||||
if !msgr.HasPermission(discord.PermissionViewChannel, discord.PermissionReadMessageHistory) {
|
||||
return nil
|
||||
}
|
||||
|
@ -171,10 +171,10 @@ func (msgr Messenger) AsBacklogger() cchat.Backlogger {
|
|||
return backlog.New(msgr.Channel)
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsTypingIndicator() cchat.TypingIndicator {
|
||||
func (msgr *Messenger) AsTypingIndicator() cchat.TypingIndicator {
|
||||
return indicate.NewTyping(msgr.Channel)
|
||||
}
|
||||
|
||||
func (msgr Messenger) AsUnreadIndicator() cchat.UnreadIndicator {
|
||||
func (msgr *Messenger) AsUnreadIndicator() cchat.UnreadIndicator {
|
||||
return indicate.NewUnread(msgr.Channel)
|
||||
}
|
||||
|
|
|
@ -1,52 +1,90 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
func (ch Completer) CompleteChannels(word string) (entries []cchat.CompletionEntry) {
|
||||
func (ch ChannelCompleter) CompleteChannels(word string) []cchat.CompletionEntry {
|
||||
// Ignore if empty word.
|
||||
if word == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Ignore if we're not in a guild.
|
||||
if !ch.GuildID.IsValid() {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
c, err := ch.State.Store.Channels(ch.GuildID)
|
||||
if err != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var match = strings.ToLower(word)
|
||||
return completeChannels(c, word, ch.State)
|
||||
}
|
||||
|
||||
for _, channel := range c {
|
||||
if !contains(match, channel.Name) {
|
||||
func DMChannels(s *state.Instance, word string) []cchat.CompletionEntry {
|
||||
channels, err := s.Store.PrivateChannels()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
// We only need the state to look for categories, which is never the case
|
||||
// for private channels.
|
||||
return completeChannels(channels, word, nil)
|
||||
}
|
||||
|
||||
func rankChannel(word string, ch discord.Channel) int {
|
||||
switch ch.Type {
|
||||
case discord.GroupDM, discord.DirectMessage:
|
||||
return rankFunc(word, ch.Name+" "+shared.PrivateName(ch))
|
||||
default:
|
||||
return rankFunc(word, ch.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func completeChannels(
|
||||
channels []discord.Channel, word string, s *state.Instance) []cchat.CompletionEntry {
|
||||
|
||||
var entries []cchat.CompletionEntry
|
||||
var distances map[string]int
|
||||
|
||||
for _, channel := range channels {
|
||||
rank := rankChannel(word, channel)
|
||||
if rank == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
var category string
|
||||
if channel.CategoryID.IsValid() {
|
||||
if c, _ := ch.State.Store.Channel(channel.CategoryID); c != nil {
|
||||
category = c.Name
|
||||
if s != nil && channel.CategoryID.IsValid() {
|
||||
if cat, _ := s.Store.Channel(channel.CategoryID); cat != nil {
|
||||
category = cat.Name
|
||||
}
|
||||
}
|
||||
|
||||
// Defer allocation until we've found something.
|
||||
ensureEntriesMade(&entries)
|
||||
ensureDistancesMade(&distances)
|
||||
|
||||
raw := channel.Mention()
|
||||
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: channel.Mention(),
|
||||
Text: text.Rich{Content: "#" + channel.Name},
|
||||
Secondary: text.Rich{Content: category},
|
||||
Raw: raw,
|
||||
Text: text.Plain("#" + channel.Name),
|
||||
Secondary: text.Plain(category),
|
||||
})
|
||||
|
||||
distances[raw] = rank
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
return
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
sortDistances(entries, distances)
|
||||
return entries
|
||||
|
||||
}
|
||||
|
|
|
@ -1,20 +1,30 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sort"
|
||||
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||
"github.com/lithammer/fuzzysearch/fuzzy"
|
||||
)
|
||||
|
||||
type Completer struct {
|
||||
type CompleterFunc func(word string) []cchat.CompletionEntry
|
||||
|
||||
type ChannelCompleter struct {
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
type Completer map[byte]CompleterFunc
|
||||
|
||||
const MaxCompletion = 15
|
||||
|
||||
func New(ch shared.Channel) cchat.Completer {
|
||||
return Completer{ch}
|
||||
completer := ChannelCompleter{ch}
|
||||
return Completer{
|
||||
'@': completer.CompleteMentions,
|
||||
'#': completer.CompleteChannels,
|
||||
':': completer.CompleteEmojis,
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteMessage implements message input completion capability for Discord.
|
||||
|
@ -28,24 +38,40 @@ func (ch Completer) Complete(words []string, i int64) []cchat.CompletionEntry {
|
|||
return nil
|
||||
}
|
||||
|
||||
switch word[0] {
|
||||
case '@':
|
||||
return ch.CompleteMentions(word[1:])
|
||||
case '#':
|
||||
return ch.CompleteChannels(word[1:])
|
||||
case ':':
|
||||
return ch.CompleteEmojis(word[1:])
|
||||
fn, ok := ch[word[0]]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
fn(word[1:])
|
||||
return nil
|
||||
}
|
||||
|
||||
func contains(contains string, strs ...string) bool {
|
||||
for _, str := range strs {
|
||||
if strings.Contains(strings.ToLower(str), contains) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
// rankFunc is the default rank function to use.
|
||||
func rankFunc(source, target string) int {
|
||||
return fuzzy.RankMatchNormalizedFold(source, target)
|
||||
}
|
||||
|
||||
func ensureEntriesMade(entries *[]cchat.CompletionEntry) {
|
||||
if *entries == nil {
|
||||
*entries = make([]cchat.CompletionEntry, 0, MaxCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureDistancesMade(distances *map[string]int) {
|
||||
if *distances == nil {
|
||||
*distances = make(map[string]int, MaxCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
// sortDistances sorts according to the given Levenshtein distances from the Raw
|
||||
// string of the entries from most accurate to least accurate.
|
||||
func sortDistances(entries []cchat.CompletionEntry, distances map[string]int) {
|
||||
if len(entries) == 0 {
|
||||
return
|
||||
}
|
||||
// The lower the distance, the more accurate.
|
||||
sort.SliceStable(entries, func(i, j int) bool {
|
||||
return distances[entries[i].Raw] < distances[entries[j].Raw]
|
||||
})
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package complete
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
|
@ -10,43 +8,54 @@ import (
|
|||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
func (ch Completer) CompleteEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||
return CompleteEmojis(ch.State, ch.GuildID, word)
|
||||
func (ch ChannelCompleter) CompleteEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||
return Emojis(ch.State, ch.GuildID, word)
|
||||
}
|
||||
|
||||
func CompleteEmojis(s *state.Instance, gID discord.GuildID, word string) []cchat.CompletionEntry {
|
||||
func Emojis(s *state.Instance, gID discord.GuildID, word string) []cchat.CompletionEntry {
|
||||
// Ignore if empty word.
|
||||
if word == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
e, err := s.EmojiState.Get(gID)
|
||||
guilds, err := s.EmojiState.Get(gID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var match = strings.ToLower(word)
|
||||
var entries = make([]cchat.CompletionEntry, 0, MaxCompletion)
|
||||
var entries []cchat.CompletionEntry
|
||||
var distances map[string]int
|
||||
|
||||
for _, guild := range e {
|
||||
GuildSearch:
|
||||
for _, guild := range guilds {
|
||||
for _, emoji := range guild.Emojis {
|
||||
if !contains(match, emoji.Name) {
|
||||
rank := rankFunc(word, emoji.Name)
|
||||
if rank == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// Defer allocation until we've found something.
|
||||
ensureEntriesMade(&entries)
|
||||
ensureDistancesMade(&distances)
|
||||
|
||||
raw := emoji.String()
|
||||
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: emoji.String(),
|
||||
Raw: raw,
|
||||
Text: text.Rich{Content: ":" + emoji.Name + ":"},
|
||||
Secondary: text.Rich{Content: guild.Name},
|
||||
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
|
||||
Image: true,
|
||||
})
|
||||
|
||||
distances[raw] = rank
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
return entries
|
||||
break GuildSearch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sortDistances(entries, distances)
|
||||
return entries
|
||||
}
|
||||
|
|
|
@ -10,60 +10,175 @@ import (
|
|||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
func (ch Completer) CompleteMentions(word string) (entries []cchat.CompletionEntry) {
|
||||
// MessageMentions generates a list of user mention completion entries from
|
||||
// messages.
|
||||
func MessageMentions(msgs []discord.Message) []cchat.CompletionEntry {
|
||||
return GuildMessageMentions(msgs, nil, nil)
|
||||
}
|
||||
|
||||
// GuildMessageMentions generates a list of member mention completion entries
|
||||
// from guild messages.
|
||||
func GuildMessageMentions(
|
||||
msgs []discord.Message,
|
||||
state *state.Instance, guild *discord.Guild) []cchat.CompletionEntry {
|
||||
|
||||
if len(msgs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Keep track of the number of authors.
|
||||
// TODO: fix excess allocations
|
||||
|
||||
var entries []cchat.CompletionEntry
|
||||
var authors map[discord.UserID]struct{}
|
||||
|
||||
for _, msg := range msgs {
|
||||
// If we've already added the author into the list, then skip.
|
||||
if _, ok := authors[msg.Author.ID]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
ensureAuthorMapMade(&authors)
|
||||
authors[msg.Author.ID] = struct{}{}
|
||||
|
||||
var rich text.Rich
|
||||
|
||||
if guild != nil && state != nil {
|
||||
m, err := state.Store.Member(guild.ID, msg.Author.ID)
|
||||
if err == nil {
|
||||
rich = message.RenderMemberName(*m, *guild, state)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to searching the author if member fails.
|
||||
if rich.IsEmpty() {
|
||||
rich = text.Plain(msg.Author.Username)
|
||||
}
|
||||
|
||||
ensureEntriesMade(&entries)
|
||||
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: msg.Author.Mention(),
|
||||
Text: rich,
|
||||
Secondary: text.Plain(msg.Author.Username + "#" + msg.Author.Discriminator),
|
||||
IconURL: msg.Author.AvatarURL(),
|
||||
})
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func ensureAuthorMapMade(authors *map[discord.UserID]struct{}) {
|
||||
if *authors == nil {
|
||||
*authors = make(map[discord.UserID]struct{}, MaxCompletion)
|
||||
}
|
||||
}
|
||||
|
||||
func Presences(s *state.Instance, word string) []cchat.CompletionEntry {
|
||||
presences, err := s.Presences(0)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var entries []cchat.CompletionEntry
|
||||
var distances map[string]int
|
||||
|
||||
for _, presence := range presences {
|
||||
rank := rankFunc(word, presence.User.Username)
|
||||
if rank == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
ensureEntriesMade(&entries)
|
||||
ensureDistancesMade(&distances)
|
||||
|
||||
raw := presence.User.Mention()
|
||||
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: raw,
|
||||
Text: text.Plain(presence.User.Username + "#" + presence.User.Discriminator),
|
||||
Secondary: text.Plain(FormatStatus(presence.Status)),
|
||||
IconURL: presence.User.AvatarURL(),
|
||||
})
|
||||
|
||||
distances[raw] = rank
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
sortDistances(entries, distances)
|
||||
return entries
|
||||
}
|
||||
|
||||
func FormatStatus(status discord.Status) string {
|
||||
switch status {
|
||||
case discord.OnlineStatus:
|
||||
return "Online"
|
||||
case discord.DoNotDisturbStatus:
|
||||
return "Busy"
|
||||
case discord.IdleStatus:
|
||||
return "Idle"
|
||||
case discord.InvisibleStatus:
|
||||
return "Invisible"
|
||||
case discord.OfflineStatus:
|
||||
return "Offline"
|
||||
default:
|
||||
return strings.Title(string(status))
|
||||
}
|
||||
}
|
||||
|
||||
func (ch ChannelCompleter) CompleteMentions(word string) []cchat.CompletionEntry {
|
||||
// If there is no input, then we should grab the latest messages.
|
||||
if word == "" {
|
||||
msgs, _ := ch.State.Store.Messages(ch.ID)
|
||||
g, _ := ch.State.Store.Guild(ch.GuildID) // nil is fine
|
||||
|
||||
// Keep track of the number of authors.
|
||||
// TODO: fix excess allocations
|
||||
var authors = make(map[discord.UserID]struct{}, MaxCompletion)
|
||||
|
||||
for _, msg := range msgs {
|
||||
// If we've already added the author into the list, then skip.
|
||||
if _, ok := authors[msg.Author.ID]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Record the current author and add the entry to the list.
|
||||
authors[msg.Author.ID] = struct{}{}
|
||||
entries = append(entries, completionUser(ch.State, msg.Author, g))
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return GuildMessageMentions(msgs, ch.State, g)
|
||||
}
|
||||
|
||||
// Lower-case everything for a case-insensitive match. contains() should
|
||||
// do the rest.
|
||||
var match = strings.ToLower(word)
|
||||
var entries []cchat.CompletionEntry
|
||||
var distances map[string]int
|
||||
|
||||
// If we're not in a guild, then we can check the list of recipients.
|
||||
if !ch.GuildID.IsValid() {
|
||||
c, err := ch.State.Store.Channel(ch.ID)
|
||||
if err != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, u := range c.DMRecipients {
|
||||
if contains(match, u.Username) {
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: u.Mention(),
|
||||
Text: text.Rich{Content: u.Username},
|
||||
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
||||
IconURL: u.AvatarURL(),
|
||||
})
|
||||
if len(entries) >= MaxCompletion {
|
||||
return
|
||||
}
|
||||
rank := rankFunc(word, u.Username)
|
||||
if rank == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
ensureEntriesMade(&entries)
|
||||
ensureDistancesMade(&distances)
|
||||
|
||||
raw := u.Mention()
|
||||
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: raw,
|
||||
Text: text.Rich{Content: u.Username},
|
||||
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
||||
IconURL: u.AvatarURL(),
|
||||
})
|
||||
|
||||
distances[raw] = rank
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
sortDistances(entries, distances)
|
||||
return entries
|
||||
}
|
||||
|
||||
// If we're in a guild, then we should search for (all) members.
|
||||
|
@ -71,50 +186,45 @@ func (ch Completer) CompleteMentions(word string) (entries []cchat.CompletionEnt
|
|||
g, gerr := ch.State.Store.Guild(ch.GuildID)
|
||||
|
||||
if merr != nil || gerr != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// If we couldn't find any members, then we can request Discord to
|
||||
// search for them.
|
||||
if len(m) == 0 {
|
||||
ch.State.MemberState.SearchMember(ch.GuildID, word)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, mem := range m {
|
||||
if contains(match, mem.User.Username, mem.Nick) {
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: mem.User.Mention(),
|
||||
Text: message.RenderMemberName(mem, *g, ch.State),
|
||||
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
|
||||
IconURL: mem.User.AvatarURL(),
|
||||
})
|
||||
if len(entries) >= MaxCompletion {
|
||||
return
|
||||
}
|
||||
rank := memberMatchString(word, &mem)
|
||||
if rank == -1 {
|
||||
continue
|
||||
}
|
||||
|
||||
ensureEntriesMade(&entries)
|
||||
ensureDistancesMade(&distances)
|
||||
|
||||
raw := mem.User.Mention()
|
||||
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: raw,
|
||||
Text: message.RenderMemberName(mem, *g, ch.State),
|
||||
Secondary: text.Plain(mem.User.Username + "#" + mem.User.Discriminator),
|
||||
IconURL: mem.User.AvatarURL(),
|
||||
})
|
||||
|
||||
distances[raw] = rank
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
sortDistances(entries, distances)
|
||||
return entries
|
||||
}
|
||||
|
||||
func completionUser(s *state.Instance, u discord.User, g *discord.Guild) cchat.CompletionEntry {
|
||||
if g != nil {
|
||||
m, err := s.Store.Member(g.ID, u.ID)
|
||||
if err == nil {
|
||||
return cchat.CompletionEntry{
|
||||
Raw: u.Mention(),
|
||||
Text: message.RenderMemberName(*m, *g, s),
|
||||
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
||||
IconURL: u.AvatarURL(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return cchat.CompletionEntry{
|
||||
Raw: u.Mention(),
|
||||
Text: text.Rich{Content: u.Username},
|
||||
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
||||
IconURL: u.AvatarURL(),
|
||||
}
|
||||
func memberMatchString(word string, m *discord.Member) int {
|
||||
return rankFunc(word, m.User.Username+" "+m.Nick)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,18 @@ import (
|
|||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
)
|
||||
|
||||
func WrapMessage(s *state.Instance, msg cchat.SendableMessage) api.SendMessageData {
|
||||
var send = api.SendMessageData{Content: msg.Content()}
|
||||
if attacher := msg.AsAttachments(); attacher != nil {
|
||||
send.Files = addAttachments(attacher.Attachments())
|
||||
}
|
||||
if noncer := msg.AsNoncer(); noncer != nil {
|
||||
send.Nonce = s.Nonces.Generate(noncer.Nonce())
|
||||
}
|
||||
|
||||
return send
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
shared.Channel
|
||||
}
|
||||
|
@ -20,16 +32,7 @@ func New(ch shared.Channel) Sender {
|
|||
}
|
||||
|
||||
func (s Sender) Send(msg cchat.SendableMessage) error {
|
||||
return Send(s.State, s.ID, msg)
|
||||
}
|
||||
|
||||
func Send(s *state.Instance, chID discord.ChannelID, msg cchat.SendableMessage) error {
|
||||
var send = api.SendMessageData{Content: msg.Content()}
|
||||
if attacher := msg.AsAttachments(); attacher != nil {
|
||||
send.Files = addAttachments(attacher.Attachments())
|
||||
}
|
||||
|
||||
_, err := s.SendMessageComplex(chID, send)
|
||||
_, err := s.State.SendMessageComplex(s.ID, WrapMessage(s.State, msg))
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
|
@ -6,8 +6,10 @@ import (
|
|||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -30,6 +32,15 @@ func NewPrivate(s *state.Instance, ch discord.Channel) (cchat.Server, error) {
|
|||
return Private{Channel: channel}, nil
|
||||
}
|
||||
|
||||
func (priv Private) Name() text.Rich {
|
||||
c, err := priv.Self()
|
||||
if err != nil {
|
||||
return text.Rich{Content: priv.ID()}
|
||||
}
|
||||
|
||||
return text.Plain(shared.PrivateName(*c))
|
||||
}
|
||||
|
||||
func (priv Private) AsIconer() cchat.Iconer {
|
||||
return NewAvatarIcon(priv.State)
|
||||
}
|
||||
|
|
|
@ -1,12 +1,44 @@
|
|||
// Package shared contains channel utilities.
|
||||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
)
|
||||
|
||||
// PrivateName returns the channel name if any, otherwise it formats its own
|
||||
// name into a list of recipients.
|
||||
func PrivateName(privCh discord.Channel) string {
|
||||
if privCh.Name != "" {
|
||||
return privCh.Name
|
||||
}
|
||||
|
||||
return FormatRecipients(privCh.DMRecipients)
|
||||
}
|
||||
|
||||
// FormatRecipients joins the given list of users into a string listing all
|
||||
// recipients with English punctuation rules.
|
||||
func FormatRecipients(users []discord.User) string {
|
||||
switch len(users) {
|
||||
case 0:
|
||||
return "<Nobody>"
|
||||
case 1:
|
||||
return users[0].Username
|
||||
case 2:
|
||||
return users[0].Username + " and " + users[1].Username
|
||||
}
|
||||
|
||||
var usernames = make([]string, len(users))
|
||||
for i, user := range users[:len(users)-1] {
|
||||
usernames[i] = user.Username
|
||||
}
|
||||
|
||||
return strings.Join(usernames, ", ") + " and " + users[len(users)-1].Username
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
ID discord.ChannelID
|
||||
GuildID discord.GuildID
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/mention"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/reference"
|
||||
"github.com/diamondburned/cchat-discord/internal/segments/segutil"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
|
@ -20,22 +21,12 @@ type Author struct {
|
|||
var _ cchat.Author = (*Author)(nil)
|
||||
|
||||
func NewUser(u discord.User, s *state.Instance) Author {
|
||||
var name = text.Rich{Content: u.Username}
|
||||
if u.Bot {
|
||||
name.Content += " "
|
||||
name.Segments = append(name.Segments,
|
||||
colored.NewBlurple(segutil.Write(&name, "[BOT]")),
|
||||
)
|
||||
}
|
||||
|
||||
// Append a clickable user popup.
|
||||
useg := mention.UserSegment(0, len(name.Content), u)
|
||||
useg.WithState(s.State)
|
||||
name.Segments = append(name.Segments, useg)
|
||||
var rich text.Rich
|
||||
richUser(&rich, u, s)
|
||||
|
||||
return Author{
|
||||
id: u.ID,
|
||||
name: name,
|
||||
name: rich,
|
||||
avatar: urlutils.AvatarURL(u.AvatarURL()),
|
||||
}
|
||||
}
|
||||
|
@ -49,36 +40,64 @@ func NewGuildMember(m discord.Member, g discord.Guild, s *state.Instance) Author
|
|||
}
|
||||
|
||||
func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text.Rich {
|
||||
var name = text.Rich{
|
||||
Content: m.User.Username,
|
||||
var rich text.Rich
|
||||
richMember(&rich, m, g, s)
|
||||
return rich
|
||||
}
|
||||
|
||||
// richMember appends the member name directly into rich.
|
||||
func richMember(
|
||||
rich *text.Rich, m discord.Member, g discord.Guild, s *state.Instance) (start, end int) {
|
||||
|
||||
var displayName = m.User.Username
|
||||
if m.Nick != "" {
|
||||
displayName = m.Nick
|
||||
}
|
||||
|
||||
// Update the nickname.
|
||||
if m.Nick != "" {
|
||||
name.Content = m.Nick
|
||||
}
|
||||
start, end = segutil.Write(rich, displayName)
|
||||
|
||||
// Update the color.
|
||||
if c := discord.MemberColor(g, m); c > 0 {
|
||||
name.Segments = append(name.Segments,
|
||||
colored.New(len(name.Content), c.Uint32()),
|
||||
rich.Segments = append(rich.Segments,
|
||||
colored.NewSegment(start, end, c.Uint32()),
|
||||
)
|
||||
}
|
||||
|
||||
// Append the bot prefix if the user is a bot.
|
||||
if m.User.Bot {
|
||||
name.Content += " "
|
||||
name.Segments = append(name.Segments,
|
||||
colored.NewBlurple(segutil.Write(&name, "[BOT]")),
|
||||
rich.Content += " "
|
||||
rich.Segments = append(rich.Segments,
|
||||
colored.NewBlurple(segutil.Write(rich, "[BOT]")),
|
||||
)
|
||||
}
|
||||
|
||||
// Append a clickable user popup.
|
||||
useg := mention.MemberSegment(0, len(name.Content), g, m)
|
||||
useg := mention.MemberSegment(start, end, g, m)
|
||||
useg.WithState(s.State)
|
||||
name.Segments = append(name.Segments, useg)
|
||||
rich.Segments = append(rich.Segments, useg)
|
||||
|
||||
return name
|
||||
return
|
||||
}
|
||||
|
||||
func richUser(
|
||||
rich *text.Rich, u discord.User, s *state.Instance) (start, end int) {
|
||||
|
||||
start, end = segutil.Write(rich, u.Username)
|
||||
|
||||
// Append the bot prefix if the user is a bot.
|
||||
if u.Bot {
|
||||
rich.Content += " "
|
||||
rich.Segments = append(rich.Segments,
|
||||
colored.NewBlurple(segutil.Write(rich, "[BOT]")),
|
||||
)
|
||||
}
|
||||
|
||||
// Append a clickable user popup.
|
||||
useg := mention.UserSegment(start, end, u)
|
||||
useg.WithState(s.State)
|
||||
rich.Segments = append(rich.Segments, useg)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (a Author) ID() cchat.ID {
|
||||
|
@ -92,3 +111,53 @@ func (a Author) Name() text.Rich {
|
|||
func (a Author) Avatar() string {
|
||||
return a.avatar
|
||||
}
|
||||
|
||||
const authorReplyingTo = " replying to "
|
||||
|
||||
// AddUserReply modifies Author to make it appear like it's a message reply.
|
||||
// Specifically, this function is used for direct messages.
|
||||
func (a *Author) AddUserReply(user discord.User, s *state.Instance) {
|
||||
a.name.Content += authorReplyingTo
|
||||
richUser(&a.name, user, s)
|
||||
}
|
||||
|
||||
func (a *Author) AddReply(name string) {
|
||||
a.name.Content += authorReplyingTo + name
|
||||
}
|
||||
|
||||
// // AddMemberReply modifies Author to make it appear like it's a message reply.
|
||||
// // Specifically, this function is used for guild messages.
|
||||
// func (a *Author) AddMemberReply(m discord.Member, g discord.Guild, s *state.Instance) {
|
||||
// a.name.Content += authorReplyingTo
|
||||
// richMember(&a.name, m, g, s)
|
||||
// }
|
||||
|
||||
// AddMessageReference adds a message reference to the author.
|
||||
func (a *Author) AddMessageReference(msgref discord.Message, s *state.Instance) {
|
||||
if !msgref.GuildID.IsValid() {
|
||||
a.name.Content += authorReplyingTo
|
||||
start, end := richUser(&a.name, msgref.Author, s)
|
||||
|
||||
a.name.Segments = append(a.name.Segments,
|
||||
reference.NewMessageSegment(start, end, msgref.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
g, err := s.Guild(msgref.GuildID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
m, err := s.Member(g.ID, msgref.Author.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.name.Content += authorReplyingTo
|
||||
start, end := richMember(&a.name, *m, *g, s)
|
||||
|
||||
a.name.Segments = append(a.name.Segments,
|
||||
reference.NewMessageSegment(start, end, msgref.ID),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
type messageHeader struct {
|
||||
id discord.MessageID
|
||||
time discord.Timestamp
|
||||
nonce string
|
||||
channelID discord.ChannelID
|
||||
guildID discord.GuildID
|
||||
}
|
||||
|
@ -34,6 +35,12 @@ func newHeader(msg discord.Message) messageHeader {
|
|||
return h
|
||||
}
|
||||
|
||||
func newHeaderNonce(msg discord.Message, nonce string) messageHeader {
|
||||
h := newHeader(msg)
|
||||
h.nonce = nonce
|
||||
return h
|
||||
}
|
||||
|
||||
func NewHeaderDelete(d *gateway.MessageDeleteEvent) messageHeader {
|
||||
return messageHeader{
|
||||
id: d.ID,
|
||||
|
@ -47,6 +54,8 @@ func (m messageHeader) ID() cchat.ID {
|
|||
return m.id.String()
|
||||
}
|
||||
|
||||
func (m messageHeader) Nonce() string { return m.nonce }
|
||||
|
||||
func (m messageHeader) MessageID() discord.MessageID { return m.id }
|
||||
func (m messageHeader) ChannelID() discord.ChannelID { return m.channelID }
|
||||
func (m messageHeader) GuildID() discord.GuildID { return m.guildID }
|
||||
|
@ -69,6 +78,7 @@ var (
|
|||
_ cchat.MessageCreate = (*Message)(nil)
|
||||
_ cchat.MessageUpdate = (*Message)(nil)
|
||||
_ cchat.MessageDelete = (*Message)(nil)
|
||||
_ cchat.Noncer = (*Message)(nil)
|
||||
)
|
||||
|
||||
func NewMessageUpdateContent(msg discord.Message, s *state.Instance) Message {
|
||||
|
@ -97,13 +107,18 @@ func NewMessageUpdateAuthor(
|
|||
}
|
||||
}
|
||||
|
||||
// NewMessageCreate uses the session to create a message. It does not do
|
||||
// API calls. Member is optional.
|
||||
func NewMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Message {
|
||||
// NewGuildMessageCreate uses the session to create a message. It does not do
|
||||
// API calls. Member is optional. This is the only call that populates the Nonce
|
||||
// in the header.
|
||||
func NewGuildMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Message {
|
||||
// Copy and change the nonce.
|
||||
message := c.Message
|
||||
message.Nonce = s.Nonces.Load(c.Nonce)
|
||||
|
||||
// This should not error.
|
||||
g, err := s.Store.Guild(c.GuildID)
|
||||
if err != nil {
|
||||
return NewMessage(c.Message, s, NewUser(c.Author, s))
|
||||
return NewMessage(message, s, NewUser(c.Author, s))
|
||||
}
|
||||
|
||||
if c.Member == nil {
|
||||
|
@ -111,10 +126,10 @@ func NewMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Message
|
|||
}
|
||||
if c.Member == nil {
|
||||
s.MemberState.RequestMember(c.GuildID, c.Author.ID)
|
||||
return NewMessage(c.Message, s, NewUser(c.Author, s))
|
||||
return NewMessage(message, s, NewUser(c.Author, s))
|
||||
}
|
||||
|
||||
return NewMessage(c.Message, s, NewGuildMember(*c.Member, *g, s))
|
||||
return NewMessage(message, s, NewGuildMember(*c.Member, *g, s))
|
||||
}
|
||||
|
||||
// NewBacklogMessage uses the session to create a message fetched from the
|
||||
|
@ -167,7 +182,7 @@ func NewMessage(m discord.Message, s *state.Instance, author Author) Message {
|
|||
}
|
||||
|
||||
return Message{
|
||||
messageHeader: newHeader(m),
|
||||
messageHeader: newHeaderNonce(m, m.Nonce),
|
||||
author: author,
|
||||
content: content,
|
||||
}
|
||||
|
@ -184,6 +199,10 @@ func (m Message) Content() text.Rich {
|
|||
return m.content
|
||||
}
|
||||
|
||||
func (m Message) Nonce() string {
|
||||
return m.nonce
|
||||
}
|
||||
|
||||
func (m Message) Mentioned() bool {
|
||||
return m.mentioned
|
||||
}
|
||||
|
|
|
@ -7,8 +7,11 @@ import (
|
|||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
|
||||
"github.com/diamondburned/cchat-discord/internal/funcutil"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
)
|
||||
|
@ -52,8 +55,9 @@ func (list *messageList) delete(id discord.MessageID) {
|
|||
type Messages struct {
|
||||
empty.Messenger
|
||||
|
||||
state *state.Instance
|
||||
acList *activeList
|
||||
state *state.Instance
|
||||
acList *activeList
|
||||
sentMsgs *nonce.Set
|
||||
|
||||
sender *Sender
|
||||
|
||||
|
@ -64,19 +68,48 @@ type Messages struct {
|
|||
}
|
||||
|
||||
func NewMessages(s *state.Instance, acList *activeList, adder ChannelAdder) *Messages {
|
||||
var sentMsgs nonce.Set
|
||||
|
||||
hubServer := &Messages{
|
||||
state: s,
|
||||
acList: acList,
|
||||
sender: NewSender(s, acList, adder),
|
||||
sentMsgs: &sentMsgs,
|
||||
sender: &Sender{
|
||||
adder: adder,
|
||||
acList: acList,
|
||||
sentMsgs: &sentMsgs,
|
||||
state: s,
|
||||
},
|
||||
messages: make(messageList, 0, 100),
|
||||
}
|
||||
|
||||
hubServer.sender.completers = complete.Completer{
|
||||
':': func(word string) []cchat.CompletionEntry {
|
||||
return complete.Emojis(s, 0, word)
|
||||
},
|
||||
'@': func(word string) []cchat.CompletionEntry {
|
||||
if word != "" {
|
||||
return complete.Presences(s, word)
|
||||
}
|
||||
|
||||
hubServer.msgMutex.Lock()
|
||||
defer hubServer.msgMutex.Unlock()
|
||||
return complete.MessageMentions(hubServer.messages)
|
||||
},
|
||||
'#': func(word string) []cchat.CompletionEntry {
|
||||
return complete.DMChannels(s, word)
|
||||
},
|
||||
}
|
||||
|
||||
hubServer.cancel = funcutil.JoinCancels(
|
||||
s.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
||||
if msg.GuildID.IsValid() || acList.isActive(msg.ChannelID) {
|
||||
return
|
||||
}
|
||||
|
||||
// We're not adding back messages we sent here, since we already
|
||||
// have a separate channel for that.
|
||||
|
||||
hubServer.msgMutex.Lock()
|
||||
hubServer.messages.append(msg.Message)
|
||||
hubServer.msgMutex.Unlock()
|
||||
|
@ -122,11 +155,32 @@ func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer
|
|||
// Bind the handler.
|
||||
return funcutil.JoinCancels(
|
||||
msgs.state.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
||||
if msg.GuildID.IsValid() || msgs.acList.isActive(msg.ChannelID) {
|
||||
if msg.GuildID.IsValid() {
|
||||
return
|
||||
}
|
||||
|
||||
ct.CreateMessage(message.NewMessageCreate(msg, msgs.state))
|
||||
var isReply = false
|
||||
if msgs.acList.isActive(msg.ChannelID) {
|
||||
if !msgs.sentMsgs.HasAndDelete(msg.Nonce) {
|
||||
return
|
||||
}
|
||||
isReply = true
|
||||
}
|
||||
|
||||
var author = message.NewUser(msg.Author, msgs.state)
|
||||
if isReply {
|
||||
c, err := msgs.state.Channel(msg.ChannelID)
|
||||
if err == nil {
|
||||
switch c.Type {
|
||||
case discord.DirectMessage:
|
||||
author.AddUserReply(c.DMRecipients[0], msgs.state)
|
||||
case discord.GroupDM:
|
||||
author.AddReply(shared.PrivateName(*c))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ct.CreateMessage(message.NewMessage(msg.Message, msgs.state, author))
|
||||
msgs.state.ReadState.MarkRead(msg.ChannelID, msg.ID)
|
||||
}),
|
||||
msgs.state.AddHandler(func(update *gateway.MessageUpdateEvent) {
|
||||
|
|
|
@ -7,7 +7,9 @@ import (
|
|||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send/complete"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
@ -17,18 +19,26 @@ type ChannelAdder interface {
|
|||
AddChannel(state *state.Instance, ch *discord.Channel)
|
||||
}
|
||||
|
||||
// TODO: unexport Sender
|
||||
|
||||
type Sender struct {
|
||||
empty.Sender
|
||||
adder ChannelAdder
|
||||
acList *activeList
|
||||
state *state.Instance
|
||||
adder ChannelAdder
|
||||
acList *activeList
|
||||
sentMsgs *nonce.Set
|
||||
state *state.Instance
|
||||
|
||||
completers complete.Completer
|
||||
}
|
||||
|
||||
func NewSender(s *state.Instance, acList *activeList, adder ChannelAdder) *Sender {
|
||||
return &Sender{adder: adder, acList: acList, state: s}
|
||||
}
|
||||
|
||||
var mentionRegex = regexp.MustCompile(`^<@!?(\d+)> ?`)
|
||||
// mentionRegex matche the following:
|
||||
//
|
||||
// <#123123>
|
||||
// <#!12312> // This is OK because we're not sending it.
|
||||
// <@123123>
|
||||
// <@!12312>
|
||||
//
|
||||
var mentionRegex = regexp.MustCompile(`(?m)^<(@|#)!?(\d+)> ?`)
|
||||
|
||||
// wrappedMessage wraps around a SendableMessage to override its content.
|
||||
type wrappedMessage struct {
|
||||
|
@ -48,28 +58,40 @@ func (s *Sender) Send(sendable cchat.SendableMessage) error {
|
|||
// Validate message.
|
||||
matches := mentionRegex.FindStringSubmatch(content)
|
||||
if matches == nil {
|
||||
return errors.New("messages sent here must start with a mention")
|
||||
return errors.New("message must start with a user or channel mention")
|
||||
}
|
||||
|
||||
targetID, err := discord.ParseSnowflake(matches[1])
|
||||
// TODO: account for channel names
|
||||
|
||||
targetID, err := discord.ParseSnowflake(matches[2])
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to parse recipient ID")
|
||||
}
|
||||
|
||||
ch, err := s.state.CreatePrivateChannel(discord.UserID(targetID))
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to find DM channel")
|
||||
var channel *discord.Channel
|
||||
switch matches[1] {
|
||||
case "@":
|
||||
channel, _ = s.state.CreatePrivateChannel(discord.UserID(targetID))
|
||||
case "#":
|
||||
channel, _ = s.state.Channel(discord.ChannelID(targetID))
|
||||
}
|
||||
if channel == nil {
|
||||
return errors.New("unknown channel")
|
||||
}
|
||||
|
||||
s.adder.AddChannel(s.state, ch)
|
||||
s.acList.add(ch.ID)
|
||||
s.adder.AddChannel(s.state, channel)
|
||||
s.acList.add(channel.ID)
|
||||
|
||||
return send.Send(s.state, ch.ID, wrappedMessage{
|
||||
SendableMessage: sendable,
|
||||
content: strings.TrimPrefix(content, matches[0]),
|
||||
})
|
||||
sendData := send.WrapMessage(s.state, sendable)
|
||||
sendData.Content = strings.TrimPrefix(content, matches[0])
|
||||
|
||||
// Store the nonce.
|
||||
s.sentMsgs.Store(sendData.Nonce)
|
||||
|
||||
_, err = s.state.SendMessageComplex(channel.ID, sendData)
|
||||
return errors.Wrap(err, "failed to send message")
|
||||
}
|
||||
|
||||
// func (msgs *Messages) AsCompleter() cchat.Completer {
|
||||
// return complete.New(msgs)
|
||||
// }
|
||||
func (s *Sender) AsCompleter() cchat.Completer {
|
||||
return s.completers
|
||||
}
|
||||
|
|
|
@ -60,11 +60,16 @@ func (acList *activeList) isActive(channelID discord.ChannelID) bool {
|
|||
return ok
|
||||
}
|
||||
|
||||
func (acList *activeList) add(chID discord.ChannelID) {
|
||||
func (acList *activeList) add(chID discord.ChannelID) (changed bool) {
|
||||
acList.mut.Lock()
|
||||
defer acList.mut.Unlock()
|
||||
|
||||
if _, ok := acList.active[chID]; ok {
|
||||
return false
|
||||
}
|
||||
|
||||
acList.active[chID] = struct{}{}
|
||||
return true
|
||||
}
|
||||
|
||||
// Server is the server (channel) that contains all incoming DM messages that
|
||||
|
|
90
internal/discord/state/nonce/nonce.go
Normal file
90
internal/discord/state/nonce/nonce.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
package nonce
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
cryptorand "crypto/rand"
|
||||
mathrand "math/rand"
|
||||
)
|
||||
|
||||
func init() {
|
||||
mathrand.Seed(time.Now().UnixNano())
|
||||
}
|
||||
|
||||
var nonceCounter uint64
|
||||
|
||||
// generateNonce generates a unique nonce ID.
|
||||
func generateNonce() string {
|
||||
return fmt.Sprintf(
|
||||
"%s-%s-%s",
|
||||
strconv.FormatInt(time.Now().Unix(), 36),
|
||||
randomBits(),
|
||||
strconv.FormatUint(atomic.AddUint64(&nonceCounter, 1), 36),
|
||||
)
|
||||
}
|
||||
|
||||
// randomBits returns a string 6 bytes long with random characters that are safe
|
||||
// to print. It falls back to math/rand's pseudorandom number generator if it
|
||||
// cannot read from the system entropy pool.
|
||||
func randomBits() string {
|
||||
randBits := make([]byte, 2)
|
||||
|
||||
_, err := cryptorand.Read(randBits)
|
||||
if err != nil {
|
||||
binary.LittleEndian.PutUint32(randBits, mathrand.Uint32())
|
||||
}
|
||||
|
||||
return base64.RawStdEncoding.EncodeToString(randBits)
|
||||
}
|
||||
|
||||
// Map is a nonce state that keeps track of known nonces and generates a
|
||||
// Discord-compatible nonce string.
|
||||
type Map sync.Map
|
||||
|
||||
// Generate generates a new internal nonce, add a bind from the new nonce to the
|
||||
// original nonce, then return the new nonce. If the given original nonce is
|
||||
// empty, then an empty string is returned.
|
||||
func (nmap *Map) Generate(original string) string {
|
||||
// Ignore empty nonces.
|
||||
if original == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
newNonce := generateNonce()
|
||||
(*sync.Map)(nmap).Store(newNonce, original)
|
||||
return newNonce
|
||||
}
|
||||
|
||||
// Load grabs the nonce and permanently deleting it if the given nonce is found.
|
||||
func (nmap *Map) Load(newNonce string) string {
|
||||
v, ok := (*sync.Map)(nmap).LoadAndDelete(newNonce)
|
||||
if ok {
|
||||
return v.(string)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Set is a unique set of nonces.
|
||||
type Set sync.Map
|
||||
|
||||
var nonceSentinel = struct{}{}
|
||||
|
||||
func (nset *Set) Store(nonce string) {
|
||||
(*sync.Map)(nset).Store(nonce, nonceSentinel)
|
||||
}
|
||||
|
||||
func (nset *Set) Has(nonce string) bool {
|
||||
_, ok := (*sync.Map)(nset).Load(nonce)
|
||||
return ok
|
||||
}
|
||||
|
||||
func (nset *Set) HasAndDelete(nonce string) bool {
|
||||
_, ok := (*sync.Map)(nset).LoadAndDelete(nonce)
|
||||
return ok
|
||||
}
|
|
@ -10,18 +10,20 @@ import (
|
|||
"github.com/diamondburned/arikawa/state"
|
||||
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
|
||||
"github.com/diamondburned/ningen"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Instance struct {
|
||||
*ningen.State
|
||||
Nonces *nonce.Map
|
||||
|
||||
// UserID is a constant user ID. It is guaranteed to be valid.
|
||||
UserID discord.UserID
|
||||
}
|
||||
|
||||
var (
|
||||
_ cchat.SessionSaver = (*Instance)(nil)
|
||||
)
|
||||
var _ cchat.SessionSaver = (*Instance)(nil)
|
||||
|
||||
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
||||
var ErrInvalidSession = errors.New("invalid session")
|
||||
|
@ -58,12 +60,12 @@ func New(s *state.State) (*Instance, error) {
|
|||
// Prefetch user.
|
||||
u, err := s.Me()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to get current user")
|
||||
return nil, errors.Wrap(err, "failed to get current user")
|
||||
}
|
||||
|
||||
n, err := ningen.FromState(s)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to create a state wrapper")
|
||||
return nil, errors.Wrap(err, "failed to create a state wrapper")
|
||||
}
|
||||
|
||||
n.Client.OnRequest = append(n.Client.OnRequest, func(r httpdriver.Request) error {
|
||||
|
@ -78,6 +80,7 @@ func New(s *state.State) (*Instance, error) {
|
|||
return &Instance{
|
||||
UserID: u.ID,
|
||||
State: n,
|
||||
Nonces: new(nonce.Map),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
40
internal/segments/reference/reference.go
Normal file
40
internal/segments/reference/reference.go
Normal file
|
@ -0,0 +1,40 @@
|
|||
package reference
|
||||
|
||||
import (
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
)
|
||||
|
||||
type MessageID cchat.ID
|
||||
|
||||
var _ text.MessageReferencer = (*MessageID)(nil)
|
||||
|
||||
func (msgID MessageID) MessageID() string {
|
||||
return string(msgID)
|
||||
}
|
||||
|
||||
type MessageSegment struct {
|
||||
empty.TextSegment
|
||||
start, end int
|
||||
messageID discord.MessageID
|
||||
}
|
||||
|
||||
var _ text.Segment = (*MessageSegment)(nil)
|
||||
|
||||
func NewMessageSegment(start, end int, msgID discord.MessageID) MessageSegment {
|
||||
return MessageSegment{
|
||||
start: start,
|
||||
end: end,
|
||||
messageID: msgID,
|
||||
}
|
||||
}
|
||||
|
||||
func (msgseg MessageSegment) Bounds() (start, end int) {
|
||||
return msgseg.start, msgseg.end
|
||||
}
|
||||
|
||||
func (msgseg MessageSegment) AsMessageReferencer() text.MessageReferencer {
|
||||
return MessageID(msgseg.messageID.String())
|
||||
}
|
Loading…
Reference in a new issue