#Not recieving Channel message

52 messages ยท Page 1 of 1 (latest)

noble flame
#

Hello everyone

I am currently learning go and am creating a server app, in which create a discord oauth2 client and a http mux server. When a new oauth2 flow is completed by a user, a event should be sent into a channel by the http server goroutine and received, by my main goroutine. I now have the problem, that i am not receiving an event after a completion of a oauth2 grant flow.

Event related functions

func NewEventEmitter() *EventEmitter {
    return &EventEmitter{
        events:   make(chan Event),
        shutdown: make(chan struct{}),
    }
}

func (emitter *EventEmitter) Emit(event Event) {
    go func(ch chan Event) {
        ch <- event
    }(emitter.events)
}

func (emitter *EventEmitter) Subscribe() <-chan Event {
    go func() {
        <-emitter.shutdown
        close(emitter.events)
    }()
    return emitter.events
}

func (handler *OAuth2Handler) SubscribeNewUserEvents() <-chan Event {
    return handler.emitter.Subscribe()
}

Emitting

event := Event{
    UserData:         user,
    VerificationCode: verificationCode,
}

handler.emitter.Emit(event)

Recieving

newUserChannel := oauthHandler.SubscribeNewUserEvents()

go func() {
    for event := range newUserChannel {
        fmt.Printf("New user event received: %+v\n", event)
    }
}()

Types

type Event struct {
    UserData         DiscordUser
    VerificationCode string
}

type EventEmitter struct {
    events   chan Event
    shutdown chan struct{}
}

type EventListener interface {
    OnNewUser(event Event)
}

type OAuth2Handler struct {
    config     *oauth2.Config
    emitter    *EventEmitter
    listener   EventListener
    httpServer *http.Server
}

This is obviously not everything regarding code, so if there is any function or method I missed please tell me.

steady jackal
#

All of that looks fine to me

#

I copied your code into a small demo app and it worked

#

Your problem is elsewhere

noble flame
steady jackal
#

For reference

package main

import (
    "fmt"
    "net/http"

    "golang.org/x/oauth2"
)

func main() {
    emitter := NewEventEmitter()
    oauthHandler := &OAuth2Handler{
        emitter: emitter,
    }

    done := make(chan struct{})
    go func() {
        defer close(done)
        for event := range oauthHandler.SubscribeNewUserEvents() {
            fmt.Printf("New user event received: %+v\n", event)
        }
    }()

    emitter.Emit(Event{
        UserData:         DiscordUser{ID: "abc123"},
        VerificationCode: "123456",
    })

    close(emitter.shutdown)
    <-done
}

func NewEventEmitter() *EventEmitter {
    return &EventEmitter{
        events:   make(chan Event),
        shutdown: make(chan struct{}),
    }
}

func (emitter *EventEmitter) Emit(event Event) {
    go func(ch chan Event) {
        ch <- event
    }(emitter.events)
}

func (emitter *EventEmitter) Subscribe() <-chan Event {
    go func() {
        <-emitter.shutdown
        close(emitter.events)
    }()
    return emitter.events
}

func (handler *OAuth2Handler) SubscribeNewUserEvents() <-chan Event {
    return handler.emitter.Subscribe()
}

type DiscordUser struct {
    ID string
}

type Event struct {
    UserData         DiscordUser
    VerificationCode string
}

type EventEmitter struct {
    events   chan Event
    shutdown chan struct{}
}

type EventListener interface {
    OnNewUser(event Event)
}

type OAuth2Handler struct {
    config     *oauth2.Config
    emitter    *EventEmitter
    listener   EventListener
    httpServer *http.Server
}
#

Changed a few things just to make it compile, but it gets the point across

noble flame
steady jackal
#

You might need to share some more code ๐Ÿ™‚

noble flame
#

sure

steady jackal
#

Or, build a small demo of your problem. See if you can reproduce it in an isolated test

noble flame
#

I have expanded on the code since then, it shouldn't change much though.

steady jackal
#

So, the channel send is OK? Are you sure you're actually sending the thing on the channel? Throw a fmt.Println right before your channel send and make sure that's not the issue

#

Also, does your SubscribeNewUserEvents have more than one consumer? This isn't the correct way to do pub/sub with channels, so if you have more than one consumer, only one of the consumers will receive the event

noble flame
#

handlers/http.go

package handlers

import (
    "encoding/json"
    "github.com/go-resty/resty/v2"
    "golang.org/x/oauth2"
    "log"
    "net/http"
    "strconv"
)

func NewEventEmitter() *EventEmitter {
    return &EventEmitter{
        events:   make(map[string][]chan Event),
        shutdown: make(chan struct{}),
    }
}

func (emitter *EventEmitter) Emit(eventType string, event Event) {
    emitter.events[eventType] = append(emitter.events[eventType], make(chan Event))
    for _, ch := range emitter.events[eventType] {
        go func(ch chan Event) {
            ch <- event
        }(ch)
    }
}

func (emitter *EventEmitter) Subscribe(eventType string) <-chan Event {
    ch := make(chan Event)
    emitter.events[eventType] = append(emitter.events[eventType], ch)
    go func() {
        <-emitter.shutdown
        close(ch)
    }()
    return ch
}

func (handler *OAuth2Handler) StartServer(port int) error {
    mux := http.NewServeMux()
    mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        url := handler.config.AuthCodeURL("state", oauth2.AccessTypeOffline)
        http.Redirect(w, r, url, http.StatusTemporaryRedirect)
    })

    mux.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
        code := r.URL.Query().Get("code")

        token, err := handler.config.Exchange(r.Context(), code)
        if err != nil {
            log.Printf("Error exchanging code: %v", err)
            http.Error(w, "Failed to exchange code", http.StatusInternalServerError)
            return
        }

        data, err := getDiscordData("https://discord.com/api/v10/users/@me", token.AccessToken)
        if err != nil {
            log.Printf("Error making API request: %v", err)
            http.Error(w, "Failed to make API request", http.StatusInternalServerError)
            return
        }

        var user DiscordUser
        err = json.Unmarshal(data.Body(), &user)
        if err != nil {
            log.Printf("Error parsing JSON response: %v", err)
            http.Error(w, "Failed to parse JSON response", http.StatusInternalServerError)
            return
        }

        verificationCode := GenerateVerificationCode()

        event := Event{
            UserData:         user,
            VerificationCode: verificationCode,
        }

        handler.emitter.Emit("new_user", event)

        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(user)
    })

    handler.httpServer = &http.Server{
        Addr:    ":" + strconv.Itoa(port),
        Handler: mux,
    }

    return handler.httpServer.ListenAndServe()
}

func (handler *OAuth2Handler) StopServer() error {
    if handler.httpServer != nil {
        return handler.httpServer.Shutdown(nil)
    }
    return nil
}

func (handler *OAuth2Handler) SubscribeNewUserEvents(eventType string) <-chan Event {
    return handler.emitter.Subscribe(eventType)
}

func getDiscordData(url string, authToken string) (resp *resty.Response, err error) {
    client := resty.New()
    client.SetAuthToken(authToken)

    res, err := client.R().Get(url)
    if err != nil || res.StatusCode() > 299 {
        return res, err
    }

    return res, nil
}
#

handlers/mail.go

package handlers

import (
    "bufio"
    "fmt"
    "github.com/joho/godotenv"
    "log"
    "math/rand"
    "net/smtp"
    "os"
    "strconv"
    "strings"
    "time"
)

func GenerateVerificationCode() string {
    rand.NewSource(time.Now().UnixNano())
    return strconv.Itoa(rand.Intn(900000) + 100000)
}

func sendVerificationCode(emails []string, username string, verificationCode string) error {
    if dotenvErr := godotenv.Load(); dotenvErr != nil {
        return dotenvErr
    }

    auth := smtp.PlainAuth(
        "",
        os.Getenv("SMTP_EMAIL"),
        os.Getenv("SMTP_PASSWORD"),
        os.Getenv("SMTP_HOST"),
    )

    body := fmt.Sprintf("...", username, verificationCode)

    message := "Subject: Verification Code\n"
    message += "MIME-version: 1.0;\r\n"
    message += "Content-Type: text/html; charset=\"UTF-8\";\r\n\r\n"
    message += body

    if err := smtp.SendMail(
        fmt.Sprintf("%s:%s", os.Getenv("SMTP_HOST"), os.Getenv("SMTP_PORT")),
        auth,
        os.Getenv("SMTP_EMAIL"),
        emails,
        []byte(message),
    ); err != nil {
        log.Println(fmt.Sprintf("Error when trying to send a Email to: %v", emails))
        log.Fatal(err)
    }
    return nil
}

func getUserVerificationCode(username string) string {
    fmt.Print(fmt.Sprintf("Enter the verification code for: %s", username))
    reader := bufio.NewReader(os.Stdin)
    code, _ := reader.ReadString('\n')
    code = strings.TrimSpace(code)
    return code
}

func HandleNewUserEvent(userEmail string, username string, verificationCode string) {
    // Send the verification code via email
    if err := sendVerificationCode([]string{userEmail}, username, verificationCode); err != nil {
        log.Println("Error sending verification code:", err)
        // Handle the error as needed
        return
    }

    // Prompt the user for the verification code
    userCode := getUserVerificationCode(username)

    // Compare the user's input with the generated verification code
    if userCode != verificationCode {
        log.Println("Invalid verification code")
        // Handle the incorrect code case
        return
    }

    log.Println("Verification code confirmed")
    // Handle the correct code case
}
#

handlers/oauth2

package handlers

import (
    "fmt"
    "golang.org/x/oauth2"
)

const (
    oauth2AuthURL     = "https://discord.com/api/oauth2/authorize"
    oauth2TokenURL    = "https://discord.com/api/oauth2/token"
    oauth2RedirectURL = "http://localhost:8000/callback"
)

func NewOAuth2Handler(listener EventListener, clientID string, clientSecret string) *OAuth2Handler {
    config := &oauth2.Config{
        Endpoint: oauth2.Endpoint{
            AuthURL:  oauth2AuthURL,
            TokenURL: oauth2TokenURL,
        },
        ClientID:     clientID,
        ClientSecret: clientSecret,
        RedirectURL:  oauth2RedirectURL,
        Scopes: []string{
            "identify",
            "email",
        },
    }

    authorizationURL := config.AuthCodeURL("state")
    fmt.Println(authorizationURL)

    return &OAuth2Handler{
        config:   config,
        emitter:  NewEventEmitter(),
        listener: listener,
    }
}
#

handlers/types.go

package handlers

import (
    "golang.org/x/oauth2"
    "net/http"
)

type DiscordUser struct {
    Id               string      `json:"id"`
    Username         string      `json:"username"`
    GlobalName       string      `json:"global_name"`
    Avatar           string      `json:"avatar"`
    Discriminator    string      `json:"discriminator"`
    PublicFlags      int         `json:"public_flags"`
    Flags            int         `json:"flags"`
    Banner           string      `json:"banner"`
    BannerColor      interface{} `json:"banner_color"`
    AccentColor      interface{} `json:"accent_color"`
    Locale           string      `json:"locale"`
    MfaEnabled       bool        `json:"mfa_enabled"`
    PremiumType      int         `json:"premium_type"`
    AvatarDecoration interface{} `json:"avatar_decoration"`
    Email            string      `json:"email"`
    Verified         bool        `json:"verified"`
}

type OAuth2Handler struct {
    config     *oauth2.Config
    emitter    *EventEmitter
    listener   EventListener
    httpServer *http.Server
}

type Event struct {
    UserData         DiscordUser
    VerificationCode string
}

type EventEmitter struct {
    events   map[string][]chan Event
    shutdown chan struct{}
}

type EventListener interface {
    OnNewUser(event Event)
}
#

main.go

package main

import (
    "fmt"
    "github.com/joho/godotenv"
    "log"
    "os"
    "verify/handlers"
)

type NewUserListener struct{}

func (listener NewUserListener) OnNewUser(event handlers.Event) {
    fmt.Printf("New user event received: %+v\n", event)
}

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Unable to load the .env")
    }

    listener := NewUserListener{}
    oauthHandler := handlers.NewOAuth2Handler(&listener, os.Getenv("DISCORD_CLIENT_ID"), os.Getenv("DISCORD_CLIENT_SECRET"))

    err = oauthHandler.StartServer(8000)
    if err != nil {
        log.Fatal("Unable to start HTTPS server:", err)
    }

    newUserChannel := oauthHandler.SubscribeNewUserEvents("new_user")

    go func() {
        for event := range newUserChannel {
            fmt.Printf("New user event received: %+v\n", event)
            // handlers.HandleNewUserEvent(event.UserData.Email, event.UserData.Username, event.VerificationCode)
        }
    }()

    err = oauthHandler.StopServer()
    if err != nil {
        log.Fatal("Unable to stop HTTPS server:", err)
    }
}
#

@steady jackal This should be all the Code.

#

You don't need to try it out for yourself, since you would need to setup a discord and gmail application but i hope this gives a better insight into how my program works.

steady jackal
#

Brill! I'll take a look ๐Ÿ™‚

noble flame
#

If you have time, we could also take a look at it in a voice channel

steady jackal
#

Maybe yeah, I'll see if I can spot the issue first

noble flame
#

Sure and thanks a lot for taking your time to help me. thanks

steady jackal
#

OK, you're starting your http server before you've started listening on the channel. httpServer.ListenAndServe() is a blocking call

#

None of this code will ever be executed during the lifetime of your application.

#

Except the error check after oauthHandler.StartServer

noble flame
#

would this solve the issue?

go func() {
        err = oauthHandler.StartServer(8000)
        if err != nil {
            log.Fatal("Unable to start HTTPS server:", err)
        }
    }()

steady jackal
#

It would not, because then your server would be immediately stopped

#

You'll start the server, spawn a goroutine to listen for events, and then shutdown the server ๐Ÿ˜„

#

What you could do is use the os/signal package to wait for an interrupt, and then shut the server down

noble flame
#

Thanks for the help

steady jackal
#

It should be relatively easy to solve. I can help you if you like?

noble flame
steady jackal
#

Understood! Good luck ๐Ÿ™‚

noble flame
steady jackal
#

Another solution (if signals scare you) would be to simply not use the http.Server type directly. You can call http.ListenAndServe instead

#

That would be the last thing you call in your main function, and it would both start the server and keep the application from exiting

noble flame
#

il keep that in mind thanks

noble flame
# steady jackal What you could do is use the `os/signal` package to wait for an interrupt, and t...

Used signals and it worked now Pogher

package main

import (
    "fmt"
    "github.com/joho/godotenv"
    "log"
    "os"
    "os/signal"
    "syscall"
    "verify/handlers"
)

type NewUserListener struct{}

func (listener NewUserListener) OnNewUser(event handlers.Event) {
    fmt.Printf("New user event received: %+v\n", event)
}

func main() {
    err := godotenv.Load()
    if err != nil {
        log.Fatal("Unable to load the .env")
    }

    listener := NewUserListener{}
    oauthHandler := handlers.NewOAuth2Handler(&listener, os.Getenv("DISCORD_CLIENT_ID"), os.Getenv("DISCORD_CLIENT_SECRET"))

    newUserChannel := oauthHandler.SubscribeNewUserEvents("new_user")

    go func() {
        for event := range newUserChannel {
            fmt.Printf("New user event received: %+v\n", event)
            // handlers.HandleNewUserEvent(event.UserData.Email, event.UserData.Username, event.VerificationCode)
        }
    }()

    go func() {
        err = oauthHandler.StartServer(8000)
        if err != nil {
            log.Fatal("Unable to start HTTPS server:", err)
        }
    }()

    interrupt := make(chan os.Signal, 1)
    signal.Notify(interrupt, os.Interrupt, syscall.SIGTERM)

    <-interrupt

    err = oauthHandler.StopServer()
    if err != nil {
        log.Fatal("Unable to stop HTTPS server:", err)
    }
}
steady jackal
noble flame
#

Yes

steady jackal
#

I would expect you to get Unable to start HTTPS server: Server closed or something

noble flame
#

You were spot on

steady jackal
#

Yeah, you can handle that specific error intentionally.

#

It's not really an error, because you intentionally shut the server down

#
if err != nil && err != http.ErrServerClosed {
    log.Fatal("Unable to start HTTPS server:", err)
}