feat(rubick): rename project and add production release bundle

This commit is contained in:
Muhammad Firas 2026-03-06 03:12:28 +07:00
commit fab6cf26eb
No known key found for this signature in database
33 changed files with 11554 additions and 0 deletions

324
msn/bing_client.go Normal file
View file

@ -0,0 +1,324 @@
package msn
import (
"encoding/json"
"fmt"
"github.com/enetx/g"
"github.com/enetx/surf"
)
// BingClient is the client for Bing Finance APIs (ownership data)
type BingClient struct {
client *surf.Client
}
// NewBingClient creates a new Bing API client with Chrome impersonation
func NewBingClient() *BingClient {
client := surf.NewClient().
Builder().
Impersonate().
Chrome().
Build().
Unwrap()
return &BingClient{client: client}
}
// Close closes idle connections
func (c *BingClient) Close() {
c.client.CloseIdleConnections()
}
// commonHeaders returns common headers for Bing API requests
func (c *BingClient) commonHeaders() map[string]string {
return map[string]string{
"Accept": "application/json",
"Accept-Language": "en-US,en;q=0.9",
"Origin": "https://www.msn.com",
"Referer": "https://www.msn.com/",
}
}
// GetTopShareHolders fetches top institutional shareholders
func (c *BingClient) GetTopShareHolders(id string, count int) ([]Holder, error) {
if id == "" {
return nil, fmt.Errorf("no stock ID provided")
}
if count <= 0 {
count = 50
}
apiURL := fmt.Sprintf("%sGetSecurityTopShareHolders/%s?rangeStart=1&count=%d",
BingAPIBaseURL,
id,
count,
)
req := c.client.Get(g.String(apiURL))
for k, v := range c.commonHeaders() {
req = req.SetHeaders(k, v)
}
resp := req.Do()
if resp.IsErr() {
return nil, fmt.Errorf("top shareholders request failed: %w", resp.Err())
}
r := resp.Ok()
if r.StatusCode != 200 {
return nil, fmt.Errorf("top shareholders API returned status %d", r.StatusCode)
}
body := r.Body.String().Ok().Std()
var result OwnershipResponse
if err := json.Unmarshal([]byte(body), &result); err != nil {
return nil, fmt.Errorf("failed to parse top shareholders response: %w", err)
}
// Return whichever field has data
if len(result.SecurityOwnerships) > 0 {
return result.SecurityOwnerships, nil
}
return result.Records, nil
}
// GetTopBuyers fetches recent top buyers
func (c *BingClient) GetTopBuyers(id string, count int) ([]Holder, error) {
if id == "" {
return nil, fmt.Errorf("no stock ID provided")
}
if count <= 0 {
count = 50
}
apiURL := fmt.Sprintf("%sGetSecurityTopBuyers/%s?rangeStart=1&count=%d",
BingAPIBaseURL,
id,
count,
)
req := c.client.Get(g.String(apiURL))
for k, v := range c.commonHeaders() {
req = req.SetHeaders(k, v)
}
resp := req.Do()
if resp.IsErr() {
return nil, fmt.Errorf("top buyers request failed: %w", resp.Err())
}
r := resp.Ok()
if r.StatusCode != 200 {
return nil, fmt.Errorf("top buyers API returned status %d", r.StatusCode)
}
body := r.Body.String().Ok().Std()
var result OwnershipResponse
if err := json.Unmarshal([]byte(body), &result); err != nil {
return nil, fmt.Errorf("failed to parse top buyers response: %w", err)
}
if len(result.SecurityOwnerships) > 0 {
return result.SecurityOwnerships, nil
}
return result.Records, nil
}
// GetTopSellers fetches recent top sellers
func (c *BingClient) GetTopSellers(id string, count int) ([]Holder, error) {
if id == "" {
return nil, fmt.Errorf("no stock ID provided")
}
if count <= 0 {
count = 50
}
apiURL := fmt.Sprintf("%sGetSecurityTopSellers/%s?rangeStart=1&count=%d",
BingAPIBaseURL,
id,
count,
)
req := c.client.Get(g.String(apiURL))
for k, v := range c.commonHeaders() {
req = req.SetHeaders(k, v)
}
resp := req.Do()
if resp.IsErr() {
return nil, fmt.Errorf("top sellers request failed: %w", resp.Err())
}
r := resp.Ok()
if r.StatusCode != 200 {
return nil, fmt.Errorf("top sellers API returned status %d", r.StatusCode)
}
body := r.Body.String().Ok().Std()
var result OwnershipResponse
if err := json.Unmarshal([]byte(body), &result); err != nil {
return nil, fmt.Errorf("failed to parse top sellers response: %w", err)
}
if len(result.SecurityOwnerships) > 0 {
return result.SecurityOwnerships, nil
}
return result.Records, nil
}
// GetNewShareHolders fetches new institutional holders
func (c *BingClient) GetNewShareHolders(id string, count int) ([]Holder, error) {
if id == "" {
return nil, fmt.Errorf("no stock ID provided")
}
if count <= 0 {
count = 50
}
apiURL := fmt.Sprintf("%sGetSecurityTopNewShareHolders/%s?rangeStart=1&count=%d",
BingAPIBaseURL,
id,
count,
)
req := c.client.Get(g.String(apiURL))
for k, v := range c.commonHeaders() {
req = req.SetHeaders(k, v)
}
resp := req.Do()
if resp.IsErr() {
return nil, fmt.Errorf("new shareholders request failed: %w", resp.Err())
}
r := resp.Ok()
if r.StatusCode != 200 {
return nil, fmt.Errorf("new shareholders API returned status %d", r.StatusCode)
}
body := r.Body.String().Ok().Std()
var result OwnershipResponse
if err := json.Unmarshal([]byte(body), &result); err != nil {
return nil, fmt.Errorf("failed to parse new shareholders response: %w", err)
}
if len(result.SecurityOwnerships) > 0 {
return result.SecurityOwnerships, nil
}
return result.Records, nil
}
// GetExitedShareHolders fetches exited institutional holders
func (c *BingClient) GetExitedShareHolders(id string, count int) ([]Holder, error) {
if id == "" {
return nil, fmt.Errorf("no stock ID provided")
}
if count <= 0 {
count = 50
}
apiURL := fmt.Sprintf("%sGetSecurityTopExitedShareHolders/%s?rangeStart=1&count=%d",
BingAPIBaseURL,
id,
count,
)
req := c.client.Get(g.String(apiURL))
for k, v := range c.commonHeaders() {
req = req.SetHeaders(k, v)
}
resp := req.Do()
if resp.IsErr() {
return nil, fmt.Errorf("exited shareholders request failed: %w", resp.Err())
}
r := resp.Ok()
if r.StatusCode != 200 {
return nil, fmt.Errorf("exited shareholders API returned status %d", r.StatusCode)
}
body := r.Body.String().Ok().Std()
var result OwnershipResponse
if err := json.Unmarshal([]byte(body), &result); err != nil {
return nil, fmt.Errorf("failed to parse exited shareholders response: %w", err)
}
if len(result.SecurityOwnerships) > 0 {
return result.SecurityOwnerships, nil
}
return result.Records, nil
}
// IsInvestorDataAvailable checks if investor data exists for a stock
func (c *BingClient) IsInvestorDataAvailable(id string) (bool, error) {
if id == "" {
return false, fmt.Errorf("no stock ID provided")
}
apiURL := fmt.Sprintf("%sIsInvestorDataAvailable/%s",
BingAPIBaseURL,
id,
)
req := c.client.Get(g.String(apiURL))
for k, v := range c.commonHeaders() {
req = req.SetHeaders(k, v)
}
resp := req.Do()
if resp.IsErr() {
return false, fmt.Errorf("investor data check request failed: %w", resp.Err())
}
r := resp.Ok()
if r.StatusCode != 200 {
return false, fmt.Errorf("investor data check API returned status %d", r.StatusCode)
}
body := r.Body.String().Ok().Std()
var available bool
if err := json.Unmarshal([]byte(body), &available); err != nil {
return false, fmt.Errorf("failed to parse investor data check response: %w", err)
}
return available, nil
}
// GetAllOwnership fetches all ownership data for a stock
func (c *BingClient) GetAllOwnership(id string, count int) (*OwnershipData, error) {
ownership := &OwnershipData{}
// Skip IsInvestorDataAvailable check as it often returns 404 even when data exists
// Just try to fetch the data directly
// Fetch all ownership data sequentially
if holders, err := c.GetTopShareHolders(id, count); err == nil {
ownership.TopHolders = holders
}
if buyers, err := c.GetTopBuyers(id, count); err == nil {
ownership.TopBuyers = buyers
}
if sellers, err := c.GetTopSellers(id, count); err == nil {
ownership.TopSellers = sellers
}
if newHolders, err := c.GetNewShareHolders(id, count); err == nil {
ownership.NewHolders = newHolders
}
if exited, err := c.GetExitedShareHolders(id, count); err == nil {
ownership.ExitedHolders = exited
}
return ownership, nil
}