Skip to content

Commit 98d8f66

Browse files
committed
2 parents 382cbbd + 1d9fd6a commit 98d8f66

9 files changed

Lines changed: 325 additions & 85 deletions

File tree

bluesky/blueskyapi.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ type PostReason struct {
8080
type Embed struct {
8181
Type string `json:"$type"`
8282
Images []Image `json:"images,omitempty"`
83-
//Video `json:",omitempty"`
83+
Video `json:",omitempty"`
8484
}
8585

8686
type Image struct {
@@ -92,7 +92,7 @@ type Image struct {
9292
type Video struct {
9393
Alt string `json:"alt"`
9494
AspectRatio AspectRatio `json:"aspectRatio"`
95-
Video Blob `json:"video"`
95+
Video *Blob `json:"video"`
9696
}
9797

9898
type AspectRatio struct {

bridge/bridge.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,7 @@ type URLXMLFormat struct {
194194
End int `xml:"end,attr"`
195195
URL string `xml:"url"`
196196
ExpandedURL string `xml:"expanded_url"`
197+
DisplayURL string `xml:"display_url"`
197198
}
198199

199200
type Hashtag struct {

config.sample.yaml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@ CDN_URL: 'http://127.0.0.1:3000'
1818
# {fullblob} is the full bluesky blob
1919
# {user_did} is the image owner's DID
2020

21-
IMG_DISPLAY_TEXT: 'pic.twitter.com/{shortblob}'
21+
IMG_DISPLAY_TEXT: 'pic.twitter.com/{shortcode}'
22+
23+
# Now this link is where it actually will go to (sometimes), although this is not displayed to the user*
24+
# IMG_URL_TEXT: 'http://127.0.0.1:3000/cdn/img/bsky/{user_did}/{fullblob}.jpg' # if you don't want to do any redirects, it will eventually arrive here.
25+
IMG_URL_TEXT: 'http://127.0.0.1:3000/img/{shortcode}' # if you want to do a redirect, and have it be short, it should arrive here.
2226

2327
# Same as the above, but for videos
24-
VID_DISPLAY_TEXT: 'pic.twitter.com/{shortblob}'
28+
VID_DISPLAY_TEXT: 'pic.twitter.com/{shortcode}'
2529

2630
# SERVER_PORT is the port the server will listen on.
2731
SERVER_PORT: 3000

config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// This is not the config file. The config file can be found in the root directory as config.yaml
33
// (you may need to create it, copy config.sample.yml )
44

5+
// This file is where the config file is parsed & loaded. Modifying this is highly discouraged (unless your a developer).
6+
57
package config
68

79
import (
@@ -27,7 +29,9 @@ type Config struct {
2729
UseXForwardedFor bool `mapstructure:"USE_X_FORWARDED_FOR"`
2830

2931
ImgDisplayText string `mapstructure:"IMG_DISPLAY_TEXT"`
32+
ImgURLText string `mapstructure:"IMG_URL_TEXT"`
3033
VidDisplayText string `mapstructure:"VID_DISPLAY_TEXT"`
34+
VidURLText string `mapstructure:"VID_URL_TEXT"`
3135
}
3236

3337
// Loads our config files.

db_controller/db_controller.go

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,17 @@ type AnalyticData struct {
8787
Timestamp time.Time `gorm:"type:timestamp"`
8888
}
8989

90-
var db *gorm.DB
91-
var cfg config.Config
90+
// ShortLink represents the schema for the short_links table
91+
type ShortLink struct {
92+
ShortCode string `gorm:"type:string;primaryKey;not null"`
93+
OriginalURL string `gorm:"type:string;uniqueIndex;not null"` // Add unique index
94+
}
95+
96+
var (
97+
db *gorm.DB
98+
cfg config.Config
99+
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
100+
)
92101

93102
func InitDB(_cfg config.Config) {
94103
cfg = _cfg
@@ -122,6 +131,7 @@ func InitDB(_cfg config.Config) {
122131
db.AutoMigrate(&MessageContext{})
123132
db.AutoMigrate(&TwitterIDs{})
124133
db.AutoMigrate(&AnalyticData{})
134+
db.AutoMigrate(&ShortLink{}) // Add this line
125135
}
126136

127137
// StoreToken stores an encrypted access token and refresh token in the database.
@@ -412,3 +422,65 @@ func StoreAnalyticData(data AnalyticData) {
412422
fmt.Println("Failed to store analytic data:", result.Error)
413423
}
414424
}
425+
426+
// StoreShortLink stores a short link in the database with optimized collision handling
427+
func StoreShortLink(shortCode string, originalURL string) error {
428+
shortLink := ShortLink{
429+
ShortCode: shortCode,
430+
OriginalURL: originalURL,
431+
}
432+
433+
// Try direct insert first with DO NOTHING on conflict
434+
result := db.Clauses(clause.OnConflict{
435+
DoNothing: true,
436+
}).Create(&shortLink)
437+
438+
if result.Error != nil {
439+
return result.Error
440+
}
441+
442+
// If no rows were affected, either there was a shortCode collision or the URL exists
443+
if result.RowsAffected == 0 {
444+
// Check if URL already exists
445+
var existing ShortLink
446+
if err := db.Where("original_url = ?", originalURL).First(&existing).Error; err == nil {
447+
return nil // URL exists, reuse existing shortcode
448+
}
449+
450+
// Handle collision with simple numeric suffixes
451+
for i := 0; i < 10; i++ {
452+
newCode := fmt.Sprintf("%s%d", shortCode[:7], i)
453+
shortLink.ShortCode = newCode
454+
result = db.Clauses(clause.OnConflict{
455+
DoNothing: true,
456+
}).Create(&shortLink)
457+
458+
if result.Error == nil && result.RowsAffected > 0 {
459+
return nil
460+
}
461+
}
462+
463+
// If simple attempts failed, try timestamp-based suffix
464+
newCode := fmt.Sprintf("%s%x", shortCode[:7], time.Now().UnixNano()%16)
465+
shortLink.ShortCode = newCode
466+
result = db.Clauses(clause.OnConflict{
467+
DoNothing: true,
468+
}).Create(&shortLink)
469+
470+
if result.Error == nil && result.RowsAffected > 0 {
471+
return nil
472+
}
473+
}
474+
475+
return nil
476+
}
477+
478+
// GetOriginalURL retrieves the original URL from the database using the short code
479+
func GetOriginalURL(shortCode string) (string, error) {
480+
var shortLink ShortLink
481+
if err := db.Where("short_code = ?", shortCode).First(&shortLink).Error; err != nil {
482+
return "", err
483+
}
484+
485+
return shortLink.OriginalURL, nil
486+
}

twitterv1/cdnproxy.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,20 @@ func UserProfileImage(c *fiber.Ctx) error {
192192
//c.Redirect("https://cdn.bsky.app/img/" + screen_name + ":profile_bigger")
193193
return c.Redirect(userinfo.ProfileImageURL)
194194
}
195+
196+
// This is here because it doesn't just want a direct link to the m3u8 file.
197+
// So we make an extremely basic site that just includes the video, and maybe the alt text if i care enough
198+
func CDNVideoProxy(c *fiber.Ctx) error {
199+
video_url := "https://video.bsky.app/watch/" + c.Params("did") + "/" + c.Params("link") + "/720p/video.m3u8" // 720p on an iphone 2g oh god
200+
thumbnail_url := "https://video.cdn.bsky.app/hls/" + c.Params("did") + "/" + c.Params("link") + "/thumbnail.jpg"
201+
202+
c.Context().SetContentType("text/html")
203+
204+
// Is the below minified? yup!
205+
// tbh, you could pretty easily just run it thru a prettier and it would make sense, but i'll explain it here:
206+
// It's a basic html page that includes the hls.js library, a video element, and a script that checks if the browser supports hls, and if it doesn't, it uses hls.js to play the video
207+
// why u hef to be mad golang warning thingy?
208+
return c.SendString(fmt.Sprintf(`
209+
<meta content="width=device-width,initial-scale=1"name=viewport><title>Bluesky Video</title><style>*{margin:0;padding:0;width:100%%;height:100%%}</style><body><script src=https://cdn.jsdelivr.net/npm/hls.js@1></script><video autoplay="autoplay" controls id=v poster=%s src=%s></video><script>var v=document.getElementById("v");if(v.canPlayType("application/vnd.apple.mpegurl"));else if(Hls.isSupported()){var h=new Hls;h.loadSource(v.src),h.attachMedia(v)}</script>
210+
`, thumbnail_url, video_url))
211+
}

twitterv1/imagelink.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package twitterv1
2+
3+
import (
4+
"crypto/sha256"
5+
"math/big"
6+
"sync"
7+
8+
"github.com/Preloading/TwitterAPIBridge/db_controller"
9+
"github.com/gofiber/fiber/v2"
10+
)
11+
12+
// Simple cache for frequently accessed URLs
13+
type urlCache struct {
14+
cache map[string]string
15+
mutex sync.RWMutex
16+
}
17+
18+
var (
19+
base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
20+
cache = &urlCache{
21+
cache: make(map[string]string),
22+
}
23+
)
24+
25+
// Get returns the cached shortcode for a URL
26+
func (c *urlCache) Get(url string) (string, bool) {
27+
c.mutex.RLock()
28+
defer c.mutex.RUnlock()
29+
code, ok := c.cache[url]
30+
return code, ok
31+
}
32+
33+
// Set caches a URL and its shortcode
34+
func (c *urlCache) Set(url, code string) {
35+
c.mutex.Lock()
36+
defer c.mutex.Unlock()
37+
c.cache[url] = code
38+
}
39+
40+
func toBase62(num *big.Int) string {
41+
base := big.NewInt(62)
42+
zero := big.NewInt(0)
43+
result := make([]byte, 7) // Changed to 7 to leave room for collision handling
44+
idx := 6
45+
46+
temp := new(big.Int).Set(num)
47+
for idx >= 0 {
48+
mod := new(big.Int)
49+
temp.DivMod(temp, base, mod)
50+
result[idx] = base62Chars[mod.Int64()]
51+
idx--
52+
if temp.Cmp(zero) == 0 {
53+
break
54+
}
55+
}
56+
// Pad with '0' if necessary
57+
for i := idx; i >= 0; i-- {
58+
result[i] = base62Chars[0]
59+
}
60+
return string(result)
61+
}
62+
63+
func CreateShortLink(originalPath string) (string, error) {
64+
// Check cache first
65+
if code, ok := cache.Get(originalPath); ok {
66+
return code, nil
67+
}
68+
69+
// Generate SHA-256 hash of the URL
70+
hash := sha256.New()
71+
hash.Write([]byte(originalPath))
72+
hashBytes := hash.Sum(nil)
73+
74+
// Convert first 8 bytes of hash to a big.Int
75+
num := new(big.Int).SetBytes(hashBytes[:8])
76+
shortCode := toBase62(num)
77+
78+
err := db_controller.StoreShortLink(shortCode, originalPath)
79+
if err != nil {
80+
return "", err
81+
}
82+
83+
// Cache the result
84+
cache.Set(originalPath, shortCode)
85+
return shortCode, nil
86+
}
87+
88+
func RedirectToLink(c *fiber.Ctx) error {
89+
90+
shortCode := c.Params("ref")
91+
originalURL, err := db_controller.GetOriginalURL(shortCode)
92+
if err != nil {
93+
return c.Status(fiber.StatusNotFound).SendString("Could not find image!")
94+
}
95+
96+
return c.Redirect(configData.CdnURL+originalURL, fiber.StatusMovedPermanently)
97+
}

0 commit comments

Comments
 (0)