mirror of
https://github.com/amigan/aim-oscar-server.git
synced 2024-11-21 12:09:48 -05:00
Location Services
This commit is contained in:
parent
9dc97738b3
commit
b660db4462
9 changed files with 242 additions and 86 deletions
|
@ -18,6 +18,7 @@ var versions map[uint16]uint16
|
|||
func init() {
|
||||
versions = make(map[uint16]uint16)
|
||||
versions[1] = 3
|
||||
versions[2] = 1
|
||||
versions[3] = 1
|
||||
versions[4] = 1
|
||||
versions[17] = 1
|
||||
|
@ -36,7 +37,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
|||
case 0x02:
|
||||
user := models.UserFromContext(ctx)
|
||||
if user != nil {
|
||||
user.Status = models.UserStatusActive
|
||||
user.Status = models.UserStatusOnline
|
||||
if err := user.Update(ctx, db); err != nil {
|
||||
return ctx, errors.Wrap(err, "could not set user as active")
|
||||
}
|
||||
|
@ -51,7 +52,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
|||
}
|
||||
|
||||
for _, buddy := range buddies {
|
||||
if buddy.Source.Status != models.UserStatusActive {
|
||||
if buddy.Source.Status != models.UserStatusOnline {
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -61,7 +62,7 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
|||
|
||||
tlvs := []*oscar.TLV{
|
||||
oscar.NewTLV(1, util.Word(0)), // TODO: user class
|
||||
oscar.NewTLV(0x06, util.Dword(0x50)), // TODO: User Status
|
||||
oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // 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
|
||||
|
@ -110,7 +111,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
|
||||
// make it set rate limits for everything family for all subtypes under 0x21.
|
||||
rg.WriteUint16(3 * 0x21) // Number of rate groups
|
||||
rg.WriteUint16(uint16(len(versions)) * 0x21) // Number of rate groups
|
||||
for family := range versions {
|
||||
for subtype := 0; subtype < 0x21; subtype++ {
|
||||
rg.WriteUint16(family)
|
||||
|
@ -135,14 +136,14 @@ func (g *GenericServiceControls) HandleSNAC(ctx context.Context, db *bun.DB, sna
|
|||
onlineSnac.Data.WriteString(user.Username)
|
||||
onlineSnac.Data.WriteUint16(0) // warning level
|
||||
|
||||
user.Status = models.UserStatusActive
|
||||
user.Status = models.UserStatusOnline
|
||||
if err := user.Update(ctx, db); err != nil {
|
||||
return ctx, errors.Wrap(err, "could not set user as active")
|
||||
}
|
||||
|
||||
tlvs := []*oscar.TLV{
|
||||
oscar.NewTLV(0x01, util.Dword(0x80)), // User Class
|
||||
oscar.NewTLV(0x06, util.Dword(0x50)), // TODO: User Status
|
||||
oscar.NewTLV(0x01, util.Dword(0)), // User Class
|
||||
oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // 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
|
||||
|
|
154
0x02_location_services.go
Normal file
154
0x02_location_services.go
Normal file
|
@ -0,0 +1,154 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"aim-oscar/aimerror"
|
||||
"aim-oscar/models"
|
||||
"aim-oscar/oscar"
|
||||
"aim-oscar/util"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/uptrace/bun"
|
||||
)
|
||||
|
||||
type LocationServices struct {
|
||||
OnlineCh chan *models.User
|
||||
}
|
||||
|
||||
func (s *LocationServices) 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 limits/permissions for Location services
|
||||
case 0x02:
|
||||
paramsSnac := oscar.NewSNAC(2, 3)
|
||||
paramsSnac.Data.WriteBinary(oscar.NewTLV(1, util.Word(256))) // Max profile length TODO: error if user sends more
|
||||
|
||||
paramsFlap := oscar.NewFLAP(2)
|
||||
paramsFlap.Data.WriteBinary(paramsSnac)
|
||||
|
||||
return ctx, session.Send(paramsFlap)
|
||||
|
||||
// Client set profile/away message
|
||||
case 0x04:
|
||||
user := models.UserFromContext(ctx)
|
||||
if user == nil {
|
||||
return ctx, aimerror.NoUserInSession
|
||||
}
|
||||
|
||||
tlvs, err := oscar.UnmarshalTLVs(snac.Data.Bytes())
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "authentication request missing TLVs")
|
||||
}
|
||||
|
||||
awayMessageTLV := oscar.FindTLV(tlvs, 0x4)
|
||||
if awayMessageTLV != nil {
|
||||
// Away message encoding is set in TLV 0x3
|
||||
awayMessageMimeTLV := oscar.FindTLV(tlvs, 0x3)
|
||||
if awayMessageMimeTLV == nil {
|
||||
return nil, errors.New("missing away message mime TLV 0x3")
|
||||
}
|
||||
user.AwayMessage = string(awayMessageTLV.Data)
|
||||
user.AwayMessageEncoding = string(awayMessageMimeTLV.Data)
|
||||
|
||||
}
|
||||
|
||||
profileTLV := oscar.FindTLV(tlvs, 0x2)
|
||||
if profileTLV != nil {
|
||||
profileMimeTLV := oscar.FindTLV(tlvs, 0x1)
|
||||
if profileMimeTLV == nil {
|
||||
return nil, errors.New("missing away message mime TLV 0x3")
|
||||
}
|
||||
user.Profile = string(profileTLV.Data)
|
||||
user.ProfileEncoding = string(profileMimeTLV.Data)
|
||||
}
|
||||
|
||||
if user.AwayMessage == "" {
|
||||
user.Status = models.UserStatusOnline
|
||||
} else {
|
||||
user.Status = models.UserStatusAway
|
||||
}
|
||||
|
||||
if err := user.Update(ctx, db); err != nil {
|
||||
return ctx, errors.Wrap(err, "could not set away message")
|
||||
}
|
||||
|
||||
s.OnlineCh <- user
|
||||
|
||||
return models.NewContextWithUser(ctx, user), nil
|
||||
|
||||
// Client is asking for user information like profile, away message, online state
|
||||
case 0x5:
|
||||
requestType, err := snac.Data.ReadUint16()
|
||||
if err != nil {
|
||||
return ctx, errors.Wrap(err, "missing request type")
|
||||
}
|
||||
|
||||
requestedUsername, err := snac.Data.ReadLPString()
|
||||
if err != nil {
|
||||
return ctx, errors.Wrap(err, "missing requested username")
|
||||
}
|
||||
|
||||
requestedUser, err := models.UserByUsername(ctx, db, requestedUsername)
|
||||
if err != nil {
|
||||
return ctx, aimerror.FetchingUser(err, requestedUsername)
|
||||
}
|
||||
|
||||
respSnac := oscar.NewSNAC(2, 6)
|
||||
respSnac.Data.WriteLPString(requestedUser.Username)
|
||||
respSnac.Data.WriteUint16(0) // TODO: warning level
|
||||
|
||||
tlvs := []*oscar.TLV{
|
||||
oscar.NewTLV(1, util.Dword(0x80)), // user class
|
||||
// oscar.NewTLV(6, util.Dword(uint32(requestedUser.Status))), // user status
|
||||
// oscar.NewTLV(0x0a, util.Dword(binary.BigEndian.Uint32([]byte(SRV_HOST)))), // user external IP
|
||||
oscar.NewTLV(0x0f, util.Dword(uint32(time.Since(requestedUser.LastActivityAt).Seconds()))), // idle time
|
||||
oscar.NewTLV(0x03, util.Dword(uint32(time.Now().Unix()))), // TODO: signon time
|
||||
// oscar.NewTLV(0x05, util.Dword(uint32(requestedUser.CreatedAt.Unix()))), // member since
|
||||
}
|
||||
|
||||
respSnac.Data.WriteUint16(uint16(len(tlvs))) // number of TLVs
|
||||
for _, tlv := range tlvs {
|
||||
respSnac.Data.WriteBinary(tlv)
|
||||
}
|
||||
|
||||
// General info (Profile)
|
||||
if requestType == 1 {
|
||||
respSnac.Data.WriteBinary(oscar.NewTLV(1, util.LPString(requestedUser.ProfileEncoding)))
|
||||
respSnac.Data.WriteBinary(oscar.NewTLV(2, util.LPString(requestedUser.Profile)))
|
||||
}
|
||||
|
||||
// Request Type 2 = online status, no TLVs
|
||||
|
||||
// Away message
|
||||
if requestType == 3 {
|
||||
respSnac.Data.WriteBinary(oscar.NewTLV(3, util.LPString(requestedUser.AwayMessageEncoding)))
|
||||
respSnac.Data.WriteBinary(oscar.NewTLV(4, util.LPString(requestedUser.AwayMessage)))
|
||||
}
|
||||
|
||||
// TODO: Request Type 4 - User capabilities
|
||||
|
||||
respFlap := oscar.NewFLAP(2)
|
||||
respFlap.Data.WriteBinary(respSnac)
|
||||
|
||||
return ctx, session.Send(respFlap)
|
||||
|
||||
case 0xb:
|
||||
/* Nobody seems to know what this client request is for
|
||||
- http://iserverd.khstu.ru/oscar/snac_02_0b.html
|
||||
- https://bugs.bitlbee.org/browser/protocols/oscar/info.c?rev=b7d3cc34f68dab7b8f7d0777711317b334fc2219#L572
|
||||
|
||||
But the one dump that exists looks like a TLV 0x1 with empty data
|
||||
*/
|
||||
unknownSnac := oscar.NewSNAC(2, 0xc)
|
||||
unknownSnac.Data.WriteUint16(1)
|
||||
unknownSnac.Data.WriteUint16(0)
|
||||
unknownFlap := oscar.NewFLAP(2)
|
||||
unknownFlap.Data.WriteBinary(unknownSnac)
|
||||
return ctx, session.Send(unknownFlap)
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
|
@ -87,7 +87,7 @@ func (b *BuddyListManagement) HandleSNAC(ctx context.Context, db *bun.DB, snac *
|
|||
return ctx, aimerror.UserNotFound(buddyScreename)
|
||||
}
|
||||
|
||||
_, err = db.NewDelete().Model((*models.Buddy)(nil)).Where("uin = ?", user.UIN).Where("with_uin = ?", buddy.UIN).Exec(ctx)
|
||||
_, err = db.NewDelete().Model((*models.Buddy)(nil)).Where("source_uin = ?", user.UIN).Where("with_uin = ?", buddy.UIN).Exec(ctx)
|
||||
if err != nil {
|
||||
return ctx, err
|
||||
}
|
||||
|
|
|
@ -2,8 +2,12 @@ package aimerror
|
|||
|
||||
import "github.com/pkg/errors"
|
||||
|
||||
func UserNotFound(screenname string) error {
|
||||
return errors.Errorf("no user with UIN %s", screenname)
|
||||
func FetchingUser(err error, username string) error {
|
||||
return errors.Wrapf(err, "could not fetch user with username %s", username)
|
||||
}
|
||||
|
||||
func UserNotFound(username string) error {
|
||||
return errors.Errorf("no user with UIN %s", username)
|
||||
}
|
||||
|
||||
var NoUserInSession = errors.New("no user in session")
|
||||
|
|
10
main.go
10
main.go
|
@ -99,12 +99,15 @@ func main() {
|
|||
|
||||
user := models.UserFromContext(ctx)
|
||||
if user != nil {
|
||||
user.Status = models.UserStatusInactive
|
||||
// tellBuddies := user.Status != models.UserStatusAway
|
||||
user.Status = models.UserStatusAway
|
||||
if err := user.Update(ctx, db); err != nil {
|
||||
log.Print(errors.Wrap(err, "could not set user as inactive"))
|
||||
}
|
||||
|
||||
onlineCh <- user
|
||||
if true {
|
||||
onlineCh <- user
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -176,10 +179,11 @@ func main() {
|
|||
|
||||
handler := oscar.NewHandler(handleFn, handleCloseFn)
|
||||
|
||||
RegisterService(0x17, &AuthorizationRegistrationService{})
|
||||
RegisterService(0x01, &GenericServiceControls{OnlineCh: onlineCh})
|
||||
RegisterService(0x02, &LocationServices{OnlineCh: onlineCh})
|
||||
RegisterService(0x03, &BuddyListManagement{})
|
||||
RegisterService(0x04, &ICBM{CommCh: commCh})
|
||||
RegisterService(0x17, &AuthorizationRegistrationService{})
|
||||
|
||||
exitChan := make(chan os.Signal, 1)
|
||||
signal.Notify(exitChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM, syscall.SIGABRT)
|
||||
|
|
|
@ -12,21 +12,30 @@ import (
|
|||
type UserStatus int16
|
||||
|
||||
const (
|
||||
UserStatusInactive UserStatus = iota
|
||||
UserStatusActive
|
||||
UserStatusOnline = 0
|
||||
UserStatusAway = 1
|
||||
UserStatusDnd = 2
|
||||
UserStatusNA = 4
|
||||
UserStatusOccupied = 0x10
|
||||
UserStatusFree4Chat = 0x20
|
||||
UserStatusInvisible = 0x100
|
||||
)
|
||||
|
||||
type User struct {
|
||||
bun.BaseModel `bun:"table:users"`
|
||||
UIN int64 `bun:",pk,autoincrement"`
|
||||
Email string `bun:",unique"`
|
||||
Username 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"`
|
||||
Status UserStatus
|
||||
LastActivityAt time.Time `bin:"-"`
|
||||
bun.BaseModel `bun:"table:users"`
|
||||
UIN int64 `bun:",pk,autoincrement"`
|
||||
Email string `bun:",unique"`
|
||||
Username 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"`
|
||||
Status UserStatus
|
||||
Profile string
|
||||
ProfileEncoding string
|
||||
AwayMessage string
|
||||
AwayMessageEncoding string
|
||||
LastActivityAt time.Time `bin:"-"`
|
||||
}
|
||||
|
||||
type userKey string
|
||||
|
|
|
@ -11,8 +11,4 @@
|
|||
- model: Message
|
||||
rows: []
|
||||
- model: Buddy
|
||||
rows:
|
||||
- source_uin: 1
|
||||
with_uin: 2
|
||||
- source_uin: 2
|
||||
with_uin: 1
|
||||
rows: []
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"aim-oscar/util"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
|
@ -26,66 +25,51 @@ func OnlineNotification() (chan *models.User, routineFn) {
|
|||
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(0)), // TODO: user class
|
||||
oscar.NewTLV(0x06, util.Dword(0x50)), // 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.UserStatusOnline {
|
||||
log.Printf("%s is now online", user.Username)
|
||||
} else if user.Status == models.UserStatusAway {
|
||||
log.Printf("%s is now away", user.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
|
||||
ctx := context.Background()
|
||||
|
||||
// Find buddies who are friends with the user
|
||||
var buddies []*models.Buddy
|
||||
err := db.NewSelect().Model(&buddies).Where("with_uin = ?", user.UIN).Relation("Source").Scan(ctx, &buddies)
|
||||
if err != nil {
|
||||
log.Printf("could not find user's buddies: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, buddy := range buddies {
|
||||
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)
|
||||
|
||||
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)))
|
||||
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
|
||||
|
||||
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)
|
||||
}
|
||||
tlvs := []*oscar.TLV{
|
||||
oscar.NewTLV(1, util.Word(0)), // TODO: user class
|
||||
oscar.NewTLV(0x06, util.Dword(uint32(user.Status))), // 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, user.Username)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -60,3 +60,7 @@ func Word(x uint16) []byte {
|
|||
func Dword(x uint32) []byte {
|
||||
return []byte{byte(x >> 24), byte(x >> 16), byte(x >> 8), byte(x & 0xf)}
|
||||
}
|
||||
|
||||
func LPString(x string) []byte {
|
||||
return append(Word(uint16(len(x))), []byte(x)...)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue