buddy list management service

This commit is contained in:
Artem Titoulenko 2021-12-19 01:14:25 -05:00
parent 68b315345b
commit 00c2f20caf
13 changed files with 357 additions and 36 deletions

View file

@ -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:

View 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
}

View file

@ -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.

View file

@ -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
View 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
View file

@ -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)
}() }()

View file

@ -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
View 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"`
}

View file

@ -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")
} }

View file

@ -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:"-"`
} }

View file

@ -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
View 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
}

View file

@ -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)
} }