mirror of
https://github.com/diamondburned/cchat-discord.git
synced 2025-05-06 06:04:23 +00:00
Added WIP private messages, removed nonce
This commit is contained in:
parent
fc795ef7b6
commit
1155ccac34
2
go.mod
2
go.mod
|
@ -4,7 +4,7 @@ go 1.14
|
|||
|
||||
require (
|
||||
github.com/diamondburned/arikawa v1.3.6
|
||||
github.com/diamondburned/cchat v0.3.11
|
||||
github.com/diamondburned/cchat v0.3.12
|
||||
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
|
||||
|
|
2
go.sum
2
go.sum
|
@ -103,6 +103,8 @@ github.com/diamondburned/cchat v0.3.8 h1:vgFe8giVfwsAO+WpTYsTDIXvRUN48osVPNu0pZN
|
|||
github.com/diamondburned/cchat v0.3.8/go.mod h1:IlMtF+XIvAJh0GL/2yFdf0/34w+Hdy5A1GgvSwAXtQI=
|
||||
github.com/diamondburned/cchat v0.3.11 h1:C1f9Tp7Kz3t+T1SlepL1RS7b/kACAKWAIZXAgJEpCHg=
|
||||
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/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=
|
||||
|
|
|
@ -13,21 +13,28 @@ import (
|
|||
|
||||
type Channel struct {
|
||||
empty.Server
|
||||
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
commander cchat.Commander
|
||||
}
|
||||
|
||||
var _ cchat.Server = (*Channel)(nil)
|
||||
|
||||
func New(s *state.Instance, ch discord.Channel) (cchat.Server, error) {
|
||||
channel, err := NewChannel(s, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return channel, nil
|
||||
}
|
||||
|
||||
func NewChannel(s *state.Instance, ch discord.Channel) (Channel, error) {
|
||||
// Ensure the state keeps the channel's permission.
|
||||
_, err := s.Permissions(ch.ID, s.UserID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to get permission")
|
||||
return Channel{}, errors.Wrap(err, "Failed to get permission")
|
||||
}
|
||||
|
||||
sharedCh := &shared.Channel{
|
||||
sharedCh := shared.Channel{
|
||||
ID: ch.ID,
|
||||
GuildID: ch.GuildID,
|
||||
State: s,
|
||||
|
@ -50,7 +57,7 @@ func (ch Channel) Name() text.Rich {
|
|||
}
|
||||
|
||||
if c.NSFW {
|
||||
return text.Rich{Content: "#!" + c.Name}
|
||||
return text.Rich{Content: "#" + c.Name + " (nsfw)"}
|
||||
} else {
|
||||
return text.Rich{Content: "#" + c.Name}
|
||||
}
|
||||
|
|
|
@ -11,11 +11,11 @@ import (
|
|||
)
|
||||
|
||||
type Commander struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
msgCompl complete.Completer
|
||||
}
|
||||
|
||||
func NewCommander(ch *shared.Channel) cchat.Commander {
|
||||
func NewCommander(ch shared.Channel) cchat.Commander {
|
||||
return Commander{
|
||||
Channel: ch,
|
||||
msgCompl: complete.Completer{
|
||||
|
|
|
@ -10,7 +10,7 @@ type Command struct {
|
|||
Name string
|
||||
Args Arguments
|
||||
Desc string
|
||||
RunFunc func(*shared.Channel, []string) ([]byte, error) // words[1:]
|
||||
RunFunc func(shared.Channel, []string) ([]byte, error) // words[1:]
|
||||
}
|
||||
|
||||
func (cmd Command) writeHelp(builder *bytes.Buffer) {
|
||||
|
|
|
@ -29,7 +29,7 @@ func (cmds Commands) Help() []byte {
|
|||
|
||||
// Run runs a command with the given words. It errors out if the command is not
|
||||
// found.
|
||||
func (cmds Commands) Run(ch *shared.Channel, words []string) ([]byte, error) {
|
||||
func (cmds Commands) Run(ch shared.Channel, words []string) ([]byte, error) {
|
||||
if words[0] == "help" {
|
||||
return cmds.Help(), nil
|
||||
}
|
||||
|
@ -80,7 +80,7 @@ var World = Commands{
|
|||
Name: "send-embed",
|
||||
Args: Arguments{"-t title", "-c color", "description"},
|
||||
Desc: "Send a basic embed to the current channel",
|
||||
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
|
||||
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
|
||||
var embed discord.Embed
|
||||
var color uint // no Uint32Var
|
||||
|
||||
|
@ -107,7 +107,7 @@ var World = Commands{
|
|||
{
|
||||
Name: "info",
|
||||
Desc: "Print information as JSON",
|
||||
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
|
||||
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
|
||||
channel, err := ch.State.Channel(ch.ID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get channel")
|
||||
|
@ -124,7 +124,7 @@ var World = Commands{
|
|||
{
|
||||
Name: "list-channels",
|
||||
Desc: "Print all channels of this guild and their topics",
|
||||
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
|
||||
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
|
||||
channels, err := ch.State.Channels(ch.GuildID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get channels")
|
||||
|
@ -142,7 +142,7 @@ var World = Commands{
|
|||
Name: "presence",
|
||||
Args: Arguments{"mention:user"},
|
||||
Desc: "Print JSON of a member/user's presence state",
|
||||
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
|
||||
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
|
||||
if err := assertArgc(argv, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -164,7 +164,7 @@ var World = Commands{
|
|||
Name: "member",
|
||||
Args: Arguments{"mention:user"},
|
||||
Desc: "Print JSON of a member/user's member state",
|
||||
RunFunc: func(ch *shared.Channel, argv []string) ([]byte, error) {
|
||||
RunFunc: func(ch shared.Channel, argv []string) ([]byte, error) {
|
||||
if err := assertArgc(argv, 1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -8,12 +8,12 @@ import (
|
|||
)
|
||||
|
||||
type Actioner struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
var _ cchat.Actioner = (*Actioner)(nil)
|
||||
|
||||
func New(ch *shared.Channel) Actioner {
|
||||
func New(ch shared.Channel) Actioner {
|
||||
return Actioner{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@ import (
|
|||
)
|
||||
|
||||
type Backlogger struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
func New(ch *shared.Channel) cchat.Backlogger {
|
||||
func New(ch shared.Channel) cchat.Backlogger {
|
||||
return Backlogger{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ import (
|
|||
)
|
||||
|
||||
type Editor struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
func New(ch *shared.Channel) cchat.Editor {
|
||||
func New(ch shared.Channel) cchat.Editor {
|
||||
return Editor{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,10 +10,10 @@ import (
|
|||
)
|
||||
|
||||
type TypingIndicator struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
func NewTyping(ch *shared.Channel) cchat.TypingIndicator {
|
||||
func NewTyping(ch shared.Channel) cchat.TypingIndicator {
|
||||
return TypingIndicator{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ import (
|
|||
)
|
||||
|
||||
type UnreadIndicator struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
func NewUnread(ch *shared.Channel) cchat.UnreadIndicator {
|
||||
func NewUnread(ch shared.Channel) cchat.UnreadIndicator {
|
||||
return UnreadIndicator{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,13 +17,13 @@ import (
|
|||
)
|
||||
|
||||
type Member struct {
|
||||
channel *shared.Channel
|
||||
channel shared.Channel
|
||||
userID discord.UserID
|
||||
origName string // use if cache is stale
|
||||
}
|
||||
|
||||
// New creates a new list member. it.Member must not be nil.
|
||||
func NewMember(ch *shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember {
|
||||
func NewMember(ch shared.Channel, opItem gateway.GuildMemberListOpItem) cchat.ListMember {
|
||||
return &Member{
|
||||
channel: ch,
|
||||
userID: opItem.Member.User.ID,
|
||||
|
|
|
@ -31,10 +31,10 @@ func seekPrevGroup(l *member.List, ix int) (item, group gateway.GuildMemberListO
|
|||
}
|
||||
|
||||
type MemberLister struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
func New(ch *shared.Channel) cchat.MemberLister {
|
||||
func New(ch shared.Channel) cchat.MemberLister {
|
||||
return MemberLister{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ type Section struct {
|
|||
}
|
||||
|
||||
func NewSection(
|
||||
ch *shared.Channel,
|
||||
ch shared.Channel,
|
||||
listID string,
|
||||
group gateway.GuildMemberListGroup) cchat.MemberSection {
|
||||
|
||||
|
@ -78,7 +78,7 @@ func (s Section) AsMemberDynamicSection() cchat.MemberDynamicSection {
|
|||
func (s Section) IsMemberDynamicSection() bool { return true }
|
||||
|
||||
type DynamicSection struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
var _ cchat.MemberDynamicSection = (*DynamicSection)(nil)
|
||||
|
|
|
@ -23,12 +23,12 @@ import (
|
|||
|
||||
type Messenger struct {
|
||||
empty.Messenger
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
var _ cchat.Messenger = (*Messenger)(nil)
|
||||
|
||||
func New(ch *shared.Channel) Messenger {
|
||||
func New(ch shared.Channel) Messenger {
|
||||
return Messenger{Channel: ch}
|
||||
}
|
||||
|
||||
|
@ -129,7 +129,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 {
|
||||
|
|
|
@ -12,10 +12,10 @@ import (
|
|||
)
|
||||
|
||||
type Nicknamer struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
func New(ch *shared.Channel) cchat.Nicknamer {
|
||||
func New(ch shared.Channel) cchat.Nicknamer {
|
||||
return Nicknamer{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,12 +8,12 @@ import (
|
|||
)
|
||||
|
||||
type Completer struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
const MaxCompletion = 15
|
||||
|
||||
func New(ch *shared.Channel) cchat.Completer {
|
||||
func New(ch shared.Channel) cchat.Completer {
|
||||
return Completer{ch}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,40 +3,50 @@ package complete
|
|||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
)
|
||||
|
||||
func (ch Completer) CompleteEmojis(word string) (entries []cchat.CompletionEntry) {
|
||||
return CompleteEmojis(ch.State, ch.GuildID, word)
|
||||
}
|
||||
|
||||
func CompleteEmojis(s *state.Instance, gID discord.GuildID, word string) []cchat.CompletionEntry {
|
||||
// Ignore if empty word.
|
||||
if word == "" {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
e, err := ch.State.EmojiState.Get(ch.GuildID)
|
||||
e, err := s.EmojiState.Get(gID)
|
||||
if err != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
var match = strings.ToLower(word)
|
||||
var entries = make([]cchat.CompletionEntry, 0, MaxCompletion)
|
||||
|
||||
for _, guild := range e {
|
||||
for _, emoji := range guild.Emojis {
|
||||
if contains(match, emoji.Name) {
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: emoji.String(),
|
||||
Text: text.Rich{Content: ":" + emoji.Name + ":"},
|
||||
Secondary: text.Rich{Content: guild.Name},
|
||||
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
|
||||
Image: true,
|
||||
})
|
||||
if len(entries) >= MaxCompletion {
|
||||
return
|
||||
}
|
||||
if !contains(match, emoji.Name) {
|
||||
continue
|
||||
}
|
||||
|
||||
entries = append(entries, cchat.CompletionEntry{
|
||||
Raw: emoji.String(),
|
||||
Text: text.Rich{Content: ":" + emoji.Name + ":"},
|
||||
Secondary: text.Rich{Content: guild.Name},
|
||||
IconURL: urlutils.Sized(emoji.EmojiURL(), 32), // small
|
||||
Image: true,
|
||||
})
|
||||
|
||||
if len(entries) >= MaxCompletion {
|
||||
return entries
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
return entries
|
||||
}
|
||||
|
|
|
@ -6,28 +6,30 @@ import (
|
|||
"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/state"
|
||||
)
|
||||
|
||||
type Sender struct {
|
||||
*shared.Channel
|
||||
shared.Channel
|
||||
}
|
||||
|
||||
var _ cchat.Sender = (*Sender)(nil)
|
||||
|
||||
func New(ch *shared.Channel) Sender {
|
||||
func New(ch shared.Channel) Sender {
|
||||
return Sender{ch}
|
||||
}
|
||||
|
||||
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 noncer := msg.AsNoncer(); noncer != nil {
|
||||
send.Nonce = noncer.Nonce()
|
||||
}
|
||||
if attacher := msg.AsAttachments(); attacher != nil {
|
||||
send.Files = addAttachments(attacher.Attachments())
|
||||
}
|
||||
|
||||
_, err := s.State.SendMessageComplex(s.ID, send)
|
||||
_, err := s.SendMessageComplex(chID, send)
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
64
internal/discord/channel/private.go
Normal file
64
internal/discord/channel/private.go
Normal file
|
@ -0,0 +1,64 @@
|
|||
package channel
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Private struct {
|
||||
Channel
|
||||
}
|
||||
|
||||
var _ cchat.Server = (*Private)(nil)
|
||||
|
||||
func NewPrivate(s *state.Instance, ch discord.Channel) (cchat.Server, error) {
|
||||
if ch.GuildID.IsValid() {
|
||||
return nil, errors.New("channel has valid guild ID: not a DM")
|
||||
}
|
||||
|
||||
channel, err := NewChannel(s, ch)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return Private{Channel: channel}, nil
|
||||
}
|
||||
|
||||
func (priv Private) AsIconer() cchat.Iconer {
|
||||
return NewAvatarIcon(priv.State)
|
||||
}
|
||||
|
||||
type AvatarIcon struct {
|
||||
State *state.Instance
|
||||
}
|
||||
|
||||
func NewAvatarIcon(state *state.Instance) cchat.Iconer {
|
||||
return AvatarIcon{state}
|
||||
}
|
||||
|
||||
func (avy AvatarIcon) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||
u, err := avy.State.WithContext(ctx).Me()
|
||||
if err != nil {
|
||||
// This shouldn't happen.
|
||||
return nil, errors.Wrap(err, "Failed to get guild")
|
||||
}
|
||||
|
||||
// Used for comparison.
|
||||
if u.Avatar != "" {
|
||||
iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL()))
|
||||
}
|
||||
|
||||
selfID := u.ID
|
||||
|
||||
return avy.State.AddHandler(func(update *gateway.UserUpdateEvent) {
|
||||
if selfID == update.ID {
|
||||
iconer.SetIcon(urlutils.AvatarURL(update.AvatarURL()))
|
||||
}
|
||||
}), nil
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
package shared
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
)
|
||||
|
@ -33,6 +35,9 @@ func (ch Channel) Messages() ([]discord.Message, error) {
|
|||
}
|
||||
|
||||
func (ch Channel) Guild() (*discord.Guild, error) {
|
||||
if !ch.GuildID.IsValid() {
|
||||
return nil, errors.New("channel not in guild")
|
||||
}
|
||||
return ch.State.Store.Guild(ch.GuildID)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ type messageHeader struct {
|
|||
time discord.Timestamp
|
||||
channelID discord.ChannelID
|
||||
guildID discord.GuildID
|
||||
nonce string
|
||||
}
|
||||
|
||||
var _ cchat.MessageHeader = (*messageHeader)(nil)
|
||||
|
@ -28,7 +27,6 @@ func newHeader(msg discord.Message) messageHeader {
|
|||
time: msg.Timestamp,
|
||||
channelID: msg.ChannelID,
|
||||
guildID: msg.GuildID,
|
||||
nonce: msg.Nonce,
|
||||
}
|
||||
if msg.EditedTimestamp.IsValid() {
|
||||
h.time = msg.EditedTimestamp
|
||||
|
@ -49,6 +47,10 @@ func (m messageHeader) ID() cchat.ID {
|
|||
return m.id.String()
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
func (m messageHeader) Time() time.Time {
|
||||
return m.time.Time()
|
||||
}
|
||||
|
@ -67,7 +69,6 @@ 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 {
|
||||
|
@ -183,10 +184,6 @@ 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
|
||||
}
|
||||
|
|
149
internal/discord/private/hub/messages.go
Normal file
149
internal/discord/private/hub/messages.go
Normal file
|
@ -0,0 +1,149 @@
|
|||
package hub
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/arikawa/gateway"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/message"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/funcutil"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
)
|
||||
|
||||
const maxMessages = 100
|
||||
|
||||
type messageList []discord.Message
|
||||
|
||||
func (list messageList) idx(id discord.MessageID) int {
|
||||
for i, msg := range list {
|
||||
if msg.ID == id {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (list *messageList) append(msg discord.Message) {
|
||||
*list = append(*list, msg)
|
||||
|
||||
// cap the length
|
||||
if len(*list) > maxMessages {
|
||||
copy(*list, (*list)[1:]) // shift left once
|
||||
(*list)[len(*list)-1] = discord.Message{} // nil out last to not memory leak
|
||||
*list = (*list)[:len(*list)-1] // slice it away
|
||||
}
|
||||
}
|
||||
|
||||
func (list *messageList) swap(newMsg discord.Message) {
|
||||
if idx := list.idx(newMsg.ID); idx > -1 {
|
||||
(*list)[idx] = newMsg
|
||||
}
|
||||
}
|
||||
|
||||
func (list *messageList) delete(id discord.MessageID) {
|
||||
if idx := list.idx(id); idx > -1 {
|
||||
*list = append((*list)[:idx], (*list)[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
type Messages struct {
|
||||
empty.Messenger
|
||||
|
||||
state *state.Instance
|
||||
acList *activeList
|
||||
|
||||
sender *Sender
|
||||
|
||||
msgMutex sync.Mutex
|
||||
messages messageList
|
||||
|
||||
cancel func()
|
||||
}
|
||||
|
||||
func NewMessages(s *state.Instance, acList *activeList, adder ChannelAdder) *Messages {
|
||||
hubServer := &Messages{
|
||||
state: s,
|
||||
acList: acList,
|
||||
sender: NewSender(s, acList, adder),
|
||||
messages: make(messageList, 0, 100),
|
||||
}
|
||||
|
||||
hubServer.cancel = funcutil.JoinCancels(
|
||||
s.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
||||
if msg.GuildID.IsValid() || acList.isActive(msg.ChannelID) {
|
||||
return
|
||||
}
|
||||
|
||||
hubServer.msgMutex.Lock()
|
||||
hubServer.messages.append(msg.Message)
|
||||
hubServer.msgMutex.Unlock()
|
||||
}),
|
||||
s.AddHandler(func(update *gateway.MessageUpdateEvent) {
|
||||
if update.GuildID.IsValid() || acList.isActive(update.ChannelID) {
|
||||
return
|
||||
}
|
||||
|
||||
// The event itself is unreliable, so we must rely on the state.
|
||||
m, err := hubServer.state.Message(update.ChannelID, update.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
hubServer.msgMutex.Lock()
|
||||
hubServer.messages.swap(*m)
|
||||
hubServer.msgMutex.Unlock()
|
||||
}),
|
||||
s.AddHandler(func(del *gateway.MessageDeleteEvent) {
|
||||
if del.GuildID.IsValid() || acList.isActive(del.ChannelID) {
|
||||
return
|
||||
}
|
||||
|
||||
hubServer.msgMutex.Lock()
|
||||
hubServer.messages.delete(del.ID)
|
||||
hubServer.msgMutex.Unlock()
|
||||
}),
|
||||
)
|
||||
|
||||
return hubServer
|
||||
}
|
||||
|
||||
func (msgs *Messages) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
|
||||
msgs.msgMutex.Lock()
|
||||
|
||||
for _, msg := range msgs.messages {
|
||||
ct.CreateMessage(message.NewDirectMessage(msg, msgs.state))
|
||||
}
|
||||
|
||||
msgs.msgMutex.Unlock()
|
||||
|
||||
// Bind the handler.
|
||||
return funcutil.JoinCancels(
|
||||
msgs.state.AddHandler(func(msg *gateway.MessageCreateEvent) {
|
||||
if msg.GuildID.IsValid() || msgs.acList.isActive(msg.ChannelID) {
|
||||
return
|
||||
}
|
||||
|
||||
ct.CreateMessage(message.NewMessageCreate(msg, msgs.state))
|
||||
msgs.state.ReadState.MarkRead(msg.ChannelID, msg.ID)
|
||||
}),
|
||||
msgs.state.AddHandler(func(update *gateway.MessageUpdateEvent) {
|
||||
if update.GuildID.IsValid() || msgs.acList.isActive(update.ChannelID) {
|
||||
return
|
||||
}
|
||||
|
||||
ct.UpdateMessage(message.NewMessageUpdateContent(update.Message, msgs.state))
|
||||
}),
|
||||
msgs.state.AddHandler(func(del *gateway.MessageDeleteEvent) {
|
||||
if del.GuildID.IsValid() || msgs.acList.isActive(del.ChannelID) {
|
||||
return
|
||||
}
|
||||
|
||||
ct.DeleteMessage(message.NewHeaderDelete(del))
|
||||
}),
|
||||
), nil
|
||||
}
|
||||
|
||||
func (msgs *Messages) AsSender() cchat.Sender { return msgs.sender }
|
75
internal/discord/private/hub/sender.go
Normal file
75
internal/discord/private/hub/sender.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
package hub
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"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/state"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ChannelAdder is used to add a new direct message channel into a container.
|
||||
type ChannelAdder interface {
|
||||
AddChannel(state *state.Instance, ch *discord.Channel)
|
||||
}
|
||||
|
||||
type Sender struct {
|
||||
empty.Sender
|
||||
adder ChannelAdder
|
||||
acList *activeList
|
||||
state *state.Instance
|
||||
}
|
||||
|
||||
func NewSender(s *state.Instance, acList *activeList, adder ChannelAdder) *Sender {
|
||||
return &Sender{adder: adder, acList: acList, state: s}
|
||||
}
|
||||
|
||||
var mentionRegex = regexp.MustCompile(`^<@!?(\d+)> ?`)
|
||||
|
||||
// wrappedMessage wraps around a SendableMessage to override its content.
|
||||
type wrappedMessage struct {
|
||||
cchat.SendableMessage
|
||||
content string
|
||||
}
|
||||
|
||||
func (wrMsg wrappedMessage) Content() string {
|
||||
return wrMsg.content
|
||||
}
|
||||
|
||||
func (s *Sender) CanAttach() bool { return true }
|
||||
|
||||
func (s *Sender) Send(sendable cchat.SendableMessage) error {
|
||||
content := sendable.Content()
|
||||
|
||||
// Validate message.
|
||||
matches := mentionRegex.FindStringSubmatch(content)
|
||||
if matches == nil {
|
||||
return errors.New("messages sent here must start with a mention")
|
||||
}
|
||||
|
||||
targetID, err := discord.ParseSnowflake(matches[1])
|
||||
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")
|
||||
}
|
||||
|
||||
s.adder.AddChannel(s.state, ch)
|
||||
s.acList.add(ch.ID)
|
||||
|
||||
return send.Send(s.state, ch.ID, wrappedMessage{
|
||||
SendableMessage: sendable,
|
||||
content: strings.TrimPrefix(content, matches[0]),
|
||||
})
|
||||
}
|
||||
|
||||
// func (msgs *Messages) AsCompleter() cchat.Completer {
|
||||
// return complete.New(msgs)
|
||||
// }
|
103
internal/discord/private/hub/server.go
Normal file
103
internal/discord/private/hub/server.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
package hub
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// automatically add all channels with active messages within the past 48 hours.
|
||||
const autoAddActive = 24 * time.Hour
|
||||
|
||||
// activeList contains a list of channel IDs that should be put into its own
|
||||
// channels.
|
||||
type activeList struct {
|
||||
mut sync.Mutex
|
||||
active map[discord.ChannelID]struct{}
|
||||
}
|
||||
|
||||
func makeActiveList(s *state.Instance) (*activeList, error) {
|
||||
channels, err := s.PrivateChannels()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to get private channels")
|
||||
}
|
||||
|
||||
ids := make(map[discord.ChannelID]struct{}, len(channels))
|
||||
now := time.Now()
|
||||
|
||||
for _, channel := range channels {
|
||||
if channel.LastMessageID.Time().Add(autoAddActive).After(now) {
|
||||
ids[channel.ID] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
return &activeList{active: ids}, nil
|
||||
}
|
||||
|
||||
func (acList *activeList) list() []discord.ChannelID {
|
||||
acList.mut.Lock()
|
||||
defer acList.mut.Unlock()
|
||||
|
||||
var channelIDs = make([]discord.ChannelID, 0, len(acList.active))
|
||||
for channelID := range acList.active {
|
||||
channelIDs = append(channelIDs, channelID)
|
||||
}
|
||||
|
||||
return channelIDs
|
||||
}
|
||||
|
||||
func (acList *activeList) isActive(channelID discord.ChannelID) bool {
|
||||
acList.mut.Lock()
|
||||
defer acList.mut.Unlock()
|
||||
|
||||
_, ok := acList.active[channelID]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (acList *activeList) add(chID discord.ChannelID) {
|
||||
acList.mut.Lock()
|
||||
defer acList.mut.Unlock()
|
||||
|
||||
acList.active[chID] = struct{}{}
|
||||
}
|
||||
|
||||
// Server is the server (channel) that contains all incoming DM messages that
|
||||
// are not being listened.
|
||||
type Server struct {
|
||||
empty.Server
|
||||
acList *activeList
|
||||
msgs *Messages
|
||||
}
|
||||
|
||||
func New(s *state.Instance, adder ChannelAdder) (*Server, error) {
|
||||
acList, err := makeActiveList(s)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to make active guild list")
|
||||
}
|
||||
|
||||
return &Server{
|
||||
acList: acList,
|
||||
msgs: NewMessages(s, acList, adder),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (hub *Server) ID() cchat.ID { return "!!!hub-server!!!" }
|
||||
|
||||
func (hub *Server) Name() text.Rich { return text.Plain("Incoming Messages") }
|
||||
|
||||
// ActiveChannelIDs returns the list of active channel IDs, that is, the channel
|
||||
// IDs that should be displayed separately.
|
||||
func (hub *Server) ActiveChannelIDs() []discord.ChannelID {
|
||||
return hub.acList.list()
|
||||
}
|
||||
|
||||
// Close unbinds the message handlers from the hub, invalidating it forever.
|
||||
func (hub *Server) Close() { hub.msgs.cancel() }
|
||||
|
||||
func (hub *Server) AsMessenger() cchat.Messenger { return hub.msgs }
|
127
internal/discord/private/private.go
Normal file
127
internal/discord/private/private.go
Normal file
|
@ -0,0 +1,127 @@
|
|||
package private
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/diamondburned/arikawa/discord"
|
||||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/channel"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/private/hub"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
"github.com/diamondburned/cchat/utils/empty"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// I don't think the cchat specs said anything about sharing a cchat.Server, so
|
||||
// we might need to do this. Nevertheless, it seems overkill.
|
||||
type containerSet struct {
|
||||
mut sync.Mutex
|
||||
set map[cchat.ServersContainer]struct{}
|
||||
}
|
||||
|
||||
func newContainerSet() *containerSet {
|
||||
return &containerSet{
|
||||
set: map[cchat.ServersContainer]struct{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func (cset *containerSet) Register(container cchat.ServersContainer) {
|
||||
cset.mut.Lock()
|
||||
cset.set[container] = struct{}{}
|
||||
cset.mut.Unlock()
|
||||
}
|
||||
|
||||
// prependServer wraps around Server to always prepend this wrapped server on
|
||||
// top of the servers container.
|
||||
type prependServer struct{ cchat.Server }
|
||||
|
||||
// PreviousID returns the appropriate parameters to prepend this server.
|
||||
func (ps prependServer) PreviousID() (cchat.ID, bool) { return "", false }
|
||||
|
||||
func (cset *containerSet) AddChannel(s *state.Instance, ch *discord.Channel) {
|
||||
c, err := channel.New(s, *ch)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
replace := prependServer{Server: c}
|
||||
|
||||
cset.mut.Lock()
|
||||
|
||||
for container := range cset.set {
|
||||
container.UpdateServer(replace)
|
||||
}
|
||||
|
||||
cset.mut.Unlock()
|
||||
}
|
||||
|
||||
type Private struct {
|
||||
empty.Server
|
||||
state *state.Instance
|
||||
hub *hub.Server
|
||||
containers *containerSet
|
||||
}
|
||||
|
||||
func New(s *state.Instance) (cchat.Server, error) {
|
||||
containers := newContainerSet()
|
||||
|
||||
hubServer, err := hub.New(s, containers)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to make hub server")
|
||||
}
|
||||
|
||||
return Private{
|
||||
state: s,
|
||||
hub: hubServer,
|
||||
containers: containers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (priv Private) ID() cchat.ID {
|
||||
// Not even a number, so no chance of colliding with snowflakes.
|
||||
return "!!!private-container!!!"
|
||||
}
|
||||
|
||||
func (priv Private) Name() text.Rich {
|
||||
return text.Plain("Private Channels")
|
||||
}
|
||||
|
||||
func (priv Private) AsLister() cchat.Lister { return priv }
|
||||
|
||||
func (priv Private) Servers(container cchat.ServersContainer) error {
|
||||
activeIDs := priv.hub.ActiveChannelIDs()
|
||||
channels := make([]*discord.Channel, 0, len(activeIDs))
|
||||
|
||||
for _, id := range activeIDs {
|
||||
c, err := priv.state.Channel(id)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to get private channel")
|
||||
}
|
||||
|
||||
channels = append(channels, c)
|
||||
}
|
||||
|
||||
// Sort so that channels with the largest last message ID (and therefore the
|
||||
// latest message) will be on top.
|
||||
sort.Slice(channels, func(i, j int) bool {
|
||||
return channels[i].LastMessageID > channels[j].LastMessageID
|
||||
})
|
||||
|
||||
servers := make([]cchat.Server, len(channels)+1)
|
||||
servers[0] = priv.hub
|
||||
|
||||
for i, ch := range channels {
|
||||
c, err := channel.New(priv.state, *ch)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create server for private channel")
|
||||
}
|
||||
|
||||
servers[i] = c
|
||||
}
|
||||
|
||||
container.SetServers(servers)
|
||||
priv.containers.Register(container)
|
||||
return nil
|
||||
}
|
|
@ -8,6 +8,7 @@ import (
|
|||
"github.com/diamondburned/cchat"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/folder"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/guild"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/private"
|
||||
"github.com/diamondburned/cchat-discord/internal/discord/state"
|
||||
"github.com/diamondburned/cchat-discord/internal/urlutils"
|
||||
"github.com/diamondburned/cchat/text"
|
||||
|
@ -20,22 +21,31 @@ var ErrMFA = session.ErrMFA
|
|||
|
||||
type Session struct {
|
||||
empty.Session
|
||||
*state.Instance
|
||||
private cchat.Server
|
||||
state *state.Instance
|
||||
}
|
||||
|
||||
func NewFromInstance(i *state.Instance) (cchat.Session, error) {
|
||||
return &Session{Instance: i}, nil
|
||||
priv, err := private.New(i)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "failed to make main private server")
|
||||
}
|
||||
|
||||
return &Session{
|
||||
private: priv,
|
||||
state: i,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Session) ID() cchat.ID {
|
||||
return s.UserID.String()
|
||||
return s.state.UserID.String()
|
||||
}
|
||||
|
||||
func (s *Session) Name() text.Rich {
|
||||
u, err := s.Store.Me()
|
||||
u, err := s.state.Store.Me()
|
||||
if err != nil {
|
||||
// This shouldn't happen, ever.
|
||||
return text.Rich{Content: "<@" + s.UserID.String() + ">"}
|
||||
return text.Rich{Content: "<@" + s.state.UserID.String() + ">"}
|
||||
}
|
||||
|
||||
return text.Rich{Content: u.Username + "#" + u.Discriminator}
|
||||
|
@ -44,7 +54,7 @@ func (s *Session) Name() text.Rich {
|
|||
func (s *Session) AsIconer() cchat.Iconer { return s }
|
||||
|
||||
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(), error) {
|
||||
u, err := s.Me()
|
||||
u, err := s.state.Me()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "Failed to get the current user")
|
||||
}
|
||||
|
@ -52,28 +62,28 @@ func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) (func(),
|
|||
// Thanks to arikawa, AvatarURL is never empty.
|
||||
iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL()))
|
||||
|
||||
return s.AddHandler(func(*gateway.UserUpdateEvent) {
|
||||
return s.state.AddHandler(func(*gateway.UserUpdateEvent) {
|
||||
// Bypass the event and use the state cache.
|
||||
if u, err := s.Store.Me(); err == nil {
|
||||
if u, err := s.state.Store.Me(); err == nil {
|
||||
iconer.SetIcon(urlutils.AvatarURL(u.AvatarURL()))
|
||||
}
|
||||
}), nil
|
||||
}
|
||||
|
||||
func (s *Session) Disconnect() error {
|
||||
return s.Close()
|
||||
return s.state.Close()
|
||||
}
|
||||
|
||||
func (s *Session) AsSessionSaver() cchat.SessionSaver { return s.Instance }
|
||||
func (s *Session) AsSessionSaver() cchat.SessionSaver { return s.state }
|
||||
|
||||
func (s *Session) Servers(container cchat.ServersContainer) error {
|
||||
// Reset the entire container when the session is closed.
|
||||
s.AddHandler(func(*session.Closed) {
|
||||
s.state.AddHandler(func(*session.Closed) {
|
||||
container.SetServers(nil)
|
||||
})
|
||||
|
||||
// Set the entire container again once reconnected.
|
||||
s.AddHandler(func(*ningen.Connected) {
|
||||
s.state.AddHandler(func(*ningen.Connected) {
|
||||
s.servers(container)
|
||||
})
|
||||
|
||||
|
@ -81,22 +91,26 @@ func (s *Session) Servers(container cchat.ServersContainer) error {
|
|||
}
|
||||
|
||||
func (s *Session) servers(container cchat.ServersContainer) error {
|
||||
// TODO: remove this once v2 is used, so we could swap it with a getter.
|
||||
ready := s.state.Ready
|
||||
|
||||
switch {
|
||||
// If the user has guild folders:
|
||||
case len(s.Ready.Settings.GuildFolders) > 0:
|
||||
case len(ready.Settings.GuildFolders) > 0:
|
||||
// TODO: account for missing guilds.
|
||||
var toplevels = make([]cchat.Server, 0, len(s.Ready.Settings.GuildFolders))
|
||||
toplevels := make([]cchat.Server, 1, len(ready.Settings.GuildFolders)+1)
|
||||
toplevels[0] = s.private
|
||||
|
||||
for _, guildFolder := range s.Ready.Settings.GuildFolders {
|
||||
for _, guildFolder := range ready.Settings.GuildFolders {
|
||||
// TODO: correct.
|
||||
switch {
|
||||
case guildFolder.ID != 0:
|
||||
fallthrough
|
||||
case len(guildFolder.GuildIDs) > 1:
|
||||
toplevels = append(toplevels, folder.New(s.Instance, guildFolder))
|
||||
toplevels = append(toplevels, folder.New(s.state, guildFolder))
|
||||
|
||||
case len(guildFolder.GuildIDs) == 1:
|
||||
g, err := guild.NewFromID(s.Instance, guildFolder.GuildIDs[0])
|
||||
g, err := guild.NewFromID(s.state, guildFolder.GuildIDs[0])
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
@ -108,11 +122,12 @@ func (s *Session) servers(container cchat.ServersContainer) error {
|
|||
|
||||
// If the user doesn't have guild folders but has sorted their guilds
|
||||
// before:
|
||||
case len(s.Ready.Settings.GuildPositions) > 0:
|
||||
var guilds = make([]cchat.Server, 0, len(s.Ready.Settings.GuildPositions))
|
||||
case len(ready.Settings.GuildPositions) > 0:
|
||||
guilds := make([]cchat.Server, 1, len(ready.Settings.GuildPositions)+1)
|
||||
guilds[0] = s.private
|
||||
|
||||
for _, id := range s.Ready.Settings.GuildPositions {
|
||||
g, err := guild.NewFromID(s.Instance, id)
|
||||
for _, id := range ready.Settings.GuildPositions {
|
||||
g, err := guild.NewFromID(s.state, id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
@ -123,14 +138,16 @@ func (s *Session) servers(container cchat.ServersContainer) error {
|
|||
|
||||
// None of the above:
|
||||
default:
|
||||
g, err := s.Guilds()
|
||||
g, err := s.state.Guilds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var servers = make([]cchat.Server, len(g))
|
||||
servers := make([]cchat.Server, len(g)+1)
|
||||
servers[0] = s.private
|
||||
|
||||
for i := range g {
|
||||
servers[i] = guild.New(s.Instance, &g[i])
|
||||
servers[i+1] = guild.New(s.state, &g[i])
|
||||
}
|
||||
|
||||
container.SetServers(servers)
|
||||
|
|
|
@ -10,7 +10,7 @@ func NewCancels() func(...func()) []func() {
|
|||
}
|
||||
|
||||
// JoinCancels joins multiple cancel callbacks into one.
|
||||
func JoinCancels(cancellers []func()) func() {
|
||||
func JoinCancels(cancellers ...func()) func() {
|
||||
return func() {
|
||||
for _, c := range cancellers {
|
||||
c()
|
||||
|
|
|
@ -187,8 +187,6 @@ func (r *Text) RenderNode(n ast.Node, enter bool) (ast.WalkStatus, error) {
|
|||
return f(r, n, enter), nil
|
||||
}
|
||||
|
||||
log.Println("unknown kind:", n.Kind())
|
||||
|
||||
switch n := n.(type) {
|
||||
case *ast.Document:
|
||||
case *ast.Paragraph:
|
||||
|
|
Loading…
Reference in a new issue