mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-22 04:29:47 -05:00
buddy list management service
This commit is contained in:
parent
68b315345b
commit
00c2f20caf
13 changed files with 357 additions and 36 deletions
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"aim-oscar/aimerror"
|
||||||
"aim-oscar/models"
|
"aim-oscar/models"
|
||||||
"aim-oscar/oscar"
|
"aim-oscar/oscar"
|
||||||
"aim-oscar/util"
|
"aim-oscar/util"
|
||||||
|
@ -17,15 +18,73 @@ var versions map[uint16]uint16
|
||||||
func init() {
|
func init() {
|
||||||
versions = make(map[uint16]uint16)
|
versions = make(map[uint16]uint16)
|
||||||
versions[1] = 3
|
versions[1] = 3
|
||||||
|
versions[3] = 1
|
||||||
versions[4] = 1
|
versions[4] = 1
|
||||||
|
versions[17] = 1
|
||||||
}
|
}
|
||||||
|
|
||||||
type GenericServiceControls struct{}
|
type GenericServiceControls struct {
|
||||||
|
OnlineCh chan *models.User
|
||||||
|
}
|
||||||
|
|
||||||
func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC, comm chan *models.Message) (context.Context, error) {
|
func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) {
|
||||||
session, _ := oscar.SessionFromContext(ctx)
|
session, _ := oscar.SessionFromContext(ctx)
|
||||||
|
|
||||||
switch snac.Header.Subtype {
|
switch snac.Header.Subtype {
|
||||||
|
|
||||||
|
// Client is ONLINE and READY
|
||||||
|
case 0x02:
|
||||||
|
user := models.UserFromContext(ctx)
|
||||||
|
if user != nil {
|
||||||
|
user.Status = models.UserStatusActive
|
||||||
|
if err := user.Update(ctx, db); err != nil {
|
||||||
|
return ctx, errors.Wrap(err, "could not set user as active")
|
||||||
|
}
|
||||||
|
|
||||||
|
g.OnlineCh <- user
|
||||||
|
|
||||||
|
// Find all of the buddies that are online and tell the user
|
||||||
|
var buddies []*models.Buddy
|
||||||
|
err := db.NewSelect().Model(&buddies).Where("with_uin = ?", user.UIN).Relation("Source").Scan(context.Background(), &buddies)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, errors.Wrapf(err, "could not find user's buddies: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, buddy := range buddies {
|
||||||
|
if buddy.Source.Status != models.UserStatusActive {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
onlineSnac := oscar.NewSNAC(3, 0xb)
|
||||||
|
onlineSnac.Data.WriteLPString(buddy.Source.Username)
|
||||||
|
onlineSnac.Data.WriteUint16(0) // TODO: user warning level
|
||||||
|
|
||||||
|
tlvs := []*oscar.TLV{
|
||||||
|
oscar.NewTLV(1, util.Word(0x80)), // TODO: user class
|
||||||
|
oscar.NewTLV(0x06, util.Dword(0x0001|0x0100)), // TODO: User Status
|
||||||
|
oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP
|
||||||
|
oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time
|
||||||
|
oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time
|
||||||
|
oscar.NewTLV(0x05, util.Dword(uint32(user.CreatedAt.Unix()))), // Member since
|
||||||
|
}
|
||||||
|
|
||||||
|
onlineSnac.Data.WriteUint16(uint16(len(tlvs)))
|
||||||
|
for _, tlv := range tlvs {
|
||||||
|
onlineSnac.Data.WriteBinary(tlv)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 models.NewContextWithUser(ctx, user), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
|
|
||||||
// Client wants to know the rate limits for all services
|
// Client wants to know the rate limits for all services
|
||||||
case 0x06:
|
case 0x06:
|
||||||
rateSnac := oscar.NewSNAC(1, 7)
|
rateSnac := oscar.NewSNAC(1, 7)
|
||||||
|
@ -51,7 +110,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
|
|
||||||
// TODO: make actual rate groups instead of this hack. I can't tell which subtypes are supported so
|
// TODO: make actual rate groups instead of this hack. I can't tell which subtypes are supported so
|
||||||
// make it set rate limits for everything family for all subtypes under 0x21.
|
// make it set rate limits for everything family for all subtypes under 0x21.
|
||||||
rg.WriteUint16(2 * 0x21) // Number of rate groups
|
rg.WriteUint16(3 * 0x21) // Number of rate groups
|
||||||
for family := range versions {
|
for family := range versions {
|
||||||
for subtype := 0; subtype < 0x21; subtype++ {
|
for subtype := 0; subtype < 0x21; subtype++ {
|
||||||
rg.WriteUint16(family)
|
rg.WriteUint16(family)
|
||||||
|
@ -68,7 +127,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
case 0x0e:
|
case 0x0e:
|
||||||
user := models.UserFromContext(ctx)
|
user := models.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ctx, errors.New("expecting user in context")
|
return ctx, aimerror.NoUserInSession
|
||||||
}
|
}
|
||||||
|
|
||||||
onlineSnac := oscar.NewSNAC(1, 0xf)
|
onlineSnac := oscar.NewSNAC(1, 0xf)
|
||||||
|
@ -76,7 +135,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
onlineSnac.Data.WriteString(user.Username)
|
onlineSnac.Data.WriteString(user.Username)
|
||||||
onlineSnac.Data.WriteUint16(0) // warning level
|
onlineSnac.Data.WriteUint16(0) // warning level
|
||||||
|
|
||||||
user.Status = "active"
|
user.Status = models.UserStatusActive
|
||||||
if err := user.Update(ctx, db); err != nil {
|
if err := user.Update(ctx, db); err != nil {
|
||||||
return ctx, errors.Wrap(err, "could not set user as active")
|
return ctx, errors.Wrap(err, "could not set user as active")
|
||||||
}
|
}
|
||||||
|
@ -98,7 +157,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
||||||
|
|
||||||
onlineFlap := oscar.NewFLAP(2)
|
onlineFlap := oscar.NewFLAP(2)
|
||||||
onlineFlap.Data.WriteBinary(onlineSnac)
|
onlineFlap.Data.WriteBinary(onlineSnac)
|
||||||
return ctx, session.Send(onlineFlap)
|
return models.NewContextWithUser(ctx, user), session.Send(onlineFlap)
|
||||||
|
|
||||||
// Client wants to know the versions of all of the services offered
|
// Client wants to know the versions of all of the services offered
|
||||||
case 0x17:
|
case 0x17:
|
||||||
|
|
102
0x03_buddy_list_management.go
Normal file
102
0x03_buddy_list_management.go
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aim-oscar/aimerror"
|
||||||
|
"aim-oscar/models"
|
||||||
|
"aim-oscar/oscar"
|
||||||
|
"aim-oscar/util"
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuddyListManagement struct{}
|
||||||
|
|
||||||
|
func (b *BuddyListManagement) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) {
|
||||||
|
session, _ := oscar.SessionFromContext(ctx)
|
||||||
|
|
||||||
|
switch snac.Header.Subtype {
|
||||||
|
|
||||||
|
// Client wants to know the buddy list params + limitations
|
||||||
|
case 0x2:
|
||||||
|
limitSnac := oscar.NewSNAC(3, 3)
|
||||||
|
limitSnac.Data.WriteBinary(oscar.NewTLV(1, util.Word(500))) // Max buddy list size
|
||||||
|
limitSnac.Data.WriteBinary(oscar.NewTLV(2, util.Word(750))) // Max list watchers
|
||||||
|
limitSnac.Data.WriteBinary(oscar.NewTLV(3, util.Word(512))) // Max online notifications ?
|
||||||
|
|
||||||
|
limitFlap := oscar.NewFLAP(2)
|
||||||
|
limitFlap.Data.WriteBinary(limitSnac)
|
||||||
|
return ctx, session.Send(limitFlap)
|
||||||
|
|
||||||
|
// Add buddy
|
||||||
|
case 0x4:
|
||||||
|
user := models.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return ctx, aimerror.NoUserInSession
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(snac.Data.Bytes()) > 0 {
|
||||||
|
buddyScreename, err := snac.Data.ReadLPString()
|
||||||
|
if err != nil {
|
||||||
|
return ctx, errors.Wrap(err, "expecting more buddies in list")
|
||||||
|
}
|
||||||
|
|
||||||
|
buddy, err := models.UserByUsername(ctx, db, buddyScreename)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, errors.Wrap(err, "error looking for User")
|
||||||
|
}
|
||||||
|
if buddy == nil {
|
||||||
|
return ctx, aimerror.UserNotFound(buddyScreename)
|
||||||
|
}
|
||||||
|
|
||||||
|
rel := &models.Buddy{
|
||||||
|
SourceUIN: user.UIN,
|
||||||
|
WithUIN: buddy.UIN,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.NewInsert().Model(rel).Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s added buddy %s to buddy list", user.Username, buddyScreename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
|
|
||||||
|
// Remove buddies from user list
|
||||||
|
case 0x5:
|
||||||
|
user := models.UserFromContext(ctx)
|
||||||
|
if user == nil {
|
||||||
|
return ctx, aimerror.NoUserInSession
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(snac.Data.Bytes()) > 0 {
|
||||||
|
buddyScreename, err := snac.Data.ReadLPString()
|
||||||
|
if err != nil {
|
||||||
|
return ctx, errors.Wrap(err, "expecting more buddies in list")
|
||||||
|
}
|
||||||
|
|
||||||
|
buddy, err := models.UserByUsername(ctx, db, buddyScreename)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, errors.Wrap(err, "error looking for User")
|
||||||
|
}
|
||||||
|
if buddy == nil {
|
||||||
|
return ctx, aimerror.UserNotFound(buddyScreename)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.NewDelete().Model((*models.Buddy)(nil)).Where("uin = ?", user.UIN).Where("with_uin = ?", buddy.UIN).Exec(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return ctx, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s removed buddy %s from buddy list", user.Username, buddyScreename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx, nil
|
||||||
|
}
|
11
0x04_ICBM.go
11
0x04_ICBM.go
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"aim-oscar/aimerror"
|
||||||
"aim-oscar/models"
|
"aim-oscar/models"
|
||||||
"aim-oscar/oscar"
|
"aim-oscar/oscar"
|
||||||
"bytes"
|
"bytes"
|
||||||
|
@ -12,7 +13,9 @@ import (
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ICBM struct{}
|
type ICBM struct {
|
||||||
|
CommCh chan *models.Message
|
||||||
|
}
|
||||||
|
|
||||||
type icbmKey string
|
type icbmKey string
|
||||||
|
|
||||||
|
@ -46,7 +49,7 @@ type channel struct {
|
||||||
Unknown uint16
|
Unknown uint16
|
||||||
}
|
}
|
||||||
|
|
||||||
func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC, comm chan *models.Message) (context.Context, error) {
|
func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) {
|
||||||
session, _ := oscar.SessionFromContext(ctx)
|
session, _ := oscar.SessionFromContext(ctx)
|
||||||
|
|
||||||
switch snac.Header.Subtype {
|
switch snac.Header.Subtype {
|
||||||
|
@ -93,7 +96,7 @@ func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC,
|
||||||
case 0x06:
|
case 0x06:
|
||||||
user := models.UserFromContext(ctx)
|
user := models.UserFromContext(ctx)
|
||||||
if user == nil {
|
if user == nil {
|
||||||
return ctx, errors.New("context should have User")
|
return ctx, aimerror.NoUserInSession
|
||||||
}
|
}
|
||||||
|
|
||||||
msgID, _ := snac.Data.ReadUint64()
|
msgID, _ := snac.Data.ReadUint64()
|
||||||
|
@ -179,7 +182,7 @@ func (icbm *ICBM) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fire the message off into the communication channel to get delivered
|
// Fire the message off into the communication channel to get delivered
|
||||||
comm <- message
|
icbm.CommCh <- message
|
||||||
|
|
||||||
// The Client usually wants a response that the server got the message. It checks that the message
|
// The Client usually wants a response that the server got the message. It checks that the message
|
||||||
// back has the same message ID that was sent and the user it was sent to.
|
// back has the same message ID that was sent and the user it was sent to.
|
||||||
|
|
|
@ -74,7 +74,7 @@ func (a *AuthorizationRegistrationService) GenerateCipher() string {
|
||||||
return base32.StdEncoding.EncodeToString(randomBytes)[:CIPHER_LENGTH]
|
return base32.StdEncoding.EncodeToString(randomBytes)[:CIPHER_LENGTH]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC, comm chan *models.Message) (context.Context, error) {
|
func (a *AuthorizationRegistrationService) HandleSNAC(ctx context.Context, db *bun.DB, snac *oscar.SNAC) (context.Context, error) {
|
||||||
session, err := oscar.SessionFromContext(ctx)
|
session, err := oscar.SessionFromContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
util.PanicIfError(err)
|
util.PanicIfError(err)
|
||||||
|
|
9
aimerror/errors.go
Normal file
9
aimerror/errors.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package aimerror
|
||||||
|
|
||||||
|
import "github.com/pkg/errors"
|
||||||
|
|
||||||
|
func UserNotFound(screenname string) error {
|
||||||
|
return errors.Errorf("no user with UIN %s", screenname)
|
||||||
|
}
|
||||||
|
|
||||||
|
var NoUserInSession = errors.New("no user in session")
|
39
main.go
39
main.go
|
@ -12,6 +12,7 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
@ -33,6 +34,18 @@ var services map[uint16]Service
|
||||||
|
|
||||||
// Map username to session
|
// Map username to session
|
||||||
var sessions map[string]*oscar.Session
|
var sessions map[string]*oscar.Session
|
||||||
|
var sessionsMutex = &sync.RWMutex{}
|
||||||
|
|
||||||
|
func getSession(username string) *oscar.Session {
|
||||||
|
sessionsMutex.RLock()
|
||||||
|
s, ok := sessions[username]
|
||||||
|
sessionsMutex.RUnlock()
|
||||||
|
|
||||||
|
if ok {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
services = make(map[uint16]Service)
|
services = make(map[uint16]Service)
|
||||||
|
@ -57,7 +70,7 @@ func main() {
|
||||||
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
|
db.AddQueryHook(bundebug.NewQueryHook(bundebug.WithVerbose(true)))
|
||||||
|
|
||||||
// Register our DB models
|
// Register our DB models
|
||||||
db.RegisterModel((*models.User)(nil), (*models.Message)(nil))
|
db.RegisterModel((*models.User)(nil), (*models.Message)(nil), (*models.Buddy)(nil))
|
||||||
|
|
||||||
// dev: load in fixtures to test against
|
// dev: load in fixtures to test against
|
||||||
fixture := dbfixture.New(db, dbfixture.WithRecreateTables())
|
fixture := dbfixture.New(db, dbfixture.WithRecreateTables())
|
||||||
|
@ -77,15 +90,21 @@ func main() {
|
||||||
commCh, messageRoutine := MessageDelivery()
|
commCh, messageRoutine := MessageDelivery()
|
||||||
go messageRoutine(db)
|
go messageRoutine(db)
|
||||||
|
|
||||||
|
// Goroutine that listens for users who change their online status and notifies their buddies
|
||||||
|
onlineCh, onlineRoutine := OnlineNotification()
|
||||||
|
go onlineRoutine(db)
|
||||||
|
|
||||||
handleCloseFn := func(ctx context.Context, session *oscar.Session) {
|
handleCloseFn := func(ctx context.Context, session *oscar.Session) {
|
||||||
log.Printf("%v disconnected", session.RemoteAddr())
|
log.Printf("%v disconnected", session.RemoteAddr())
|
||||||
|
|
||||||
user := models.UserFromContext(ctx)
|
user := models.UserFromContext(ctx)
|
||||||
if user != nil {
|
if user != nil {
|
||||||
user.Status = "offline"
|
user.Status = models.UserStatusInactive
|
||||||
if err := user.Update(ctx, db); err != nil {
|
if err := user.Update(ctx, db); err != nil {
|
||||||
log.Print(errors.Wrap(err, "could not set user as active"))
|
log.Print(errors.Wrap(err, "could not set user as inactive"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onlineCh <- user
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,8 +138,10 @@ func main() {
|
||||||
|
|
||||||
// Send available services
|
// Send available services
|
||||||
servicesSnac := oscar.NewSNAC(1, 3)
|
servicesSnac := oscar.NewSNAC(1, 3)
|
||||||
servicesSnac.Data.WriteUint16(0x1)
|
for family := range versions {
|
||||||
servicesSnac.Data.WriteUint16(0x4)
|
servicesSnac.Data.WriteUint16(family)
|
||||||
|
}
|
||||||
|
|
||||||
servicesFlap := oscar.NewFLAP(2)
|
servicesFlap := oscar.NewFLAP(2)
|
||||||
servicesFlap.Data.WriteBinary(servicesSnac)
|
servicesFlap.Data.WriteBinary(servicesSnac)
|
||||||
session.Send(servicesFlap)
|
session.Send(servicesFlap)
|
||||||
|
@ -141,7 +162,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if service, ok := services[snac.Header.Family]; ok {
|
if service, ok := services[snac.Header.Family]; ok {
|
||||||
newCtx, err := service.HandleSNAC(ctx, db, snac, commCh)
|
newCtx, err := service.HandleSNAC(ctx, db, snac)
|
||||||
util.PanicIfError(err)
|
util.PanicIfError(err)
|
||||||
return newCtx
|
return newCtx
|
||||||
}
|
}
|
||||||
|
@ -156,14 +177,16 @@ func main() {
|
||||||
handler := oscar.NewHandler(handleFn, handleCloseFn)
|
handler := oscar.NewHandler(handleFn, handleCloseFn)
|
||||||
|
|
||||||
RegisterService(0x17, &AuthorizationRegistrationService{})
|
RegisterService(0x17, &AuthorizationRegistrationService{})
|
||||||
RegisterService(0x01, &GenericServiceControls{})
|
RegisterService(0x01, &GenericServiceControls{OnlineCh: onlineCh})
|
||||||
RegisterService(0x04, &ICBM{})
|
RegisterService(0x03, &BuddyListManagement{})
|
||||||
|
RegisterService(0x04, &ICBM{CommCh: commCh})
|
||||||
|
|
||||||
exitChan := make(chan os.Signal, 1)
|
exitChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(exitChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
signal.Notify(exitChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
||||||
go func() {
|
go func() {
|
||||||
<-exitChan
|
<-exitChan
|
||||||
close(commCh)
|
close(commCh)
|
||||||
|
close(onlineCh)
|
||||||
fmt.Println("Shutting down")
|
fmt.Println("Shutting down")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}()
|
}()
|
||||||
|
|
|
@ -16,6 +16,8 @@ func MessageDelivery() (chan *models.Message, routineFn) {
|
||||||
commCh := make(chan *models.Message, 1)
|
commCh := make(chan *models.Message, 1)
|
||||||
|
|
||||||
routine := func(db *bun.DB) {
|
routine := func(db *bun.DB) {
|
||||||
|
log.Printf("message delivery routine started")
|
||||||
|
|
||||||
for {
|
for {
|
||||||
message, more := <-commCh
|
message, more := <-commCh
|
||||||
if !more {
|
if !more {
|
||||||
|
@ -24,9 +26,9 @@ func MessageDelivery() (chan *models.Message, routineFn) {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("got a message: %s", message)
|
log.Printf("got a message: %s", message)
|
||||||
if s, ok := sessions[message.To]; ok {
|
if s := getSession(message.To); s != nil {
|
||||||
messageSnac := oscar.NewSNAC(4, 7)
|
messageSnac := oscar.NewSNAC(4, 7)
|
||||||
messageSnac.Data.WriteUint64(message.MessageID)
|
messageSnac.Data.WriteUint64(message.Cookie)
|
||||||
messageSnac.Data.WriteUint16(1)
|
messageSnac.Data.WriteUint16(1)
|
||||||
messageSnac.Data.WriteLPString(message.From)
|
messageSnac.Data.WriteLPString(message.From)
|
||||||
messageSnac.Data.WriteUint16(0) // TODO: sender's warning level
|
messageSnac.Data.WriteUint16(0) // TODO: sender's warning level
|
||||||
|
@ -60,14 +62,14 @@ func MessageDelivery() (chan *models.Message, routineFn) {
|
||||||
messageFlap := oscar.NewFLAP(2)
|
messageFlap := oscar.NewFLAP(2)
|
||||||
messageFlap.Data.WriteBinary(messageSnac)
|
messageFlap.Data.WriteBinary(messageSnac)
|
||||||
if err := s.Send(messageFlap); err != nil {
|
if err := s.Send(messageFlap); err != nil {
|
||||||
log.Panicf("could not deliver message %d: %s", message.MessageID, err.Error())
|
log.Panicf("could not deliver message %d: %s", message.Cookie, err.Error())
|
||||||
continue
|
continue
|
||||||
} else {
|
} else {
|
||||||
log.Printf("sent message %d to user %s at %s", message.MessageID, message.To, s.RemoteAddr())
|
log.Printf("sent message %d to user %s at %s", message.Cookie, message.To, s.RemoteAddr())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := message.MarkDelivered(context.Background(), db); err != nil {
|
if err := message.MarkDelivered(context.Background(), db); err != nil {
|
||||||
log.Panicf("could not mark message %d as delivered: %s", message.MessageID, err.Error())
|
log.Panicf("could not mark message %d as delivered: %s", message.Cookie, err.Error())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("could not find session for user %s", message.To)
|
log.Printf("could not find session for user %s", message.To)
|
||||||
|
|
12
models/Buddy.go
Normal file
12
models/Buddy.go
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
package models
|
||||||
|
|
||||||
|
import "github.com/uptrace/bun"
|
||||||
|
|
||||||
|
type Buddy struct {
|
||||||
|
bun.BaseModel `bun:"table:buddies"`
|
||||||
|
ID int `bun:",pk"`
|
||||||
|
SourceUIN int64 `bun:",notnull"`
|
||||||
|
Source *User `bun:"rel:has-one,join:source_uin=uin"`
|
||||||
|
WithUIN int64 `bun:",notnull"`
|
||||||
|
Target *User `bun:"rel:has-one,join:with_uin=uin"`
|
||||||
|
}
|
|
@ -11,7 +11,8 @@ import (
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
bun.BaseModel `bun:"table:messages"`
|
bun.BaseModel `bun:"table:messages"`
|
||||||
MessageID uint64 `bun:",pk,notnull,unique"`
|
ID int `bun:",pk"`
|
||||||
|
Cookie uint64 `bun:",notnull"`
|
||||||
From string
|
From string
|
||||||
To string
|
To string
|
||||||
Contents string
|
Contents string
|
||||||
|
@ -19,12 +20,12 @@ type Message struct {
|
||||||
DeliveredAt time.Time `bun:",nullzero"`
|
DeliveredAt time.Time `bun:",nullzero"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func InsertMessage(ctx context.Context, db *bun.DB, messageId uint64, from string, to string, contents string) (*Message, error) {
|
func InsertMessage(ctx context.Context, db *bun.DB, cookie uint64, from string, to string, contents string) (*Message, error) {
|
||||||
msg := &Message{
|
msg := &Message{
|
||||||
MessageID: messageId,
|
Cookie: cookie,
|
||||||
From: from,
|
From: from,
|
||||||
To: to,
|
To: to,
|
||||||
Contents: contents,
|
Contents: contents,
|
||||||
}
|
}
|
||||||
if _, err := db.NewInsert().Model(msg).Exec(ctx, msg); err != nil {
|
if _, err := db.NewInsert().Model(msg).Exec(ctx, msg); err != nil {
|
||||||
return nil, errors.Wrap(err, "could not update user")
|
return nil, errors.Wrap(err, "could not update user")
|
||||||
|
@ -39,7 +40,7 @@ func (m *Message) String() string {
|
||||||
|
|
||||||
func (m *Message) MarkDelivered(ctx context.Context, db *bun.DB) error {
|
func (m *Message) MarkDelivered(ctx context.Context, db *bun.DB) error {
|
||||||
m.DeliveredAt = time.Now()
|
m.DeliveredAt = time.Now()
|
||||||
if _, err := db.NewUpdate().Model(m).Where("message_id = ?", m.MessageID).Exec(ctx); err != nil {
|
if _, err := db.NewUpdate().Model(m).Where("cookie = ?", m.Cookie).Exec(ctx); err != nil {
|
||||||
return errors.Wrap(err, "could not mark message as updated")
|
return errors.Wrap(err, "could not mark message as updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,13 @@ import (
|
||||||
"github.com/uptrace/bun"
|
"github.com/uptrace/bun"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type UserStatus int16
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserStatusInactive UserStatus = iota
|
||||||
|
UserStatusActive
|
||||||
|
)
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
bun.BaseModel `bun:"table:users"`
|
bun.BaseModel `bun:"table:users"`
|
||||||
UIN int64 `bun:",pk,autoincrement"`
|
UIN int64 `bun:",pk,autoincrement"`
|
||||||
|
@ -18,7 +25,7 @@ type User struct {
|
||||||
Cipher string
|
Cipher string
|
||||||
CreatedAt 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"`
|
UpdatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp"`
|
||||||
Status string
|
Status UserStatus
|
||||||
LastActivityAt time.Time `bin:"-"`
|
LastActivityAt time.Time `bin:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,18 @@
|
||||||
- model: User
|
- model: User
|
||||||
rows:
|
rows:
|
||||||
- username: toof
|
- uin: 1
|
||||||
|
username: toof
|
||||||
password: bar
|
password: bar
|
||||||
email: toof@plot.club
|
email: toof@plot.club
|
||||||
- username: artem
|
- uin: 2
|
||||||
|
username: artem
|
||||||
password: bar
|
password: bar
|
||||||
email: artem@plot.club
|
email: artem@plot.club
|
||||||
- model: Message
|
- model: Message
|
||||||
rows: []
|
rows: []
|
||||||
|
- model: Buddy
|
||||||
|
rows:
|
||||||
|
- source_uin: 1
|
||||||
|
with_uin: 2
|
||||||
|
- source_uin: 2
|
||||||
|
with_uin: 1
|
||||||
|
|
96
online_routine.go
Normal file
96
online_routine.go
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"aim-oscar/models"
|
||||||
|
"aim-oscar/oscar"
|
||||||
|
"aim-oscar/util"
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/uptrace/bun"
|
||||||
|
)
|
||||||
|
|
||||||
|
func OnlineNotification() (chan *models.User, routineFn) {
|
||||||
|
commCh := make(chan *models.User, 1)
|
||||||
|
|
||||||
|
routine := func(db *bun.DB) {
|
||||||
|
log.Printf("online notification starting up")
|
||||||
|
|
||||||
|
for {
|
||||||
|
user, more := <-commCh
|
||||||
|
if !more {
|
||||||
|
log.Printf("online notification routine shutting down")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status == models.UserStatusActive {
|
||||||
|
fmt.Printf("%s is online", user.Username)
|
||||||
|
|
||||||
|
var buddies []*models.Buddy
|
||||||
|
err := db.NewSelect().Model(&buddies).Where("with_uin = ?", user.UIN).Relation("Source").Relation("Target").Scan(context.Background(), &buddies)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not find user's buddies: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, buddy := range buddies {
|
||||||
|
if s := getSession(buddy.Source.Username); s != nil {
|
||||||
|
onlineSnac := oscar.NewSNAC(3, 0xb)
|
||||||
|
onlineSnac.Data.WriteLPString(user.Username)
|
||||||
|
onlineSnac.Data.WriteUint16(0) // TODO: user warning level
|
||||||
|
|
||||||
|
tlvs := []*oscar.TLV{
|
||||||
|
oscar.NewTLV(1, util.Word(0x80)), // TODO: user class
|
||||||
|
oscar.NewTLV(0x06, util.Dword(0x0001|0x0100)), // TODO: User Status
|
||||||
|
oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // External IP
|
||||||
|
oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(user.LastActivityAt).Seconds()))), // Idle Time
|
||||||
|
oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // Client Signon Time
|
||||||
|
oscar.NewTLV(0x05, util.Dword(uint32(user.CreatedAt.Unix()))), // Member since
|
||||||
|
}
|
||||||
|
|
||||||
|
onlineSnac.Data.WriteUint16(uint16(len(tlvs)))
|
||||||
|
for _, tlv := range tlvs {
|
||||||
|
onlineSnac.Data.WriteBinary(tlv)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, buddy.Target.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.Status == models.UserStatusInactive {
|
||||||
|
var buddies []*models.Buddy
|
||||||
|
err := db.NewSelect().Model(&buddies).Where("with_uin = ?", user.UIN).Relation("Source").Relation("Target").Scan(context.Background(), &buddies)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("could not find user's buddies: %s", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, buddy := range buddies {
|
||||||
|
if s := getSession(buddy.Source.Username); s != nil {
|
||||||
|
offlineSnac := oscar.NewSNAC(3, 0xb)
|
||||||
|
offlineSnac.Data.WriteLPString(user.Username)
|
||||||
|
offlineSnac.Data.WriteUint16(0) // TODO: user warning level
|
||||||
|
offlineSnac.Data.WriteUint16(1)
|
||||||
|
offlineSnac.Data.WriteBinary(oscar.NewTLV(1, util.Dword(0x80)))
|
||||||
|
|
||||||
|
offlineFlap := oscar.NewFLAP(2)
|
||||||
|
offlineFlap.Data.WriteBinary(offlineSnac)
|
||||||
|
if err := s.Send(offlineFlap); err != nil {
|
||||||
|
log.Printf("could not tell %s that %s is offline", buddy.Source.Username, buddy.Target.Username)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return commCh, routine
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"aim-oscar/models"
|
|
||||||
"aim-oscar/oscar"
|
"aim-oscar/oscar"
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
|
@ -9,5 +8,5 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type Service interface {
|
type Service interface {
|
||||||
HandleSNAC(context.Context, *bun.DB, *oscar.SNAC, chan *models.Message) (context.Context, error)
|
HandleSNAC(context.Context, *bun.DB, *oscar.SNAC) (context.Context, error)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue