Mike 3 лет назад
Родитель
Сommit
7f1b0ee53f
6 измененных файлов с 359 добавлено и 44 удалено
  1. 14 8
      cmd/main.go
  2. 5 0
      go.mod
  3. 66 34
      oscar/feedbag.go
  4. 152 0
      oscar/feedbag_store.go
  5. 120 0
      oscar/feedbag_store_test.go
  6. 2 2
      oscar/protocol.go

+ 14 - 8
cmd/main.go

@@ -11,11 +11,17 @@ import (
 	"strings"
 )
 
+const testFile string = "/Users/mike/dev/goaim/aim.db"
+
 func main() {
 
 	sm := oscar.NewSessionManager()
+	fm, err := oscar.NewFeedbagStore(testFile)
+	if err != nil {
+		log.Fatal(err)
+	}
 
-	go listenBOS(sm)
+	go listenBOS(sm, fm)
 	go listenStats()
 	go listenAlert()
 	go listenOdir()
@@ -56,7 +62,7 @@ func webServer(ch chan string) {
 	}
 }
 
-func listenBOS(sm *oscar.SessionManager) {
+func listenBOS(sm *oscar.SessionManager, fm *oscar.FeedbagStore) {
 	// Listen on TCP port 5190
 	listener, err := net.Listen("tcp", ":5191")
 	if err != nil {
@@ -78,7 +84,7 @@ func listenBOS(sm *oscar.SessionManager) {
 			continue
 		}
 		seq := uint32(100)
-		go handleBOSConnection(sm, conn, &seq)
+		go handleBOSConnection(sm, fm, conn, &seq)
 		go sendIM(conn, ch, &seq)
 	}
 }
@@ -118,7 +124,7 @@ func listenStats() {
 
 		fmt.Println("got a connection on listenStats")
 		seq := uint32(100)
-		if err := oscar.ReadBos(nil, conn, &seq); err != nil {
+		if err := oscar.ReadBos(nil, nil, conn, &seq); err != nil {
 			if err == io.EOF {
 				break
 			} else {
@@ -150,7 +156,7 @@ func listenAlert() {
 
 		fmt.Println("got a connection on listenAlert")
 		seq := uint32(100)
-		if err := oscar.ReadBos(nil, conn, &seq); err != nil && err != io.EOF {
+		if err := oscar.ReadBos(nil, nil, conn, &seq); err != nil && err != io.EOF {
 			if err == io.EOF {
 				break
 			} else {
@@ -182,7 +188,7 @@ func listenOdir() {
 
 		fmt.Println("got a connection on listenOdir")
 		seq := uint32(100)
-		if err := oscar.ReadBos(nil, conn, &seq); err != nil {
+		if err := oscar.ReadBos(nil, nil, conn, &seq); err != nil {
 			if err == io.EOF {
 				break
 			} else {
@@ -220,7 +226,7 @@ func handleAuthConnection(sm *oscar.SessionManager, conn net.Conn) {
 	}
 }
 
-func handleBOSConnection(sm *oscar.SessionManager, conn net.Conn, seq *uint32) {
+func handleBOSConnection(sm *oscar.SessionManager, fm *oscar.FeedbagStore, conn net.Conn, seq *uint32) {
 	fmt.Println("VerifyLogin...")
 	sess, err := oscar.VerifyLogin(sm, conn, seq)
 	if err != nil {
@@ -236,7 +242,7 @@ func handleBOSConnection(sm *oscar.SessionManager, conn net.Conn, seq *uint32) {
 		}
 	}
 
-	if err := oscar.ReadBos(sess, conn, seq); err != nil && err != io.EOF {
+	if err := oscar.ReadBos(sess, fm, conn, seq); err != nil && err != io.EOF {
 		if err != io.EOF {
 			fmt.Println(err.Error())
 			os.Exit(1)

+ 5 - 0
go.mod

@@ -1,3 +1,8 @@
 module github.com/mkaminski/goaim
 
 go 1.20
+
+require (
+	github.com/google/uuid v1.3.0 // indirect
+	github.com/mattn/go-sqlite3 v1.14.17 // indirect
+)

+ 66 - 34
oscar/feedbag.go

@@ -6,7 +6,6 @@ import (
 	"fmt"
 	"io"
 	"reflect"
-	"time"
 )
 
 const (
@@ -81,25 +80,22 @@ const (
 	FeedbagClassIdMin                     = 0x0400
 )
 
-var feedbag []*feedbagItem
-var feedbagModified = time.Now().Unix()
-
-func routeFeedbag(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func routeFeedbag(sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	switch snac.subGroup {
 	case FeedbagErr:
 		panic("not implemented")
 	case FeedbagRightsQuery:
 		return SendAndReceiveFeedbagRightsQuery(flap, snac, r, w, sequence)
 	case FeedbagQuery:
-		return ReceiveAndSendFeedbagQuery(flap, snac, r, w, sequence)
+		return ReceiveAndSendFeedbagQuery(sess, fm, flap, snac, r, w, sequence)
 	case FeedbagQueryIfModified:
-		return ReceiveAndSendFeedbagQueryIfModified(flap, snac, r, w, sequence)
+		return ReceiveAndSendFeedbagQueryIfModified(sess, fm, flap, snac, r, w, sequence)
 	case FeedbagUse:
 		return ReceiveUse(flap, snac, r, w, sequence)
 	case FeedbagInsertItem:
-		return ReceiveInsertItem(flap, snac, r, w, sequence)
+		return ReceiveInsertItem(sess, fm, flap, snac, r, w, sequence)
 	case FeedbagUpdateItem:
-		return ReceiveUpdateItem(flap, snac, r, w, sequence)
+		return ReceiveUpdateItem(sess, fm, flap, snac, r, w, sequence)
 	case FeedbagDeleteItem:
 		panic("not implemented")
 	case FeedbagInsertClass:
@@ -315,9 +311,12 @@ func (f *feedbagItem) read(r io.Reader) error {
 	if err := binary.Read(r, binary.BigEndian, &l); err != nil {
 		return err
 	}
+	buf = make([]byte, l)
+	if _, err := r.Read(buf); err != nil {
+		return err
+	}
 
-	buff := bytes.NewBuffer(make([]byte, l))
-	return f.TLVPayload.read(buff, map[uint16]reflect.Kind{
+	return f.TLVPayload.read(bytes.NewBuffer(buf), map[uint16]reflect.Kind{
 		0xC8:                           reflect.Slice,
 		FeedbagClassIdBuddy:            reflect.Slice,
 		FeedbagClassIdGroup:            reflect.Slice,
@@ -373,17 +372,31 @@ func (s *snacFeedbagQuery) write(w io.Writer) error {
 	return binary.Write(w, binary.BigEndian, s.lastUpdate)
 }
 
-func ReceiveAndSendFeedbagQuery(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendFeedbagQuery(sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("receiveAndSendFeedbagQuery read SNAC frame: %+v\n", snac)
 
+	fb, err := fm.Retrieve(sess.screenName)
+	if err != nil {
+		return err
+	}
+
+	var lastModified uint32
+	if len(fb) > 0 {
+		lm, err := fm.LastModified(sess.screenName)
+		if err != nil {
+			return err
+		}
+		lastModified = uint32(lm.Unix())
+	}
+
 	snacFrameOut := snacFrame{
 		foodGroup: 0x13,
 		subGroup:  0x06,
 	}
 	snacPayloadOut := &snacFeedbagQuery{
 		version:    0,
-		items:      feedbag,
-		lastUpdate: uint32(feedbagModified),
+		items:      fb,
+		lastUpdate: lastModified,
 	}
 
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
@@ -408,7 +421,7 @@ func (s *snacQueryIfModified) read(r io.Reader) error {
 	return binary.Read(r, binary.BigEndian, &s.count)
 }
 
-func ReceiveAndSendFeedbagQueryIfModified(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveAndSendFeedbagQueryIfModified(sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveAndSendFeedbagQueryIfModified read SNAC frame: %+v\n", snac)
 
 	snacPayload := &snacQueryIfModified{}
@@ -418,29 +431,41 @@ func ReceiveAndSendFeedbagQueryIfModified(flap *flapFrame, snac *snacFrame, r io
 
 	fmt.Printf("ReceiveAndSendFeedbagQueryIfModified read SNAC: %+v\n", snacPayload)
 
-	lenz := uint8(len(feedbag))
-	if lenz > 0 && snacPayload.count == lenz {
-
-		snacFrameOut := snacFrame{
-			foodGroup: 0x13,
-			subGroup:  0x0F,
-		}
-		snacPayloadOut := &snacQueryIfModified{
-			lastUpdate: uint32(feedbagModified),
-			count:      lenz,
-		}
+	fb, err := fm.Retrieve(sess.screenName)
+	if err != nil {
+		return err
+	}
 
-		return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
+	lm, err := fm.LastModified(sess.screenName)
+	if err != nil {
+		return err
 	}
 
+	//if lm.Before(time.Unix(int64(snacPayload.lastUpdate), 0)) {
+	//todo not sure this works right now
+	//	snacFrameOut := snacFrame{
+	//		foodGroup: 0x13,
+	//		subGroup:  0x0F,
+	//	}
+	//	lm, err := fm.LastModified(sess.screenName)
+	//	if err != nil {
+	//		return err
+	//	}
+	//	snacPayloadOut := &snacQueryIfModified{
+	//		lastUpdate: uint32(lm.Unix()),
+	//		count:      uint8(len(fb)),
+	//	}
+	//	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
+	//}
+
 	snacFrameOut := snacFrame{
 		foodGroup: 0x13,
 		subGroup:  0x06,
 	}
 	snacPayloadOut := &snacFeedbagQuery{
 		version:    0,
-		items:      feedbag,
-		lastUpdate: uint32(feedbagModified),
+		items:      fb,
+		lastUpdate: uint32(lm.Unix()),
 	}
 
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
@@ -454,10 +479,11 @@ func (s *snacFeedbagStatusReply) write(w io.Writer) error {
 	return binary.Write(w, binary.BigEndian, s.results)
 }
 
-func ReceiveInsertItem(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveInsertItem(sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveInsertItem read SNAC frame: %+v\n", snac)
 
 	snacPayloadOut := &snacFeedbagStatusReply{}
+	var feedbag []*feedbagItem
 
 	for {
 		item := &feedbagItem{}
@@ -472,15 +498,19 @@ func ReceiveInsertItem(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Write
 		fmt.Printf("ReceiveInsertItem read SNAC feedbag item: %+v\n", item)
 	}
 
+	if err := fm.Upsert(sess.screenName, feedbag); err != nil {
+		return err
+	}
+
 	snacFrameOut := snacFrame{
 		foodGroup: FEEDBAG,
 		subGroup:  FeedbagStatus,
 	}
-	feedbagModified = time.Now().Unix()
+
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
 }
 
-func ReceiveUpdateItem(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
+func ReceiveUpdateItem(sess *Session, fm *FeedbagStore, flap *flapFrame, snac *snacFrame, r io.Reader, w io.Writer, sequence *uint32) error {
 	fmt.Printf("ReceiveUpdateItem read SNAC frame: %+v\n", snac)
 
 	var items []*feedbagItem
@@ -497,6 +527,10 @@ func ReceiveUpdateItem(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Write
 		items = append(items, item)
 	}
 
+	if err := fm.Upsert(sess.screenName, items); err != nil {
+		return err
+	}
+
 	snacPayloadOut := &snacFeedbagStatusReply{}
 
 	for _, item := range items {
@@ -509,8 +543,6 @@ func ReceiveUpdateItem(flap *flapFrame, snac *snacFrame, r io.Reader, w io.Write
 		subGroup:  FeedbagStatus,
 	}
 
-	feedbagModified = time.Now().Unix()
-
 	return writeOutSNAC(snac, flap, snacFrameOut, snacPayloadOut, sequence, w)
 }
 

+ 152 - 0
oscar/feedbag_store.go

@@ -0,0 +1,152 @@
+package oscar
+
+import (
+	"bytes"
+	"database/sql"
+	_ "github.com/mattn/go-sqlite3"
+	"reflect"
+	"time"
+)
+
+const file string = "/Users/mike/dev/goaim/aim.db"
+
+var feedbagDDL = `
+	CREATE TABLE IF NOT EXISTS user
+	(
+		screenName VARCHAR(16) PRIMARY KEY
+	);
+	CREATE TABLE IF NOT EXISTS feedbag
+	(
+		screenName   VARCHAR(16),
+		groupID      INTEGER,
+		itemID       INTEGER,
+		classID      INTEGER,
+		name         TEXT,
+		attributes   BLOB,
+		lastModified INTEGER,
+		UNIQUE (screenName, groupID, itemID)
+	);
+`
+
+func NewFeedbagStore(dbFile string) (*FeedbagStore, error) {
+	db, err := sql.Open("sqlite3", dbFile)
+	if err != nil {
+		return nil, err
+	}
+	if _, err := db.Exec(feedbagDDL); err != nil {
+		return nil, err
+	}
+	return &FeedbagStore{db: db}, nil
+}
+
+type FeedbagStore struct {
+	db *sql.DB
+}
+
+func (f *FeedbagStore) Retrieve(screenName string) ([]*feedbagItem, error) {
+	q := `
+		SELECT 
+			groupID,
+			itemID,
+			classID,
+			name,
+			attributes
+		FROM feedbag
+		WHERE screenName = ?
+	`
+
+	rows, err := f.db.Query(q, screenName)
+	if err != nil {
+		return nil, err
+	}
+	defer rows.Close()
+
+	var items []*feedbagItem
+	for rows.Next() {
+		var item feedbagItem
+		var attrs []byte
+		if err := rows.Scan(&item.groupID, &item.itemID, &item.classID, &item.name, &attrs); err != nil {
+			return nil, err
+		}
+		err = item.TLVPayload.read(bytes.NewBuffer(attrs), map[uint16]reflect.Kind{
+			0xC8: reflect.Slice,
+			//FeedbagClassIdBuddy:            reflect.Slice,
+			FeedbagClassIdGroup: reflect.Uint16,
+			//FeedbagClassIdPermit:           reflect.Slice,
+			//FeedbagClassIdDeny:             reflect.Slice,
+			//FeedbagClassIdPdinfo:           reflect.Slice,
+			//FeedbagClassIdBuddyPrefs:       reflect.Slice,
+			//FeedbagClassIdNonbuddy:         reflect.Slice,
+			//FeedbagClassIdTpaProvider:      reflect.Slice,
+			//FeedbagClassIdTpaSubscription:  reflect.Slice,
+			//FeedbagClassIdClientPrefs:      reflect.Slice,
+			//FeedbagClassIdStock:            reflect.Slice,
+			//FeedbagClassIdWeather:          reflect.Slice,
+			//FeedbagClassIdWatchList:        reflect.Slice,
+			//FeedbagClassIdIgnoreList:       reflect.Slice,
+			//FeedbagClassIdDateTime:         reflect.Slice,
+			//FeedbagClassIdExternalUser:     reflect.Slice,
+			//FeedbagClassIdRootCreator:      reflect.Slice,
+			//FeedbagClassIdFish:             reflect.Slice,
+			//FeedbagClassIdImportTimestamp:  reflect.Slice,
+			//FeedbagClassIdBart:             reflect.Slice,
+			FeedbagClassIdRbOrder: reflect.Uint16,
+			//FeedbagClassIdPersonality:      reflect.Slice,
+			//FeedbagClassIdAlProf:           reflect.Slice,
+			//FeedbagClassIdAlInfo:           reflect.Slice,
+			//FeedbagClassIdInteraction:      reflect.Slice,
+			//FeedbagClassIdVanityInfo:       reflect.Slice,
+			//FeedbagClassIdFavoriteLocation: reflect.Slice,
+			//FeedbagClassIdBartPdinfo:       reflect.Slice,
+			//FeedbagClassIdXIcqStatusNote:   reflect.Slice,
+			//FeedbagClassIdMin:              reflect.Slice,
+		})
+		if err != nil {
+			return items, err
+		}
+		items = append(items, &item)
+	}
+
+	return items, nil
+}
+
+func (f *FeedbagStore) LastModified(screenName string) (time.Time, error) {
+	var lastModified sql.NullInt64
+	sql := `SELECT MAX(lastModified) FROM feedbag WHERE screenName = ?`
+	err := f.db.QueryRow(sql, screenName).Scan(&lastModified)
+	return time.Unix(lastModified.Int64, 0), err
+}
+
+func (f *FeedbagStore) Upsert(screenName string, items []*feedbagItem) error {
+
+	q := `
+		INSERT INTO feedbag (screenName, groupID, itemID, classID, name, attributes, lastModified)
+		VALUES (?, ?, ?, ?, ?, ?, UNIXEPOCH())
+		ON CONFLICT (screenName, groupID, itemID)
+			DO UPDATE SET classID      = excluded.classID,
+						  name         = excluded.name,
+						  attributes   = excluded.attributes,
+						  lastModified = UNIXEPOCH()
+	`
+
+	for _, item := range items {
+
+		buf := &bytes.Buffer{}
+		if err := item.TLVPayload.write(buf); err != nil {
+			return err
+		}
+
+		_, err := f.db.Exec(q,
+			screenName,
+			item.groupID,
+			item.itemID,
+			item.classID,
+			item.name,
+			buf.Bytes())
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}

+ 120 - 0
oscar/feedbag_store_test.go

@@ -0,0 +1,120 @@
+package oscar
+
+import (
+	"os"
+	"reflect"
+	"testing"
+)
+
+func TestFeedbagStore(t *testing.T) {
+
+	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const screenName = "sn2day"
+
+	defer func() {
+		err := os.Remove(testFile)
+		if err != nil {
+			t.Error("unable to clean up test file")
+		}
+	}()
+
+	f, err := NewFeedbagStore(testFile)
+	if err != nil {
+		t.Fatalf("failed to create new feedbag store: %s", err.Error())
+	}
+
+	itemsIn := []*feedbagItem{
+		{
+			groupID: 0,
+			itemID:  1805,
+			classID: 3,
+			name:    "spimmer1234",
+			TLVPayload: TLVPayload{
+				TLVs: []*TLV{
+					{
+						tType: 0x01,
+						val:   uint16(1000),
+					},
+				},
+			},
+		},
+		{
+			groupID: 0x0A,
+			itemID:  0,
+			classID: 1,
+			name:    "Friends",
+		},
+	}
+	if err := f.Upsert(screenName, itemsIn); err != nil {
+		t.Fatalf("failed to upsert: %s", err.Error())
+	}
+
+	itemsOut, err := f.Retrieve(screenName)
+	if err != nil {
+		t.Fatalf("failed to retrieve: %s", err.Error())
+	}
+
+	if !reflect.DeepEqual(itemsIn, itemsOut) {
+		t.Fatalf("items did not match:\n in: %v\n out: %v", itemsIn, itemsOut)
+	}
+}
+
+func TestLastModifiedEmpty(t *testing.T) {
+
+	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const screenName = "sn2day"
+
+	defer func() {
+		err := os.Remove(testFile)
+		if err != nil {
+			t.Error("unable to clean up test file")
+		}
+	}()
+
+	f, err := NewFeedbagStore(testFile)
+	if err != nil {
+		t.Fatalf("failed to create new feedbag store: %s", err.Error())
+	}
+
+	_, err = f.LastModified(screenName)
+
+	if err != nil {
+		t.Fatalf("get error from last modified: %s", err.Error())
+	}
+}
+
+func TestLastModifiedNotEmpty(t *testing.T) {
+
+	const testFile string = "/Users/mike/dev/goaim/aim_test.db"
+	const screenName = "sn2day"
+
+	defer func() {
+		err := os.Remove(testFile)
+		if err != nil {
+			t.Error("unable to clean up test file")
+		}
+	}()
+
+	f, err := NewFeedbagStore(testFile)
+	if err != nil {
+		t.Fatalf("failed to create new feedbag store: %s", err.Error())
+	}
+
+	itemsIn := []*feedbagItem{
+		{
+			groupID: 0x0A,
+			itemID:  0,
+			classID: 1,
+			name:    "Friends",
+		},
+	}
+	if err := f.Upsert(screenName, itemsIn); err != nil {
+		t.Fatalf("failed to upsert: %s", err.Error())
+	}
+
+	_, err = f.LastModified(screenName)
+
+	if err != nil {
+		t.Fatalf("get error from last modified: %s", err.Error())
+	}
+}

+ 2 - 2
oscar/protocol.go

@@ -364,7 +364,7 @@ const (
 	ARS                  = 0x044A
 )
 
-func ReadBos(sess *Session, rw io.ReadWriter, sequence *uint32) error {
+func ReadBos(sess *Session, fm *FeedbagStore, rw io.ReadWriter, sequence *uint32) error {
 	for {
 		// receive
 		flap := &flapFrame{}
@@ -420,7 +420,7 @@ func ReadBos(sess *Session, rw io.ReadWriter, sequence *uint32) error {
 		case ODIR:
 		case BART:
 		case FEEDBAG:
-			if err := routeFeedbag(flap, snac, buf, rw, sequence); err != nil {
+			if err := routeFeedbag(sess, fm, flap, snac, buf, rw, sequence); err != nil {
 				return err
 			}
 		case ICQ: