diff --git a/aimerror/errors.go b/aimerror/errors.go index 9a7c0f9..89dc4a5 100644 --- a/aimerror/errors.go +++ b/aimerror/errors.go @@ -2,12 +2,12 @@ package aimerror import "github.com/pkg/errors" -func FetchingUser(err error, username string) error { - return errors.Wrapf(err, "could not fetch user with username %s", username) +func FetchingUser(err error, screen_name string) error { + return errors.Wrapf(err, "could not fetch user with screen_name %s", screen_name) } -func UserNotFound(username string) error { - return errors.Errorf("no user with UIN %s", username) +func UserNotFound(screen_name string) error { + return errors.Errorf("no user with UIN %s", screen_name) } var NoUserInSession = errors.New("no user in session") diff --git a/main.go b/main.go index 6e66cb9..b8d5125 100644 --- a/main.go +++ b/main.go @@ -114,6 +114,8 @@ func main() { pgdriver.WithWriteTimeout(5*time.Second), ) + log.Printf("DB URL: %s", DB_URL) + // Set up the DB sqldb := sql.OpenDB(pgconn) db := bun.NewDB(sqldb, pgdialect.New()) @@ -124,7 +126,7 @@ func main() { db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true))) // Register our DB models - db.RegisterModel((*models.User)(nil), (*models.Message)(nil), (*models.Buddy)(nil)) + db.RegisterModel((*models.User)(nil), (*models.Message)(nil), (*models.Buddy)(nil), (*models.EmailVerification)(nil)) // dev: load in fixtures to test against fixture := dbfixture.New(db, dbfixture.WithRecreateTables()) @@ -169,7 +171,7 @@ func main() { } onlineCh <- user - sessionManager.RemoveSession(user.Username) + sessionManager.RemoveSession(user.ScreenName) } } @@ -180,10 +182,10 @@ func main() { } if user := models.UserFromContext(ctx); user != nil { - fmt.Printf("%s (%v) ->\n%+v\n", user.Username, session.RemoteAddr(), flap) + fmt.Printf("%s (%v) ->\n%+v\n", user.ScreenName, session.RemoteAddr(), flap) user.LastActivityAt = time.Now() ctx = models.NewContextWithUser(ctx, user) - sessionManager.SetSession(user.Username, session) + sessionManager.SetSession(user.ScreenName, session) } else { fmt.Printf("%v ->\n%+v\n", session.RemoteAddr(), flap) } diff --git a/models/EmailVerification.go b/models/EmailVerification.go new file mode 100644 index 0000000..5a17ae5 --- /dev/null +++ b/models/EmailVerification.go @@ -0,0 +1,18 @@ +package models + +import ( + "time" + + "github.com/uptrace/bun" +) + +// TODO: move this out of here and into API server +type EmailVerification struct { + bun.BaseModel `bun:"table:email_verification"` + UserUIN int64 `bun:",pk,notnull,unique"` + User *User `bun:"rel:has-one,join:user_uin=uin"` + Token string `bun:",notnull"` + Used bool `bun:",notnull,default:false"` + CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` +} diff --git a/models/User.go b/models/User.go index 73f6e85..ae43c45 100644 --- a/models/User.go +++ b/models/User.go @@ -25,12 +25,14 @@ type User struct { bun.BaseModel `bun:"table:users"` UIN int64 `bun:",pk,autoincrement"` Email string `bun:",unique"` - Username string `bun:",unique"` + ScreenName string `bun:",unique"` Password string Cipher string - CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` - UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"` + DeletedAt *time.Time `bun:",nullzero"` Status UserStatus + Verified bool `bun:",notnull,default:false"` Profile string ProfileEncoding string AwayMessage string @@ -48,9 +50,9 @@ var ( currentUser = userKey("user") ) -func UserByUsername(ctx context.Context, db *bun.DB, username string) (*User, error) { +func UserByScreenName(ctx context.Context, db *bun.DB, screen_name string) (*User, error) { user := new(User) - if err := db.NewSelect().Model(user).Where("username = ?", username).Scan(ctx, user); err != nil { + if err := db.NewSelect().Model(user).Where("screen_name = ?", screen_name).Scan(ctx, user); err != nil { if err == sql.ErrNoRows { return nil, nil } diff --git a/models/fixtures.yml b/models/fixtures.yml index f097908..f4f5192 100644 --- a/models/fixtures.yml +++ b/models/fixtures.yml @@ -1,14 +1,20 @@ - model: User rows: - - uin: 1 - username: toof + - uin: 10000 + screen_name: toof password: bar email: toof@example.com - - uin: 2 - username: artem + verified: true + - uin: 10001 + screen_name: artem password: bar email: artem@example.com + verified: false - model: Message rows: [] - model: Buddy rows: [] +- model: EmailVerification + rows: + - user_uin: 2 + token: foobar diff --git a/online_routine.go b/online_routine.go index 45a7d5e..db90728 100644 --- a/online_routine.go +++ b/online_routine.go @@ -25,9 +25,9 @@ func OnlineNotification(sm *SessionManager) (chan *models.User, routineFn) { } if user.Status == models.UserStatusOnline { - log.Printf("%s is now online", user.Username) + log.Printf("%s is now online", user.ScreenName) } else if user.Status == models.UserStatusAway { - log.Printf("%s is now away", user.Username) + log.Printf("%s is now away", user.ScreenName) } ctx := context.Background() @@ -44,11 +44,11 @@ func OnlineNotification(sm *SessionManager) (chan *models.User, routineFn) { if buddy.Source.Status == models.UserStatusAway || buddy.Source.Status == models.UserStatusDnd { continue } - log.Printf("telling %s that %s has a new status: %d!", buddy.Source.Username, user.Username, user.Status) + log.Printf("telling %s that %s has a new status: %d!", buddy.Source.ScreenName, user.ScreenName, user.Status) - if s := sm.GetSession(buddy.Source.Username); s != nil { + if s := sm.GetSession(buddy.Source.ScreenName); s != nil { onlineSnac := oscar.NewSNAC(3, 0xb) - onlineSnac.Data.WriteLPString(user.Username) + onlineSnac.Data.WriteLPString(user.ScreenName) onlineSnac.Data.WriteUint16(0) // TODO: user warning level tlvs := []*oscar.TLV{ @@ -68,7 +68,7 @@ func OnlineNotification(sm *SessionManager) (chan *models.User, routineFn) { onlineFlap := oscar.NewFLAP(2) onlineFlap.Data.WriteBinary(onlineSnac) if err := s.Send(onlineFlap); err != nil { - log.Printf("could not tell %s that %s is online", buddy.Source.Username, user.Username) + log.Printf("could not tell %s that %s is online", buddy.Source.ScreenName, user.ScreenName) } } } diff --git a/package.json b/package.json index 9a110c4..c558bd1 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "license": "GPLv4", "scripts": { - "dev": "nodemon --watch ./ -e go --ignore '*_test.go' --delay 200ms --exec 'go build && ./aim-oscar || exit 1' --signal SIGTERM", + "dev": "nodemon --watch './' -e go,yml --ignore '*_test.go' --delay 200ms --exec 'go build && ./aim-oscar || exit 1' --signal SIGTERM", "start": "go build && ./aim-oscar" }, "devDependencies": { diff --git a/services/0x01_generic_service_controls.go b/services/0x01_generic_service_controls.go index 629e8ef..7fab25f 100644 --- a/services/0x01_generic_service_controls.go +++ b/services/0x01_generic_service_controls.go @@ -57,7 +57,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna } onlineSnac := oscar.NewSNAC(3, 0xb) - onlineSnac.Data.WriteLPString(buddy.Source.Username) + onlineSnac.Data.WriteLPString(buddy.Source.ScreenName) onlineSnac.Data.WriteUint16(0) // TODO: user warning level tlvs := []*oscar.TLV{ @@ -77,7 +77,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna onlineFlap := oscar.NewFLAP(2) onlineFlap.Data.WriteBinary(onlineSnac) if err := session.Send(onlineFlap); err != nil { - return ctx, errors.Wrapf(err, "could not tell %s that %s is online", buddy.Source.Username, buddy.Target.Username) + return ctx, errors.Wrapf(err, "could not tell %s that %s is online", buddy.Source.ScreenName, buddy.Target.ScreenName) } } @@ -132,8 +132,8 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna } onlineSnac := oscar.NewSNAC(1, 0xf) - onlineSnac.Data.WriteUint8(uint8(len(user.Username))) - onlineSnac.Data.WriteString(user.Username) + onlineSnac.Data.WriteUint8(uint8(len(user.ScreenName))) + onlineSnac.Data.WriteString(user.ScreenName) onlineSnac.Data.WriteUint16(0) // warning level user.Status = models.UserStatusOnline diff --git a/services/0x02_location_services.go b/services/0x02_location_services.go index c7db4fc..2772b0e 100644 --- a/services/0x02_location_services.go +++ b/services/0x02_location_services.go @@ -86,18 +86,18 @@ func (s *LocationServices) HandleSNAC(ctx context.Context, db *bun.DB, snac *osc return ctx, errors.Wrap(err, "missing request type") } - requestedUsername, err := snac.Data.ReadLPString() + requestedScreenName, err := snac.Data.ReadLPString() if err != nil { - return ctx, errors.Wrap(err, "missing requested username") + return ctx, errors.Wrap(err, "missing requested screen_name") } - requestedUser, err := models.UserByUsername(ctx, db, requestedUsername) + requestedUser, err := models.UserByScreenName(ctx, db, requestedScreenName) if err != nil { - return ctx, aimerror.FetchingUser(err, requestedUsername) + return ctx, aimerror.FetchingUser(err, requestedScreenName) } respSnac := oscar.NewSNAC(2, 6) - respSnac.Data.WriteLPString(requestedUser.Username) + respSnac.Data.WriteLPString(requestedUser.ScreenName) respSnac.Data.WriteUint16(0) // TODO: warning level tlvs := []*oscar.TLV{ diff --git a/services/0x03_buddy_list_management.go b/services/0x03_buddy_list_management.go index 6d7326b..ce16eca 100644 --- a/services/0x03_buddy_list_management.go +++ b/services/0x03_buddy_list_management.go @@ -43,7 +43,7 @@ func (b *BuddyListManagement) HandleSNAC(ctx context.Context, db *bun.DB, snac * return ctx, errors.Wrap(err, "expecting more buddies in list") } - buddy, err := models.UserByUsername(ctx, db, buddyScreename) + buddy, err := models.UserByScreenName(ctx, db, buddyScreename) if err != nil { return ctx, errors.Wrap(err, "error looking for User") } @@ -61,7 +61,7 @@ func (b *BuddyListManagement) HandleSNAC(ctx context.Context, db *bun.DB, snac * return ctx, err } - log.Printf("%s added buddy %s to buddy list", user.Username, buddyScreename) + log.Printf("%s added buddy %s to buddy list", user.ScreenName, buddyScreename) } return ctx, nil @@ -79,7 +79,7 @@ func (b *BuddyListManagement) HandleSNAC(ctx context.Context, db *bun.DB, snac * return ctx, errors.Wrap(err, "expecting more buddies in list") } - buddy, err := models.UserByUsername(ctx, db, buddyScreename) + buddy, err := models.UserByScreenName(ctx, db, buddyScreename) if err != nil { return ctx, errors.Wrap(err, "error looking for User") } @@ -92,7 +92,7 @@ func (b *BuddyListManagement) HandleSNAC(ctx context.Context, db *bun.DB, snac * return ctx, err } - log.Printf("%s removed buddy %s from buddy list", user.Username, buddyScreename) + log.Printf("%s removed buddy %s from buddy list", user.ScreenName, buddyScreename) } return ctx, nil diff --git a/services/0x04_ICBM.go b/services/0x04_ICBM.go index c8cd3ae..152f071 100644 --- a/services/0x04_ICBM.go +++ b/services/0x04_ICBM.go @@ -181,14 +181,14 @@ func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) // TLV 0x6 is the client telling the server to store the message if the recipient is offline saveofflineTLV := oscar.FindTLV(tlvs, 6) if saveofflineTLV != nil { - message, err = models.InsertMessage(ctx, db, msgID, user.Username, to, string(messageContents)) + message, err = models.InsertMessage(ctx, db, msgID, user.ScreenName, to, string(messageContents)) if err != nil { return ctx, errors.Wrap(err, "could not insert message") } } else { message = &models.Message{ Cookie: msgID, - From: user.Username, + From: user.ScreenName, To: to, Contents: string(messageContents), } @@ -204,7 +204,7 @@ func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) ackSnac := oscar.NewSNAC(4, 0xc) ackSnac.Data.WriteUint64(msgID) ackSnac.Data.WriteUint16(2) - ackSnac.Data.WriteLPString(user.Username) + ackSnac.Data.WriteLPString(user.ScreenName) ackFlap := oscar.NewFLAP(2) ackFlap.Data.WriteBinary(ackSnac) return ctx, session.Send(ackFlap) diff --git a/services/0x17_authorization_registration_service.go b/services/0x17_authorization_registration_service.go index b6d1d80..da8e10d 100644 --- a/services/0x17_authorization_registration_service.go +++ b/services/0x17_authorization_registration_service.go @@ -88,19 +88,19 @@ func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *b tlvs, err := oscar.UnmarshalTLVs(snac.Data.Bytes()) util.PanicIfError(err) - usernameTLV := oscar.FindTLV(tlvs, 1) - if usernameTLV == nil { - return ctx, errors.New("missing username TLV") + screenNameTLV := oscar.FindTLV(tlvs, 1) + if screenNameTLV == nil { + return ctx, errors.New("missing screen_name TLV") } // Fetch the user - user, err := models.UserByUsername(ctx, db, string(usernameTLV.Data)) + user, err := models.UserByScreenName(ctx, db, string(screenNameTLV.Data)) if err != nil { return ctx, err } if user == nil { snac := oscar.NewSNAC(0x17, 0x03) - snac.Data.WriteBinary(usernameTLV) + snac.Data.WriteBinary(screenNameTLV) snac.Data.WriteBinary(oscar.NewTLV(0x08, []byte{0, 4})) resp := oscar.NewFLAP(2) resp.Data.WriteBinary(snac) @@ -126,21 +126,21 @@ func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *b tlvs, err := oscar.UnmarshalTLVs(snac.Data.Bytes()) util.PanicIfError(err) - usernameTLV := oscar.FindTLV(tlvs, 1) - if usernameTLV == nil { - return ctx, errors.New("missing username TLV 0x1") + screenNameTLV := oscar.FindTLV(tlvs, 1) + if screenNameTLV == nil { + return ctx, errors.New("missing screen_name TLV 0x1") } - username := string(usernameTLV.Data) + screen_name := string(screenNameTLV.Data) ctx := context.Background() - user, err := models.UserByUsername(ctx, db, username) + user, err := models.UserByScreenName(ctx, db, screen_name) if err != nil { return ctx, err } if user == nil { snac := oscar.NewSNAC(0x17, 0x03) - snac.Data.WriteBinary(usernameTLV) + snac.Data.WriteBinary(screenNameTLV) snac.Data.WriteBinary(oscar.NewTLV(0x08, []byte{0, 4})) resp := oscar.NewFLAP(2) resp.Data.WriteBinary(snac) @@ -162,8 +162,24 @@ func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *b if !bytes.Equal(expectedPasswordHash, passwordHashTLV.Data) { // Tell the client this was a bad password badPasswordSnac := oscar.NewSNAC(0x17, 0x03) - badPasswordSnac.Data.WriteBinary(usernameTLV) - badPasswordSnac.Data.WriteBinary(oscar.NewTLV(0x08, []byte{0, 4})) + badPasswordSnac.Data.WriteBinary(screenNameTLV) + badPasswordSnac.Data.WriteBinary(oscar.NewTLV(0x08, []byte{0, 4})) // incorrect nick/pass + badPasswordFlap := oscar.NewFLAP(2) + badPasswordFlap.Data.WriteBinary(badPasswordSnac) + session.Send(badPasswordFlap) + + // Tell them to leave + discoFlap := oscar.NewFLAP(4) + return ctx, session.Send(discoFlap) + } + + // Only users that have verified their email can use the service + if !user.Verified || user.DeletedAt != nil { + // Tell the client this was a bad password + badPasswordSnac := oscar.NewSNAC(0x17, 0x03) + badPasswordSnac.Data.WriteBinary(screenNameTLV) + badPasswordSnac.Data.WriteBinary(oscar.NewTLV(0x08, []byte{0, 7})) // invalid account + badPasswordSnac.Data.WriteBinary(oscar.NewTLV(0x04, []byte("http://runningman.network/errors/unverified-account"))) badPasswordFlap := oscar.NewFLAP(2) badPasswordFlap.Data.WriteBinary(badPasswordSnac) session.Send(badPasswordFlap) @@ -175,7 +191,7 @@ func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *b // Send BOS response + cookie authSnac := oscar.NewSNAC(0x17, 0x3) - authSnac.Data.WriteBinary(usernameTLV) + authSnac.Data.WriteBinary(screenNameTLV) authSnac.Data.WriteBinary(oscar.NewTLV(0x5, []byte(a.BOSAddress))) cookie, err := json.Marshal(AuthorizationCookie{ diff --git a/session_manager.go b/session_manager.go index 7f72015..455f0dc 100644 --- a/session_manager.go +++ b/session_manager.go @@ -18,15 +18,15 @@ func NewSessionManager() *SessionManager { return sm } -func (sm *SessionManager) SetSession(username string, session *oscar.Session) { +func (sm *SessionManager) SetSession(screen_name string, session *oscar.Session) { sm.mutex.Lock() - sm.sessions[username] = session + sm.sessions[screen_name] = session sm.mutex.Unlock() } -func (sm *SessionManager) GetSession(username string) *oscar.Session { +func (sm *SessionManager) GetSession(screen_name string) *oscar.Session { sm.mutex.RLock() - s, ok := sm.sessions[username] + s, ok := sm.sessions[screen_name] sm.mutex.RUnlock() if ok { @@ -35,8 +35,8 @@ func (sm *SessionManager) GetSession(username string) *oscar.Session { return nil } -func (sm *SessionManager) RemoveSession(username string) { +func (sm *SessionManager) RemoveSession(screen_name string) { sm.mutex.Lock() - sm.sessions[username] = nil + sm.sessions[screen_name] = nil sm.mutex.Unlock() }