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 (
|
require (
|
||||||
github.com/diamondburned/arikawa v1.3.6
|
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/diamondburned/ningen v0.2.1-0.20201023061015-ce64ffb0bb12
|
||||||
github.com/dustin/go-humanize v1.0.0
|
github.com/dustin/go-humanize v1.0.0
|
||||||
github.com/go-test/deep v1.0.7
|
github.com/go-test/deep v1.0.7
|
||||||
|
github.com/lithammer/fuzzysearch v1.1.1
|
||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966
|
||||||
github.com/yuin/goldmark v1.1.30
|
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.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 h1:mew54lsDrwrJs4U2FtdbNFl/wAZcueIgZCsImHQzVL4=
|
||||||
github.com/diamondburned/cchat v0.3.12/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
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 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.20200621014632-6babb812b249/go.mod h1:xW9hpBZsGi8KpAh10TyP+YQlYBo+Xc+2w4TR6N0951A=
|
||||||
github.com/diamondburned/ningen v0.1.1-0.20200708085949-b64e350f3b8c h1:3h/kyk6HplYZF3zLi106itjYJWjbuMK/twijeGLEy2M=
|
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/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
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/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 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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=
|
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.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.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.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/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-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/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 {
|
func (ch Channel) Name() text.Rich {
|
||||||
c, err := ch.Self()
|
c, err := ch.Self()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return text.Rich{Content: ch.Channel.ID.String()}
|
return text.Rich{Content: ch.ID()}
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.NSFW {
|
if c.NSFW {
|
||||||
|
|
|
@ -12,13 +12,13 @@ import (
|
||||||
|
|
||||||
type Commander struct {
|
type Commander struct {
|
||||||
shared.Channel
|
shared.Channel
|
||||||
msgCompl complete.Completer
|
msgCompl complete.ChannelCompleter
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCommander(ch shared.Channel) cchat.Commander {
|
func NewCommander(ch shared.Channel) cchat.Commander {
|
||||||
return Commander{
|
return Commander{
|
||||||
Channel: ch,
|
Channel: ch,
|
||||||
msgCompl: complete.Completer{
|
msgCompl: complete.ChannelCompleter{
|
||||||
Channel: ch,
|
Channel: ch,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,11 +28,11 @@ type Messenger struct {
|
||||||
|
|
||||||
var _ cchat.Messenger = (*Messenger)(nil)
|
var _ cchat.Messenger = (*Messenger)(nil)
|
||||||
|
|
||||||
func New(ch shared.Channel) Messenger {
|
func New(ch shared.Channel) *Messenger {
|
||||||
return Messenger{Channel: ch}
|
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)
|
state := msgr.State.WithContext(ctx)
|
||||||
|
|
||||||
m, err := state.Messages(msgr.ID)
|
m, err := state.Messages(msgr.ID)
|
||||||
|
@ -112,7 +112,7 @@ func (msgr Messenger) JoinServer(ctx context.Context, ct cchat.MessagesContainer
|
||||||
addcancel(
|
addcancel(
|
||||||
msgr.State.AddHandler(func(m *gateway.MessageCreateEvent) {
|
msgr.State.AddHandler(func(m *gateway.MessageCreateEvent) {
|
||||||
if m.ChannelID == msgr.ID {
|
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)
|
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
|
return funcutil.JoinCancels(addcancel()...), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsSender() cchat.Sender {
|
func (msgr *Messenger) AsSender() cchat.Sender {
|
||||||
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -140,7 +140,7 @@ func (msgr Messenger) AsSender() cchat.Sender {
|
||||||
return send.New(msgr.Channel)
|
return send.New(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsEditor() cchat.Editor {
|
func (msgr *Messenger) AsEditor() cchat.Editor {
|
||||||
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
if !msgr.HasPermission(discord.PermissionSendMessages) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -148,22 +148,22 @@ func (msgr Messenger) AsEditor() cchat.Editor {
|
||||||
return edit.New(msgr.Channel)
|
return edit.New(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsActioner() cchat.Actioner {
|
func (msgr *Messenger) AsActioner() cchat.Actioner {
|
||||||
return action.New(msgr.Channel)
|
return action.New(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsNicknamer() cchat.Nicknamer {
|
func (msgr *Messenger) AsNicknamer() cchat.Nicknamer {
|
||||||
return nickname.New(msgr.Channel)
|
return nickname.New(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsMemberLister() cchat.MemberLister {
|
func (msgr *Messenger) AsMemberLister() cchat.MemberLister {
|
||||||
if !msgr.GuildID.IsValid() {
|
if !msgr.GuildID.IsValid() {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return memberlist.New(msgr.Channel)
|
return memberlist.New(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsBacklogger() cchat.Backlogger {
|
func (msgr *Messenger) AsBacklogger() cchat.Backlogger {
|
||||||
if !msgr.HasPermission(discord.PermissionViewChannel, discord.PermissionReadMessageHistory) {
|
if !msgr.HasPermission(discord.PermissionViewChannel, discord.PermissionReadMessageHistory) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -171,10 +171,10 @@ func (msgr Messenger) AsBacklogger() cchat.Backlogger {
|
||||||
return backlog.New(msgr.Channel)
|
return backlog.New(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsTypingIndicator() cchat.TypingIndicator {
|
func (msgr *Messenger) AsTypingIndicator() cchat.TypingIndicator {
|
||||||
return indicate.NewTyping(msgr.Channel)
|
return indicate.NewTyping(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (msgr Messenger) AsUnreadIndicator() cchat.UnreadIndicator {
|
func (msgr *Messenger) AsUnreadIndicator() cchat.UnreadIndicator {
|
||||||
return indicate.NewUnread(msgr.Channel)
|
return indicate.NewUnread(msgr.Channel)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,52 +1,90 @@
|
||||||
package complete
|
package complete
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"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"
|
"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.
|
// Ignore if empty word.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ignore if we're not in a guild.
|
// Ignore if we're not in a guild.
|
||||||
if !ch.GuildID.IsValid() {
|
if !ch.GuildID.IsValid() {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
c, err := ch.State.Store.Channels(ch.GuildID)
|
c, err := ch.State.Store.Channels(ch.GuildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var match = strings.ToLower(word)
|
return completeChannels(c, word, ch.State)
|
||||||
|
}
|
||||||
|
|
||||||
for _, channel := range c {
|
func DMChannels(s *state.Instance, word string) []cchat.CompletionEntry {
|
||||||
if !contains(match, channel.Name) {
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var category string
|
var category string
|
||||||
if channel.CategoryID.IsValid() {
|
if s != nil && channel.CategoryID.IsValid() {
|
||||||
if c, _ := ch.State.Store.Channel(channel.CategoryID); c != nil {
|
if cat, _ := s.Store.Channel(channel.CategoryID); cat != nil {
|
||||||
category = c.Name
|
category = cat.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defer allocation until we've found something.
|
||||||
|
ensureEntriesMade(&entries)
|
||||||
|
ensureDistancesMade(&distances)
|
||||||
|
|
||||||
|
raw := channel.Mention()
|
||||||
|
|
||||||
entries = append(entries, cchat.CompletionEntry{
|
entries = append(entries, cchat.CompletionEntry{
|
||||||
Raw: channel.Mention(),
|
Raw: raw,
|
||||||
Text: text.Rich{Content: "#" + channel.Name},
|
Text: text.Plain("#" + channel.Name),
|
||||||
Secondary: text.Rich{Content: category},
|
Secondary: text.Plain(category),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
distances[raw] = rank
|
||||||
|
|
||||||
if len(entries) >= MaxCompletion {
|
if len(entries) >= MaxCompletion {
|
||||||
return
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
sortDistances(entries, distances)
|
||||||
|
return entries
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,20 +1,30 @@
|
||||||
package complete
|
package complete
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"sort"
|
||||||
|
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/shared"
|
"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
|
shared.Channel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Completer map[byte]CompleterFunc
|
||||||
|
|
||||||
const MaxCompletion = 15
|
const MaxCompletion = 15
|
||||||
|
|
||||||
func New(ch shared.Channel) cchat.Completer {
|
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.
|
// CompleteMessage implements message input completion capability for Discord.
|
||||||
|
@ -28,24 +38,40 @@ func (ch Completer) Complete(words []string, i int64) []cchat.CompletionEntry {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
switch word[0] {
|
fn, ok := ch[word[0]]
|
||||||
case '@':
|
if !ok {
|
||||||
return ch.CompleteMentions(word[1:])
|
return nil
|
||||||
case '#':
|
|
||||||
return ch.CompleteChannels(word[1:])
|
|
||||||
case ':':
|
|
||||||
return ch.CompleteEmojis(word[1:])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn(word[1:])
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func contains(contains string, strs ...string) bool {
|
// rankFunc is the default rank function to use.
|
||||||
for _, str := range strs {
|
func rankFunc(source, target string) int {
|
||||||
if strings.Contains(strings.ToLower(str), contains) {
|
return fuzzy.RankMatchNormalizedFold(source, target)
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
}
|
func ensureEntriesMade(entries *[]cchat.CompletionEntry) {
|
||||||
|
if *entries == nil {
|
||||||
return false
|
*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
|
package complete
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
|
@ -10,43 +8,54 @@ import (
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ch Completer) CompleteEmojis(word string) (entries []cchat.CompletionEntry) {
|
func (ch ChannelCompleter) CompleteEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||||
return CompleteEmojis(ch.State, ch.GuildID, word)
|
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.
|
// Ignore if empty word.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
e, err := s.EmojiState.Get(gID)
|
guilds, err := s.EmojiState.Get(gID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var match = strings.ToLower(word)
|
var entries []cchat.CompletionEntry
|
||||||
var entries = make([]cchat.CompletionEntry, 0, MaxCompletion)
|
var distances map[string]int
|
||||||
|
|
||||||
for _, guild := range e {
|
GuildSearch:
|
||||||
|
for _, guild := range guilds {
|
||||||
for _, emoji := range guild.Emojis {
|
for _, emoji := range guild.Emojis {
|
||||||
if !contains(match, emoji.Name) {
|
rank := rankFunc(word, emoji.Name)
|
||||||
|
if rank == -1 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Defer allocation until we've found something.
|
||||||
|
ensureEntriesMade(&entries)
|
||||||
|
ensureDistancesMade(&distances)
|
||||||
|
|
||||||
|
raw := emoji.String()
|
||||||
|
|
||||||
entries = append(entries, cchat.CompletionEntry{
|
entries = append(entries, cchat.CompletionEntry{
|
||||||
Raw: emoji.String(),
|
Raw: raw,
|
||||||
Text: text.Rich{Content: ":" + emoji.Name + ":"},
|
Text: text.Rich{Content: ":" + emoji.Name + ":"},
|
||||||
Secondary: text.Rich{Content: guild.Name},
|
Secondary: text.Rich{Content: guild.Name},
|
||||||
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
|
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
|
||||||
Image: true,
|
Image: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
distances[raw] = rank
|
||||||
|
|
||||||
if len(entries) >= MaxCompletion {
|
if len(entries) >= MaxCompletion {
|
||||||
return entries
|
break GuildSearch
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sortDistances(entries, distances)
|
||||||
return entries
|
return entries
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,60 +10,175 @@ import (
|
||||||
"github.com/diamondburned/cchat/text"
|
"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 there is no input, then we should grab the latest messages.
|
||||||
if word == "" {
|
if word == "" {
|
||||||
msgs, _ := ch.State.Store.Messages(ch.ID)
|
msgs, _ := ch.State.Store.Messages(ch.ID)
|
||||||
g, _ := ch.State.Store.Guild(ch.GuildID) // nil is fine
|
g, _ := ch.State.Store.Guild(ch.GuildID) // nil is fine
|
||||||
|
|
||||||
// Keep track of the number of authors.
|
return GuildMessageMentions(msgs, ch.State, g)
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lower-case everything for a case-insensitive match. contains() should
|
var entries []cchat.CompletionEntry
|
||||||
// do the rest.
|
var distances map[string]int
|
||||||
var match = strings.ToLower(word)
|
|
||||||
|
|
||||||
// If we're not in a guild, then we can check the list of recipients.
|
// If we're not in a guild, then we can check the list of recipients.
|
||||||
if !ch.GuildID.IsValid() {
|
if !ch.GuildID.IsValid() {
|
||||||
c, err := ch.State.Store.Channel(ch.ID)
|
c, err := ch.State.Store.Channel(ch.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, u := range c.DMRecipients {
|
for _, u := range c.DMRecipients {
|
||||||
if contains(match, u.Username) {
|
rank := rankFunc(word, u.Username)
|
||||||
entries = append(entries, cchat.CompletionEntry{
|
if rank == -1 {
|
||||||
Raw: u.Mention(),
|
continue
|
||||||
Text: text.Rich{Content: u.Username},
|
}
|
||||||
Secondary: text.Rich{Content: u.Username + "#" + u.Discriminator},
|
|
||||||
IconURL: u.AvatarURL(),
|
ensureEntriesMade(&entries)
|
||||||
})
|
ensureDistancesMade(&distances)
|
||||||
if len(entries) >= MaxCompletion {
|
|
||||||
return
|
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.
|
// 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)
|
g, gerr := ch.State.Store.Guild(ch.GuildID)
|
||||||
|
|
||||||
if merr != nil || gerr != nil {
|
if merr != nil || gerr != nil {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we couldn't find any members, then we can request Discord to
|
// If we couldn't find any members, then we can request Discord to
|
||||||
// search for them.
|
// search for them.
|
||||||
if len(m) == 0 {
|
if len(m) == 0 {
|
||||||
ch.State.MemberState.SearchMember(ch.GuildID, word)
|
ch.State.MemberState.SearchMember(ch.GuildID, word)
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mem := range m {
|
for _, mem := range m {
|
||||||
if contains(match, mem.User.Username, mem.Nick) {
|
rank := memberMatchString(word, &mem)
|
||||||
entries = append(entries, cchat.CompletionEntry{
|
if rank == -1 {
|
||||||
Raw: mem.User.Mention(),
|
continue
|
||||||
Text: message.RenderMemberName(mem, *g, ch.State),
|
}
|
||||||
Secondary: text.Rich{Content: mem.User.Username + "#" + mem.User.Discriminator},
|
|
||||||
IconURL: mem.User.AvatarURL(),
|
ensureEntriesMade(&entries)
|
||||||
})
|
ensureDistancesMade(&distances)
|
||||||
if len(entries) >= MaxCompletion {
|
|
||||||
return
|
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 {
|
func memberMatchString(word string, m *discord.Member) int {
|
||||||
if g != nil {
|
return rankFunc(word, m.User.Username+" "+m.Nick)
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,18 @@ import (
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"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 {
|
type Sender struct {
|
||||||
shared.Channel
|
shared.Channel
|
||||||
}
|
}
|
||||||
|
@ -20,16 +32,7 @@ func New(ch shared.Channel) Sender {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s Sender) Send(msg cchat.SendableMessage) error {
|
func (s Sender) Send(msg cchat.SendableMessage) error {
|
||||||
return Send(s.State, s.ID, msg)
|
_, err := s.State.SendMessageComplex(s.ID, WrapMessage(s.State, 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)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,8 +6,10 @@ import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
"github.com/diamondburned/cchat"
|
"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/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
|
"github.com/diamondburned/cchat/text"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -30,6 +32,15 @@ func NewPrivate(s *state.Instance, ch discord.Channel) (cchat.Server, error) {
|
||||||
return Private{Channel: channel}, nil
|
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 {
|
func (priv Private) AsIconer() cchat.Iconer {
|
||||||
return NewAvatarIcon(priv.State)
|
return NewAvatarIcon(priv.State)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,44 @@
|
||||||
|
// Package shared contains channel utilities.
|
||||||
package shared
|
package shared
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"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 {
|
type Channel struct {
|
||||||
ID discord.ChannelID
|
ID discord.ChannelID
|
||||||
GuildID discord.GuildID
|
GuildID discord.GuildID
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||||
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
"github.com/diamondburned/cchat-discord/internal/segments/colored"
|
||||||
"github.com/diamondburned/cchat-discord/internal/segments/mention"
|
"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/segments/segutil"
|
||||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||||
"github.com/diamondburned/cchat/text"
|
"github.com/diamondburned/cchat/text"
|
||||||
|
@ -20,22 +21,12 @@ type Author struct {
|
||||||
var _ cchat.Author = (*Author)(nil)
|
var _ cchat.Author = (*Author)(nil)
|
||||||
|
|
||||||
func NewUser(u discord.User, s *state.Instance) Author {
|
func NewUser(u discord.User, s *state.Instance) Author {
|
||||||
var name = text.Rich{Content: u.Username}
|
var rich text.Rich
|
||||||
if u.Bot {
|
richUser(&rich, u, s)
|
||||||
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)
|
|
||||||
|
|
||||||
return Author{
|
return Author{
|
||||||
id: u.ID,
|
id: u.ID,
|
||||||
name: name,
|
name: rich,
|
||||||
avatar: urlutils.AvatarURL(u.AvatarURL()),
|
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 {
|
func RenderMemberName(m discord.Member, g discord.Guild, s *state.Instance) text.Rich {
|
||||||
var name = text.Rich{
|
var rich text.Rich
|
||||||
Content: m.User.Username,
|
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.
|
start, end = segutil.Write(rich, displayName)
|
||||||
if m.Nick != "" {
|
|
||||||
name.Content = m.Nick
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the color.
|
// Update the color.
|
||||||
if c := discord.MemberColor(g, m); c > 0 {
|
if c := discord.MemberColor(g, m); c > 0 {
|
||||||
name.Segments = append(name.Segments,
|
rich.Segments = append(rich.Segments,
|
||||||
colored.New(len(name.Content), c.Uint32()),
|
colored.NewSegment(start, end, c.Uint32()),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append the bot prefix if the user is a bot.
|
// Append the bot prefix if the user is a bot.
|
||||||
if m.User.Bot {
|
if m.User.Bot {
|
||||||
name.Content += " "
|
rich.Content += " "
|
||||||
name.Segments = append(name.Segments,
|
rich.Segments = append(rich.Segments,
|
||||||
colored.NewBlurple(segutil.Write(&name, "[BOT]")),
|
colored.NewBlurple(segutil.Write(rich, "[BOT]")),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append a clickable user popup.
|
// 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)
|
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 {
|
func (a Author) ID() cchat.ID {
|
||||||
|
@ -92,3 +111,53 @@ func (a Author) Name() text.Rich {
|
||||||
func (a Author) Avatar() string {
|
func (a Author) Avatar() string {
|
||||||
return a.avatar
|
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 {
|
type messageHeader struct {
|
||||||
id discord.MessageID
|
id discord.MessageID
|
||||||
time discord.Timestamp
|
time discord.Timestamp
|
||||||
|
nonce string
|
||||||
channelID discord.ChannelID
|
channelID discord.ChannelID
|
||||||
guildID discord.GuildID
|
guildID discord.GuildID
|
||||||
}
|
}
|
||||||
|
@ -34,6 +35,12 @@ func newHeader(msg discord.Message) messageHeader {
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newHeaderNonce(msg discord.Message, nonce string) messageHeader {
|
||||||
|
h := newHeader(msg)
|
||||||
|
h.nonce = nonce
|
||||||
|
return h
|
||||||
|
}
|
||||||
|
|
||||||
func NewHeaderDelete(d *gateway.MessageDeleteEvent) messageHeader {
|
func NewHeaderDelete(d *gateway.MessageDeleteEvent) messageHeader {
|
||||||
return messageHeader{
|
return messageHeader{
|
||||||
id: d.ID,
|
id: d.ID,
|
||||||
|
@ -47,6 +54,8 @@ func (m messageHeader) ID() cchat.ID {
|
||||||
return m.id.String()
|
return m.id.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m messageHeader) Nonce() string { return m.nonce }
|
||||||
|
|
||||||
func (m messageHeader) MessageID() discord.MessageID { return m.id }
|
func (m messageHeader) MessageID() discord.MessageID { return m.id }
|
||||||
func (m messageHeader) ChannelID() discord.ChannelID { return m.channelID }
|
func (m messageHeader) ChannelID() discord.ChannelID { return m.channelID }
|
||||||
func (m messageHeader) GuildID() discord.GuildID { return m.guildID }
|
func (m messageHeader) GuildID() discord.GuildID { return m.guildID }
|
||||||
|
@ -69,6 +78,7 @@ var (
|
||||||
_ cchat.MessageCreate = (*Message)(nil)
|
_ cchat.MessageCreate = (*Message)(nil)
|
||||||
_ cchat.MessageUpdate = (*Message)(nil)
|
_ cchat.MessageUpdate = (*Message)(nil)
|
||||||
_ cchat.MessageDelete = (*Message)(nil)
|
_ cchat.MessageDelete = (*Message)(nil)
|
||||||
|
_ cchat.Noncer = (*Message)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewMessageUpdateContent(msg discord.Message, s *state.Instance) Message {
|
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
|
// NewGuildMessageCreate uses the session to create a message. It does not do
|
||||||
// API calls. Member is optional.
|
// API calls. Member is optional. This is the only call that populates the Nonce
|
||||||
func NewMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Message {
|
// 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.
|
// This should not error.
|
||||||
g, err := s.Store.Guild(c.GuildID)
|
g, err := s.Store.Guild(c.GuildID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return NewMessage(c.Message, s, NewUser(c.Author, s))
|
return NewMessage(message, s, NewUser(c.Author, s))
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.Member == nil {
|
if c.Member == nil {
|
||||||
|
@ -111,10 +126,10 @@ func NewMessageCreate(c *gateway.MessageCreateEvent, s *state.Instance) Message
|
||||||
}
|
}
|
||||||
if c.Member == nil {
|
if c.Member == nil {
|
||||||
s.MemberState.RequestMember(c.GuildID, c.Author.ID)
|
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
|
// 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{
|
return Message{
|
||||||
messageHeader: newHeader(m),
|
messageHeader: newHeaderNonce(m, m.Nonce),
|
||||||
author: author,
|
author: author,
|
||||||
content: content,
|
content: content,
|
||||||
}
|
}
|
||||||
|
@ -184,6 +199,10 @@ func (m Message) Content() text.Rich {
|
||||||
return m.content
|
return m.content
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m Message) Nonce() string {
|
||||||
|
return m.nonce
|
||||||
|
}
|
||||||
|
|
||||||
func (m Message) Mentioned() bool {
|
func (m Message) Mentioned() bool {
|
||||||
return m.mentioned
|
return m.mentioned
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,8 +7,11 @@ import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/arikawa/gateway"
|
"github.com/diamondburned/arikawa/gateway"
|
||||||
"github.com/diamondburned/cchat"
|
"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/message"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
"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-discord/internal/funcutil"
|
||||||
"github.com/diamondburned/cchat/utils/empty"
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
)
|
)
|
||||||
|
@ -52,8 +55,9 @@ func (list *messageList) delete(id discord.MessageID) {
|
||||||
type Messages struct {
|
type Messages struct {
|
||||||
empty.Messenger
|
empty.Messenger
|
||||||
|
|
||||||
state *state.Instance
|
state *state.Instance
|
||||||
acList *activeList
|
acList *activeList
|
||||||
|
sentMsgs *nonce.Set
|
||||||
|
|
||||||
sender *Sender
|
sender *Sender
|
||||||
|
|
||||||
|
@ -64,19 +68,48 @@ type Messages struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMessages(s *state.Instance, acList *activeList, adder ChannelAdder) *Messages {
|
func NewMessages(s *state.Instance, acList *activeList, adder ChannelAdder) *Messages {
|
||||||
|
var sentMsgs nonce.Set
|
||||||
|
|
||||||
hubServer := &Messages{
|
hubServer := &Messages{
|
||||||
state: s,
|
state: s,
|
||||||
acList: acList,
|
acList: acList,
|
||||||
sender: NewSender(s, acList, adder),
|
sentMsgs: &sentMsgs,
|
||||||
|
sender: &Sender{
|
||||||
|
adder: adder,
|
||||||
|
acList: acList,
|
||||||
|
sentMsgs: &sentMsgs,
|
||||||
|
state: s,
|
||||||
|
},
|
||||||
messages: make(messageList, 0, 100),
|
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(
|
hubServer.cancel = funcutil.JoinCancels(
|
||||||
s.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
s.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
||||||
if msg.GuildID.IsValid() || acList.isActive(msg.ChannelID) {
|
if msg.GuildID.IsValid() || acList.isActive(msg.ChannelID) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We're not adding back messages we sent here, since we already
|
||||||
|
// have a separate channel for that.
|
||||||
|
|
||||||
hubServer.msgMutex.Lock()
|
hubServer.msgMutex.Lock()
|
||||||
hubServer.messages.append(msg.Message)
|
hubServer.messages.append(msg.Message)
|
||||||
hubServer.msgMutex.Unlock()
|
hubServer.msgMutex.Unlock()
|
||||||
|
@ -122,11 +155,32 @@ func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer
|
||||||
// Bind the handler.
|
// Bind the handler.
|
||||||
return funcutil.JoinCancels(
|
return funcutil.JoinCancels(
|
||||||
msgs.state.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
msgs.state.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
||||||
if msg.GuildID.IsValid() || msgs.acList.isActive(msg.ChannelID) {
|
if msg.GuildID.IsValid() {
|
||||||
return
|
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.ReadState.MarkRead(msg.ChannelID, msg.ID)
|
||||||
}),
|
}),
|
||||||
msgs.state.AddHandler(func(update *gateway.MessageUpdateEvent) {
|
msgs.state.AddHandler(func(update *gateway.MessageUpdateEvent) {
|
||||||
|
|
|
@ -7,7 +7,9 @@ import (
|
||||||
"github.com/diamondburned/arikawa/discord"
|
"github.com/diamondburned/arikawa/discord"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
"github.com/diamondburned/cchat-discord/internal/discord/channel/message/send"
|
"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"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
|
||||||
"github.com/diamondburned/cchat/utils/empty"
|
"github.com/diamondburned/cchat/utils/empty"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
@ -17,18 +19,26 @@ type ChannelAdder interface {
|
||||||
AddChannel(state *state.Instance, ch *discord.Channel)
|
AddChannel(state *state.Instance, ch *discord.Channel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: unexport Sender
|
||||||
|
|
||||||
type Sender struct {
|
type Sender struct {
|
||||||
empty.Sender
|
empty.Sender
|
||||||
adder ChannelAdder
|
adder ChannelAdder
|
||||||
acList *activeList
|
acList *activeList
|
||||||
state *state.Instance
|
sentMsgs *nonce.Set
|
||||||
|
state *state.Instance
|
||||||
|
|
||||||
|
completers complete.Completer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSender(s *state.Instance, acList *activeList, adder ChannelAdder) *Sender {
|
// mentionRegex matche the following:
|
||||||
return &Sender{adder: adder, acList: acList, state: s}
|
//
|
||||||
}
|
// <#123123>
|
||||||
|
// <#!12312> // This is OK because we're not sending it.
|
||||||
var mentionRegex = regexp.MustCompile(`^<@!?(\d+)> ?`)
|
// <@123123>
|
||||||
|
// <@!12312>
|
||||||
|
//
|
||||||
|
var mentionRegex = regexp.MustCompile(`(?m)^<(@|#)!?(\d+)> ?`)
|
||||||
|
|
||||||
// wrappedMessage wraps around a SendableMessage to override its content.
|
// wrappedMessage wraps around a SendableMessage to override its content.
|
||||||
type wrappedMessage struct {
|
type wrappedMessage struct {
|
||||||
|
@ -48,28 +58,40 @@ func (s *Sender) Send(sendable cchat.SendableMessage) error {
|
||||||
// Validate message.
|
// Validate message.
|
||||||
matches := mentionRegex.FindStringSubmatch(content)
|
matches := mentionRegex.FindStringSubmatch(content)
|
||||||
if matches == nil {
|
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 {
|
if err != nil {
|
||||||
return errors.Wrap(err, "failed to parse recipient ID")
|
return errors.Wrap(err, "failed to parse recipient ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
ch, err := s.state.CreatePrivateChannel(discord.UserID(targetID))
|
var channel *discord.Channel
|
||||||
if err != nil {
|
switch matches[1] {
|
||||||
return errors.Wrap(err, "failed to find DM channel")
|
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.adder.AddChannel(s.state, channel)
|
||||||
s.acList.add(ch.ID)
|
s.acList.add(channel.ID)
|
||||||
|
|
||||||
return send.Send(s.state, ch.ID, wrappedMessage{
|
sendData := send.WrapMessage(s.state, sendable)
|
||||||
SendableMessage: sendable,
|
sendData.Content = strings.TrimPrefix(content, matches[0])
|
||||||
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 {
|
func (s *Sender) AsCompleter() cchat.Completer {
|
||||||
// return complete.New(msgs)
|
return s.completers
|
||||||
// }
|
}
|
||||||
|
|
|
@ -60,11 +60,16 @@ func (acList *activeList) isActive(channelID discord.ChannelID) bool {
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (acList *activeList) add(chID discord.ChannelID) {
|
func (acList *activeList) add(chID discord.ChannelID) (changed bool) {
|
||||||
acList.mut.Lock()
|
acList.mut.Lock()
|
||||||
defer acList.mut.Unlock()
|
defer acList.mut.Unlock()
|
||||||
|
|
||||||
|
if _, ok := acList.active[chID]; ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
acList.active[chID] = struct{}{}
|
acList.active[chID] = struct{}{}
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server is the server (channel) that contains all incoming DM messages that
|
// Server is the server (channel) that contains all incoming DM messages that
|
||||||
|
|
|
@ -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/state"
|
||||||
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
"github.com/diamondburned/arikawa/utils/httputil/httpdriver"
|
||||||
"github.com/diamondburned/cchat"
|
"github.com/diamondburned/cchat"
|
||||||
|
"github.com/diamondburned/cchat-discord/internal/discord/state/nonce"
|
||||||
"github.com/diamondburned/ningen"
|
"github.com/diamondburned/ningen"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Instance struct {
|
type Instance struct {
|
||||||
*ningen.State
|
*ningen.State
|
||||||
|
Nonces *nonce.Map
|
||||||
|
|
||||||
|
// UserID is a constant user ID. It is guaranteed to be valid.
|
||||||
UserID discord.UserID
|
UserID discord.UserID
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var _ cchat.SessionSaver = (*Instance)(nil)
|
||||||
_ cchat.SessionSaver = (*Instance)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
// ErrInvalidSession is returned if SessionRestore is given a bad session.
|
||||||
var ErrInvalidSession = errors.New("invalid session")
|
var ErrInvalidSession = errors.New("invalid session")
|
||||||
|
@ -58,12 +60,12 @@ func New(s *state.State) (*Instance, error) {
|
||||||
// Prefetch user.
|
// Prefetch user.
|
||||||
u, err := s.Me()
|
u, err := s.Me()
|
||||||
if err != nil {
|
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)
|
n, err := ningen.FromState(s)
|
||||||
if err != nil {
|
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 {
|
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{
|
return &Instance{
|
||||||
UserID: u.ID,
|
UserID: u.ID,
|
||||||
State: n,
|
State: n,
|
||||||
|
Nonces: new(nonce.Map),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 New Issue