Skip to content

Commit

Permalink
Merge branch 'develop' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
clockworksoul committed Oct 18, 2021
2 parents d18ad6c + 4d67380 commit 770aee5
Show file tree
Hide file tree
Showing 33 changed files with 1,169 additions and 118 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/quickstart.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,4 @@ go build -o gort
./gort bootstrap --allow-insecure localhost:4000

# Using Gort.
# TODO: Worth having a hidden command to emulate this?
# TODO: Worth having a hidden command to emulate this?
2 changes: 1 addition & 1 deletion .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ jobs:
- name: Install
run: npm install -g wait-port
- name: Test
run: ${GITHUB_WORKSPACE}/.github/workflows/quickstart.sh
run: ${GITHUB_WORKSPACE}/.github/workflows/quickstart.sh
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ More information about writing commands can be found in the Gort Guide:

In Gort, a set of one or more related commands can be installed as a "command bundle".

A bundle is [represented in YAML](https://guide.getgort.io/bundle-configurations.html), specifying which executable to use for each command and who is allowed to execute each commands.
A bundle is [represented in YAML](https://guide.getgort.io/bundle-configurations.html), specifying which executable to use for each command and who is allowed to execute each commands.

A very simple bundle file is shown below.

Expand Down Expand Up @@ -246,4 +246,3 @@ Gort is in a state of active heavy development. The date that various [milestone

* [Gort Slack Community](https://join.slack.com/t/getgort/shared_invite/zt-scgi5f7r-1U9awWMWNITl1MCzrpV3~Q)
* [GitHub Issues](https://github.com/getgort/gort/issues)

2 changes: 2 additions & 0 deletions Tiltfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
docker_compose('docker-compose.yml')
docker_build('getgort/gort', '.')
9 changes: 5 additions & 4 deletions adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,7 @@ func TriggerCommand(ctx context.Context, rawCommand string, id RequestorIdentity
if id.GortUser == nil {
var autocreated bool

if id.GortUser, autocreated, err = findOrMakeGortUser(ctx, id.ChatUser); err != nil {
if id.GortUser, autocreated, err = findOrMakeGortUser(ctx, id.Adapter, id.ChatUser); err != nil {
switch {
case gerrs.Is(err, ErrSelfRegistrationOff):
msg := "I'm terribly sorry, but either I don't " +
Expand Down Expand Up @@ -745,7 +745,7 @@ func buildRequestorIdentity(ctx context.Context, adapter Adapter, channelId, use
return id, err
}

user, err := dal.UserGetByEmail(ctx, id.ChatUser.Email)
user, err := dal.UserGetByID(ctx, id.Adapter.GetName(), id.ChatUser.ID)
switch {
case err == nil:
id.GortUser = &user
Expand Down Expand Up @@ -797,7 +797,7 @@ func findAllEntries(ctx context.Context, bundleName, commandName string, finder
}

// findOrMakeGortUser ...
func findOrMakeGortUser(ctx context.Context, info *UserInfo) (*rest.User, bool, error) {
func findOrMakeGortUser(ctx context.Context, adapter Adapter, info *UserInfo) (*rest.User, bool, error) {
// Get the data access interface.
da, err := dataaccess.Get()
if err != nil {
Expand All @@ -806,7 +806,7 @@ func findOrMakeGortUser(ctx context.Context, info *UserInfo) (*rest.User, bool,

// Try to figure out what user we're working with here.
exists := true
user, err := da.UserGetByEmail(ctx, info.Email)
user, err := da.UserGetByID(ctx, adapter.GetName(), info.ID)
if gerrs.Is(err, errs.ErrNoSuchUser) {
exists = false
} else if err != nil {
Expand Down Expand Up @@ -845,6 +845,7 @@ func findOrMakeGortUser(ctx context.Context, info *UserInfo) (*rest.User, bool,
FullName: info.RealNameNormalized,
Password: randomPassword,
Username: info.Name,
Mappings: map[string]string{adapter.GetName(): info.ID},
}

log.WithField("user.username", user.Username).
Expand Down
1 change: 1 addition & 0 deletions adapter/discord/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
An Adapter implementation for Discord.
238 changes: 238 additions & 0 deletions adapter/discord/adapter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
/*
* Copyright 2021 The Gort Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package discord

import (
"context"
"fmt"
"strings"

"github.com/bwmarrin/discordgo"
"github.com/getgort/gort/adapter"
"github.com/getgort/gort/data"
)

// NewAdapter will construct a DiscordAdapter instance for a given provider configuration.
func NewAdapter(provider data.DiscordProvider) (adapter.Adapter, error) {
// Create a new Discord session using the provided bot token.
dg, err := discordgo.New("Bot " + provider.BotToken)
if err != nil {
return nil, err
}

return &Adapter{
provider: provider,
session: dg,
}, nil
}

var _ adapter.Adapter = &Adapter{}

// Adapter is the Discord provider implementation of a relay, which knows how
// to receive events from the Discord API, translate them into Gort events, and
// forward them along.
type Adapter struct {
session *discordgo.Session
provider data.DiscordProvider
events chan *adapter.ProviderEvent
}

// GetChannelInfo provides info on a specific provider channel accessible
// to the adapter.
func (s *Adapter) GetChannelInfo(channelID string) (*adapter.ChannelInfo, error) {
channel, err := s.session.Channel(channelID)
if err != nil {
return nil, err
}
return newChannelInfoFromDiscordChannel(channel), nil
}

func newChannelInfoFromDiscordChannel(channel *discordgo.Channel) *adapter.ChannelInfo {
out := &adapter.ChannelInfo{
ID: channel.ID,
Name: channel.Name,
}
for _, r := range channel.Recipients {
out.Members = append(out.Members, r.Username)
}
return out
}

// GetName provides the name of this adapter as per the configuration.
func (s *Adapter) GetName() string {
return s.provider.Name
}

// GetPresentChannels returns a slice of channels that a user is present in.
func (s *Adapter) GetPresentChannels() ([]*adapter.ChannelInfo, error) {
allChannels, err := s.session.UserChannels()
if err != nil {
return nil, err
}

channels := make([]*adapter.ChannelInfo, 0)
for _, ch := range allChannels {
channels = append(channels, newChannelInfoFromDiscordChannel(ch))
}

return channels, nil
}

// GetUserInfo provides info on a specific provider user accessible
// to the adapter.
func (s *Adapter) GetUserInfo(userID string) (*adapter.UserInfo, error) {
u, err := s.session.User(userID)
if err != nil {
return nil, err
}

return newUserInfoFromDiscordUser(u), nil
}

func newUserInfoFromDiscordUser(user *discordgo.User) *adapter.UserInfo {
u := &adapter.UserInfo{}

u.ID = user.ID
u.Name = user.Username
u.DisplayName = user.Avatar
u.DisplayNameNormalized = user.Avatar
u.Email = user.Email
return u
}

// Listen causes the Adapter to initiate a connection to its provider and
// begin relaying back events (including errors) via the returned channel.
func (s *Adapter) Listen(ctx context.Context) <-chan *adapter.ProviderEvent {
s.events = make(chan *adapter.ProviderEvent, 100)

// Register the messageCreate func as a callback for MessageCreate events.
s.session.AddHandler(s.messageCreate)
s.session.AddHandler(s.onConnected)
s.session.AddHandler(s.onDisconnected)

go func() {
// Open a websocket connection to Discord and begin listening.
err := s.session.Open()
if err != nil {
if strings.Contains(err.Error(), "Authentication failed.") {
s.events <- s.onInvalidAuth()
} else {
s.events <- s.onConnectionError(err.Error())
}
}
}()

return s.events
}

// This function will be called (due to AddHandler above) every time a new
// message is created on any channel that the authenticated bot has access to.
func (s *Adapter) messageCreate(sess *discordgo.Session, m *discordgo.MessageCreate) {
// Ignore all messages created by the bot itself
if m.Author.ID == sess.State.User.ID {
return
}
channel, err := sess.Channel(m.ChannelID)
if err != nil {
panic(err)
}
if len(channel.Recipients) > 0 {
s.events <- s.wrapEvent(
adapter.EventChannelMessage,
&adapter.DirectMessageEvent{
ChannelID: m.ChannelID,
Text: m.Content,
UserID: m.Author.ID,
},
)
} else {
s.events <- s.wrapEvent(
adapter.EventChannelMessage,
&adapter.ChannelMessageEvent{
ChannelID: m.ChannelID,
Text: m.Content,
UserID: m.Author.ID,
},
)
}
}

// onConnected is called when the Slack API emits a ConnectedEvent.
func (s *Adapter) onConnected(sess *discordgo.Session, m *discordgo.Connect) *adapter.ProviderEvent {
return s.wrapEvent(
adapter.EventConnected,
&adapter.ConnectedEvent{},
)
}

// onConnectionError is called when the Slack API emits an ConnectionErrorEvent.
func (s *Adapter) onConnectionError(message string) *adapter.ProviderEvent {
return s.wrapEvent(
adapter.EventConnectionError,
&adapter.ErrorEvent{Msg: message},
)
}

// onDisconnected is called when the Discord API emits a DisconnectedEvent.
func (s *Adapter) onDisconnected(sess *discordgo.Session, m *discordgo.Disconnect) {
s.events <- s.wrapEvent(
adapter.EventDisconnected,
&adapter.DisconnectedEvent{},
)
}

// onInvalidAuth is called when the Slack API emits an InvalidAuthEvent.
func (s *Adapter) onInvalidAuth() *adapter.ProviderEvent {
return s.wrapEvent(
adapter.EventAuthenticationError,
&adapter.AuthenticationErrorEvent{
Msg: fmt.Sprintf("Connection failed to %s: invalid credentials", s.provider.Name),
},
)
}

// wrapEvent creates a new ProviderEvent instance with metadata and the Event data attached.
func (s *Adapter) wrapEvent(eventType adapter.EventType, data interface{}) *adapter.ProviderEvent {
return &adapter.ProviderEvent{
EventType: eventType,
Data: data,
Info: &adapter.Info{
Provider: adapter.NewProviderInfoFromConfig(s.provider),
},
Adapter: s,
}
}

// SendErrorMessage sends an error message to a specified channel.
// TODO Create a MessageBuilder at some point to replace this.
func (s *Adapter) SendErrorMessage(channelID string, title string, text string) error {
_, err := s.session.ChannelMessageSend(channelID, fmt.Sprintf("%v\n%v", title, text))
if err != nil {
return err
}
return nil
}

// SendMessage sends a standard output message to a specified channel.
// TODO Create a MessageBuilder at some point to replace this.
func (s *Adapter) SendMessage(channelID string, message string) error {
_, err := s.session.ChannelMessageSend(channelID, message)
if err != nil {
return err
}
return nil
}
3 changes: 3 additions & 0 deletions adapter/providerinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ func NewProviderInfoFromConfig(provider data.Provider) *ProviderInfo {
p := &ProviderInfo{}

switch ap := provider.(type) {
case data.DiscordProvider:
p.Type = "discord"
p.Name = ap.Name
case data.SlackProvider:
p.Type = "slack"
p.Name = ap.Name
Expand Down
Loading

0 comments on commit 770aee5

Please sign in to comment.