1
0
Fork 0
mirror of https://github.com/diamondburned/cchat-discord.git synced 2025-01-23 18:56:39 +00:00

Mostly finished.

Commits will be more descriptive from here on out.
This commit is contained in:
diamondburned (Forefront) 2020-06-15 20:57:33 -07:00
parent d93cf981a1
commit 87688060e9
7 changed files with 457 additions and 21 deletions

View file

@ -1,9 +1,14 @@
package discord
import (
"context"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/segments"
"github.com/diamondburned/cchat/text"
"github.com/pkg/errors"
)
type Channel struct {
@ -13,7 +18,17 @@ type Channel struct {
session *Session
}
func NewChannel(s *Session, ch *discord.Channel) *Channel {
var (
_ cchat.Server = (*Channel)(nil)
_ cchat.ServerMessage = (*Channel)(nil)
_ cchat.ServerMessageSender = (*Channel)(nil)
// _ cchat.ServerMessageSendCompleter = (*Channel)(nil)
_ cchat.ServerNickname = (*Channel)(nil)
// _ cchat.ServerMessageEditor = (*Channel)(nil)
// _ cchat.ServerMessageActioner = (*Channel)(nil)
)
func NewChannel(s *Session, ch discord.Channel) *Channel {
return &Channel{
id: ch.ID,
guildID: ch.GuildID,
@ -30,6 +45,128 @@ func (ch *Channel) Name() text.Rich {
return text.Rich{Content: "#" + ch.name}
}
func (ch *Channel) Nickname(labeler cchat.LabelContainer) error {
func (ch *Channel) Nickname(ctx context.Context, labeler cchat.LabelContainer) error {
// We don't have a nickname if we're not in a guild.
if !ch.guildID.Valid() {
return nil
}
state := ch.session.WithContext(ctx)
// MemberColor should fill up the state cache.
c, err := state.MemberColor(ch.guildID, ch.session.userID)
if err != nil {
return errors.Wrap(err, "Failed to get self member color")
}
m, err := state.Member(ch.guildID, ch.session.userID)
if err != nil {
return errors.Wrap(err, "Failed to get self member")
}
var rich = text.Rich{Content: m.User.Username}
if m.Nick != "" {
rich.Content = m.Nick
}
if c > 0 {
rich.Segments = []text.Segment{
segments.NewColored(len(rich.Content), c.Uint32()),
}
}
labeler.SetLabel(rich)
return nil
}
func (ch *Channel) JoinServer(ctx context.Context, ct cchat.MessagesContainer) (func(), error) {
state := ch.session.WithContext(ctx)
m, err := state.Messages(ch.id)
if err != nil {
return nil, err
}
var addcancel = newCancels()
if ch.guildID.Valid() {
// Create the backlog without any member information.
g, err := state.Guild(ch.guildID)
if err != nil {
return nil, errors.Wrap(err, "Failed to get guild")
}
// Listen to new members before creating the backlog and requesting members.
addcancel(ch.session.AddHandler(func(c *gateway.GuildMembersChunkEvent) {
m, err := ch.session.Store.Messages(ch.id)
if err != nil {
// TODO: log
return
}
g, err := ch.session.Store.Guild(c.GuildID)
if err != nil {
return
}
for _, member := range c.Members {
// Loop over all messages and replace the author.
for _, msg := range m {
if msg.Author.ID != member.User.ID {
continue
}
ct.UpdateMessage(NewMessageUpdateAuthor(msg, member, *g))
}
}
}))
for _, m := range m {
ct.CreateMessage(NewBacklogMessage(m, ch.session, *g))
}
} else {
for _, m := range m {
ct.CreateMessage(NewDirectMessage(m))
}
}
// Bind the handler.
addcancel(
ch.session.AddHandler(func(m *gateway.MessageCreateEvent) {
ct.CreateMessage(NewMessageWithMember(m.Message, ch.session, m.Member))
}),
ch.session.AddHandler(func(m *gateway.MessageUpdateEvent) {
// If the updated content is empty. TODO: add embed support.
if m.Content == "" {
return
}
ct.UpdateMessage(NewMessageUpdateContent(m.Message))
}),
ch.session.AddHandler(func(m *gateway.MessageDeleteEvent) {
ct.DeleteMessage(NewHeaderDelete(m))
}),
)
return joinCancels(addcancel()), nil
}
func (ch *Channel) SendMessage(msg cchat.SendableMessage) error {
_, err := ch.session.SendText(ch.id, msg.Content())
return err
}
func newCancels() func(...func()) []func() {
var cancels []func()
return func(appended ...func()) []func() {
cancels = append(cancels, appended...)
return cancels
}
}
func joinCancels(cancellers []func()) func() {
return func() {
for _, c := range cancellers {
c()
}
}
}

3
go.mod
View file

@ -4,6 +4,7 @@ go 1.14
require (
github.com/diamondburned/arikawa v0.9.4
github.com/diamondburned/cchat v0.0.26
github.com/diamondburned/cchat v0.0.28
github.com/diamondburned/ningen v0.0.0-20200610212436-159f7105a2be
github.com/pkg/errors v0.9.1
)

8
go.sum
View file

@ -1,13 +1,21 @@
github.com/diamondburned/arikawa v0.8.7-0.20200522214036-530bff74a2c6/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/arikawa v0.9.4 h1:Mrp0Vz9R2afbvhWS6m/oLIQy22/uxXb459LUv7qrZPA=
github.com/diamondburned/arikawa v0.9.4/go.mod h1:nIhVIatzTQhPUa7NB8w4koG1RF9gYbpAr8Fj8sKq660=
github.com/diamondburned/cchat v0.0.26 h1:QBt4d65uzUPJz3jF8b2pJ09Jz8LeBRyG2ol47FOy0g0=
github.com/diamondburned/cchat v0.0.26/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/cchat v0.0.28 h1:+1VnltW0rl8/NZTUP+x89jVhi3YTTR+e6iLprZ7HcwM=
github.com/diamondburned/cchat v0.0.28/go.mod h1:+zXktogE45A0om4fT6B/z6Ii7FXNafjxsNspI0rlhbU=
github.com/diamondburned/ningen v0.0.0-20200610212436-159f7105a2be h1:mUw8X/YzJGFSdL8y3Q/XqyzqPyIMNVSDyZGOP3JXgJA=
github.com/diamondburned/ningen v0.0.0-20200610212436-159f7105a2be/go.mod h1:B2hq2B4va1MlnMmXuv9vXmyu9gscxJLmwrmcSB1Les8=
github.com/gorilla/schema v1.1.0 h1:CamqUDOFUBqzrvxuz2vEwo8+SUdwsluFh7IlzJh30LY=
github.com/gorilla/schema v1.1.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU=
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/twmb/murmur3 v1.1.3 h1:D83U0XYKcHRYwYIpBKf3Pks91Z0Byda/9SJ8B6EMRcA=
github.com/twmb/murmur3 v1.1.3/go.mod h1:Qq/R7NUyOfr65zD+6Q5IHKsJLwP7exErjN6lyyq3OSQ=
github.com/yuin/goldmark v1.1.30/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200423211502-4bdfaf469ed5/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=

View file

@ -1,37 +1,78 @@
package discord
import (
"context"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat/text"
"github.com/pkg/errors"
)
type Guild struct {
id discord.Snowflake
name string
session *Session
}
var (
_ cchat.Server = (*Guild)(nil)
_ cchat.Icon = (*Guild)(nil)
_ cchat.Server = (*Guild)(nil)
_ cchat.ServerList = (*Guild)(nil)
)
func NewGuild(s *Session, g *discord.Guild) *Guild {
return &Guild{
id: g.ID,
name: g.Name,
session: s,
}
}
func (g *Guild) self(ctx context.Context) (*discord.Guild, error) {
return g.session.WithContext(ctx).Guild(g.id)
}
func (g *Guild) selfState() (*discord.Guild, error) {
return g.session.Store.Guild(g.id)
}
func (g *Guild) ID() string {
return g.id.String()
}
func (g *Guild) Name() text.Rich {
return text.Rich{Content: g.name}
s, err := g.selfState()
if err != nil {
// This shouldn't happen.
return text.Rich{Content: g.id.String()}
}
return text.Rich{Content: s.Name}
}
func (g *Guild) Guilds(container cchat.ServersContainer) error {
func (g *Guild) Icon(ctx context.Context, iconer cchat.IconContainer) error {
s, err := g.self(ctx)
if err != nil {
// This shouldn't happen.
return errors.Wrap(err, "Failed to get guild")
}
if s.Icon != "" {
iconer.SetIcon(s.IconURL() + "?size=64")
}
return nil
}
func (g *Guild) Servers(container cchat.ServersContainer) error {
c, err := g.session.Channels(g.id)
if err != nil {
return errors.Wrap(err, "Failed to get channels")
}
var channels = make([]cchat.Server, len(c))
for i := range c {
channels[i] = NewChannel(g.session, c[i])
}
container.SetServers(channels)
return nil
}

193
message.go Normal file
View file

@ -0,0 +1,193 @@
package discord
import (
"time"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/gateway"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat-discord/segments"
"github.com/diamondburned/cchat/text"
)
type messageHeader struct {
id discord.Snowflake
time discord.Timestamp
channelID discord.Snowflake
guildID discord.Snowflake
nonce string
}
var _ cchat.MessageHeader = (*messageHeader)(nil)
func newHeader(msg discord.Message) messageHeader {
var h = messageHeader{
id: msg.ID,
time: msg.Timestamp,
channelID: msg.ChannelID,
guildID: msg.GuildID,
nonce: msg.Nonce,
}
if msg.EditedTimestamp.Valid() {
h.time = msg.EditedTimestamp
}
return h
}
func NewHeaderDelete(d *gateway.MessageDeleteEvent) messageHeader {
return messageHeader{
id: d.ID,
time: discord.Timestamp(time.Now()),
channelID: d.ChannelID,
guildID: d.GuildID,
}
}
func (m messageHeader) ID() string {
return m.id.String()
}
func (m messageHeader) Time() time.Time {
return m.time.Time()
}
type Author struct {
id discord.Snowflake
name text.Rich
avatar string
}
func NewUser(u discord.User) Author {
return Author{
id: u.ID,
name: text.Rich{Content: u.Username},
avatar: u.AvatarURL() + "?size=128",
}
}
func NewGuildMember(m discord.Member, g discord.Guild) Author {
var name = text.Rich{
Content: m.User.Username,
}
// Update the nickname.
if m.Nick != "" {
name.Content = m.Nick
}
// Update the color.
if c := discord.MemberColor(g, m); c > 0 {
name.Segments = []text.Segment{
segments.NewColored(len(name.Content), c.Uint32()),
}
}
return Author{
id: m.User.ID,
name: name,
avatar: m.User.AvatarURL() + "?size=128",
}
}
func (a Author) ID() string {
return a.id.String()
}
func (a Author) Name() text.Rich {
return a.name
}
func (a Author) Avatar() string {
return a.avatar
}
type Message struct {
messageHeader
author Author
content text.Rich
// TODO
mentioned bool
}
func NewMessageUpdateContent(msg discord.Message) Message {
return Message{
messageHeader: newHeader(msg),
content: text.Rich{Content: msg.Content},
}
}
func NewMessageUpdateAuthor(msg discord.Message, member discord.Member, g discord.Guild) Message {
return Message{
messageHeader: newHeader(msg),
author: NewGuildMember(member, g),
}
}
// NewMessageWithSession uses the session to create a message. It does not do
// API calls. Member is optional.
func NewMessageWithMember(m discord.Message, s *Session, mem *discord.Member) Message {
// This should not error.
g, err := s.Store.Guild(m.GuildID)
if err != nil {
return NewMessage(m, NewUser(m.Author))
}
if mem == nil {
mem, _ = s.Store.Member(m.GuildID, m.Author.ID)
}
if mem == nil {
s.Members.RequestMember(m.GuildID, m.Author.ID)
return NewMessage(m, NewUser(m.Author))
}
return NewMessage(m, NewGuildMember(*mem, *g))
}
// NewBacklogMessage uses the session to create a message fetched from the
// backlog. It takes in an existing guild and tries to fetch a new member, if
// it's nil.
func NewBacklogMessage(m discord.Message, s *Session, g discord.Guild) Message {
// If the message doesn't have a guild, then we don't need all the
// complicated member fetching process.
if !m.GuildID.Valid() {
return NewMessage(m, NewUser(m.Author))
}
mem, err := s.Store.Member(m.GuildID, m.Author.ID)
if err != nil {
s.Members.RequestMember(m.GuildID, m.Author.ID)
return NewMessage(m, NewUser(m.Author))
}
return NewMessage(m, NewGuildMember(*mem, g))
}
func NewDirectMessage(m discord.Message) Message {
return NewMessage(m, NewUser(m.Author))
}
func NewMessage(m discord.Message, author Author) Message {
return Message{
messageHeader: newHeader(m),
author: author,
content: text.Rich{Content: m.Content},
}
}
func (m Message) Author() cchat.MessageAuthor {
return m.author
}
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
}

25
segments/segments.go Normal file
View file

@ -0,0 +1,25 @@
package segments
import "github.com/diamondburned/cchat/text"
type Colored struct {
strlen int
color uint32
}
var (
_ text.Colorer = (*Colored)(nil)
_ text.Segment = (*Colored)(nil)
)
func NewColored(strlen int, color uint32) Colored {
return Colored{strlen, color}
}
func (color Colored) Bounds() (start, end int) {
return 0, color.strlen
}
func (color Colored) Color() uint32 {
return color.color
}

View file

@ -1,24 +1,28 @@
package discord
import (
"context"
"github.com/diamondburned/arikawa/discord"
"github.com/diamondburned/arikawa/state"
"github.com/diamondburned/cchat"
"github.com/diamondburned/cchat/text"
"github.com/diamondburned/ningen"
"github.com/pkg/errors"
)
type Service struct{}
var (
_ cchat.Service = (*Service)(nil)
_ cchat.Icon = (*Service)(nil)
_ cchat.Service = (*Service)(nil)
)
func (Service) Name() text.Rich {
return text.Rich{Content: "Discord"}
}
func (Service) Icon(iconer cchat.IconContainer) error {
func (Service) Icon(ctx context.Context, iconer cchat.IconContainer) error {
iconer.SetIcon("https://discord.com/assets/2c21aeda16de354ba5334551a883b481.png")
return nil
}
@ -51,33 +55,60 @@ func (Authenticator) Authenticate(form []string) (cchat.Session, error) {
return nil, err
}
return NewSession(s)
}
type Session struct {
*ningen.State
userID discord.Snowflake
}
var (
_ cchat.Icon = (*Session)(nil)
_ cchat.Session = (*Session)(nil)
_ cchat.ServerList = (*Session)(nil)
_ cchat.SessionSaver = (*Session)(nil)
)
func NewSession(s *state.State) (*Session, error) {
// Prefetch user.
_, err = s.Me()
u, err := s.Me()
if err != nil {
return nil, errors.Wrap(err, "Failed to get current user")
}
n, err := ningen.FromState(s)
if err != nil {
return nil, errors.Wrap(err, "Failed to create a state wrapper")
}
return &Session{
State: s,
userID: u.ID,
State: n,
}, nil
}
type Session struct {
*state.State
}
func (s *Session) ID() string {
u, _ := s.Store.Me()
return u.ID.String()
return s.userID.String()
}
func (s *Session) Name() text.Rich {
u, _ := s.Store.Me()
u, err := s.Store.Me()
if err != nil {
// This shouldn't happen, ever.
return text.Rich{Content: "<@" + s.userID.String() + ">"}
}
return text.Rich{Content: u.Username + "#" + u.Discriminator}
}
func (s *Session) Icon(iconer cchat.IconContainer) error {
u, _ := s.Store.Me()
func (s *Session) Icon(ctx context.Context, iconer cchat.IconContainer) error {
u, err := s.Store.Me()
if err != nil {
return errors.Wrap(err, "Failed to get the current user")
}
// Thanks to arikawa, AvatarURL is never empty.
iconer.SetIcon(u.AvatarURL())
return nil
}