├── .github
├── assets
│ ├── media_only.jpg
│ ├── no_caption.jpg
│ ├── orig_embed.jpg
│ ├── Step1_image.jpg
│ ├── Step2_image.jpg
│ ├── Step4_image_a.jpg
│ └── Step4_image_b.jpg
└── workflows
│ └── build.yml
├── handlers
├── scraper
│ ├── dictionary.bin
│ ├── cache.go
│ └── data.go
├── oembed.go
├── images.go
├── videos.go
├── embed.go
└── grid.go
├── scripts
├── build.sh
├── docker-compose.yml
└── k8s
│ ├── instafix-deployment.yaml
│ └── instafix-ingress.yaml
├── utils
├── strconv.go
├── crawlerdetect.go
├── strings.go
└── jsonesc.go
├── views
├── model
│ └── model.go
├── oembed.manual.go
├── embed.jade
├── home.jade
├── home.jade.go
├── jade.go
└── embed.jade.go
├── .gitignore
├── go.mod
├── Caddyfile
├── LICENSE
├── Dockerfile
├── README.md
├── main.go
└── go.sum
/.github/assets/media_only.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/.github/assets/media_only.jpg
--------------------------------------------------------------------------------
/.github/assets/no_caption.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/.github/assets/no_caption.jpg
--------------------------------------------------------------------------------
/.github/assets/orig_embed.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/.github/assets/orig_embed.jpg
--------------------------------------------------------------------------------
/.github/assets/Step1_image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/.github/assets/Step1_image.jpg
--------------------------------------------------------------------------------
/.github/assets/Step2_image.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/.github/assets/Step2_image.jpg
--------------------------------------------------------------------------------
/.github/assets/Step4_image_a.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/.github/assets/Step4_image_a.jpg
--------------------------------------------------------------------------------
/.github/assets/Step4_image_b.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/.github/assets/Step4_image_b.jpg
--------------------------------------------------------------------------------
/handlers/scraper/dictionary.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Wikidepia/InstaFix/HEAD/handlers/scraper/dictionary.bin
--------------------------------------------------------------------------------
/scripts/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | # Optional: If not using GHCR, let it build locally
4 | #docker build -t instafix ../src/
5 |
6 | # Refresh existing containers
7 | docker-compose down
8 | docker-compose up -d
--------------------------------------------------------------------------------
/utils/strconv.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "unsafe"
4 |
5 | func S2B(s string) []byte {
6 | return unsafe.Slice(unsafe.StringData(s), len(s))
7 | }
8 |
9 | func B2S(b []byte) string {
10 | return *(*string)(unsafe.Pointer(&b))
11 | }
12 |
--------------------------------------------------------------------------------
/views/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type ViewsData struct {
4 | Card string
5 | Title string `default:"InstaFix"`
6 | ImageURL string `default:""`
7 | VideoURL string `default:""`
8 | URL string
9 | Description string
10 | OEmbedURL string
11 | Width int `default:"400"`
12 | Height int `default:"400"`
13 | }
14 |
15 | type OEmbedData struct {
16 | Text string
17 | URL string
18 | }
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Allowlisting gitignore template for GO projects prevents us
2 | # from adding various unwanted local files, such as generated
3 | # files, developer configurations or IDE-specific files etc.
4 | #
5 | # Recommended: Go.AllowList.gitignore
6 |
7 | # Ignore everything
8 | *
9 |
10 | # But not these files...
11 | !/.gitignore
12 |
13 | !*.go
14 | !go.sum
15 | !go.mod
16 |
17 | !README.md
18 | !LICENSE
19 |
20 | # !Makefile
21 |
22 | # ...even if they are in subdirectories
23 | !*/
24 |
--------------------------------------------------------------------------------
/scripts/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | instafix:
5 | image: ghcr.io/wikidepia/instafix:main
6 | restart: unless-stopped
7 | expose:
8 | - "3000:3000" # Use HOST:3000 for direct access if Traefik is not setup
9 | labels:
10 | - traefik.enable=true
11 | - traefik.http.routers.instafix.entryPoints=web-secure
12 | - traefik.http.routers.instafix.rule=Host(`ddinstagram.com`)
13 | - traefik.http.routers.instafix.tls=true
14 | - traefik.http.services.instafix.loadbalancer.server.port=3000
--------------------------------------------------------------------------------
/scripts/k8s/instafix-deployment.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: instafix
5 | spec:
6 | replicas: 10
7 | strategy:
8 | type: Recreate
9 | selector:
10 | matchLabels:
11 | app: instafix
12 |
13 | template:
14 | metadata:
15 | labels:
16 | app: instafix
17 | spec:
18 | containers:
19 | - image: ghcr.io/wikidepia/instafix:main
20 | name: instafix
21 | ports:
22 | - containerPort: 3000
23 | protocol: TCP
24 | restartPolicy: Always
25 |
--------------------------------------------------------------------------------
/handlers/oembed.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "instafix/utils"
5 | "instafix/views"
6 | "instafix/views/model"
7 | "net/http"
8 | )
9 |
10 | func OEmbed(w http.ResponseWriter, r *http.Request) {
11 | urlQuery := r.URL.Query()
12 | if urlQuery == nil {
13 | return
14 | }
15 | headingText := urlQuery.Get("text")
16 | headingURL := urlQuery.Get("url")
17 | if headingText == "" || headingURL == "" {
18 | return
19 | }
20 | w.Header().Set("Content-Type", "application/json")
21 |
22 | // Totally safe 100% valid template 👍
23 | OEmbedData := &model.OEmbedData{
24 | Text: utils.EscapeJSONString(headingText),
25 | URL: headingURL,
26 | }
27 |
28 | views.OEmbed(OEmbedData, w)
29 | }
30 |
--------------------------------------------------------------------------------
/views/oembed.manual.go:
--------------------------------------------------------------------------------
1 | // Code generated by "Myself"; PLEASE EDIT.
2 |
3 | package views
4 |
5 | import (
6 | "instafix/views/model"
7 | "io"
8 | )
9 |
10 | const (
11 | oembed__0 = `{"author_name": `
12 | oembed__1 = `,"author_url": "`
13 | oembed__2 = `","provider_name": "InstaFix","provider_url": "https://github.com/Wikidepia/InstaFix","title": "Instagram","type": "link","version": "1.0"}`
14 | )
15 |
16 | func OEmbed(o *model.OEmbedData, wr io.Writer) {
17 | buffer := &WriterAsBuffer{wr}
18 |
19 | buffer.WriteString(oembed__0)
20 | WriteAll(o.Text, false, buffer) // Escape in handlers
21 | buffer.WriteString(oembed__1)
22 | WriteAll(o.URL, false, buffer) // Escape in handlers
23 | buffer.WriteString(oembed__2)
24 |
25 | }
26 |
--------------------------------------------------------------------------------
/handlers/images.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | scraper "instafix/handlers/scraper"
5 | "net/http"
6 | "strconv"
7 |
8 | "github.com/go-chi/chi/v5"
9 | )
10 |
11 | func Images(w http.ResponseWriter, r *http.Request) {
12 | postID := chi.URLParam(r, "postID")
13 | mediaNum, err := strconv.Atoi(chi.URLParam(r, "mediaNum"))
14 | if err != nil {
15 | http.Error(w, err.Error(), http.StatusInternalServerError)
16 | return
17 | }
18 |
19 | item, err := scraper.GetData(postID)
20 | if err != nil {
21 | http.Error(w, err.Error(), http.StatusInternalServerError)
22 | return
23 | }
24 |
25 | // Redirect to image URL
26 | if mediaNum > len(item.Medias) {
27 | return
28 | }
29 | imageURL := item.Medias[max(1, mediaNum)-1].URL
30 | http.Redirect(w, r, imageURL, http.StatusFound)
31 | }
32 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module instafix
2 |
3 | go 1.23.2
4 |
5 | require (
6 | github.com/PuerkitoBio/goquery v1.10.0
7 | github.com/RyanCarrier/dijkstra/v2 v2.0.2
8 | github.com/cespare/xxhash/v2 v2.3.0
9 | github.com/elastic/go-freelru v0.16.0
10 | github.com/go-chi/chi/v5 v5.1.0
11 | github.com/kelindar/binary v1.0.19
12 | github.com/klauspost/compress v1.17.11
13 | github.com/tdewolff/parse/v2 v2.7.19
14 | github.com/tidwall/gjson v1.18.0
15 | go.etcd.io/bbolt v1.3.11
16 | golang.org/x/image v0.22.0
17 | golang.org/x/net v0.31.0
18 | golang.org/x/sync v0.9.0
19 | )
20 |
21 | require (
22 | github.com/andybalholm/cascadia v1.3.2 // indirect
23 | github.com/stretchr/testify v1.9.0 // indirect
24 | github.com/tidwall/match v1.1.1 // indirect
25 | github.com/tidwall/pretty v1.2.1 // indirect
26 | golang.org/x/sys v0.27.0 // indirect
27 | )
28 |
--------------------------------------------------------------------------------
/utils/crawlerdetect.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "strings"
5 | )
6 |
7 | var knownBots = []string{
8 | "bot",
9 | "facebook",
10 | "embed",
11 | "got",
12 | "firefox/92",
13 | "firefox/38",
14 | "curl",
15 | "wget",
16 | "go-http",
17 | "yahoo",
18 | "generator",
19 | "whatsapp",
20 | "preview",
21 | "link",
22 | "proxy",
23 | "vkshare",
24 | "images",
25 | "analyzer",
26 | "index",
27 | "crawl",
28 | "spider",
29 | "python",
30 | "cfnetwork",
31 | "node",
32 | "mastodon",
33 | "http.rb",
34 | "discord",
35 | "ruby",
36 | "bun/",
37 | "fiddler",
38 | "revoltchat",
39 | }
40 |
41 | func IsBot(userAgent string) bool {
42 | userAgent = strings.ToLower(userAgent)
43 | for _, bot := range knownBots {
44 | if strings.Contains(userAgent, bot) {
45 | return true
46 | }
47 | }
48 | return false
49 | }
50 |
--------------------------------------------------------------------------------
/scripts/k8s/instafix-ingress.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: traefik.containo.us/v1alpha1
2 | kind: IngressRoute
3 | metadata:
4 | name: instafix
5 | spec:
6 | entryPoints:
7 | - websecure
8 | routes:
9 | - match: Host(`ddinstagram.com`)
10 | kind: Rule
11 | services:
12 | - name: instafix
13 | port: 3000
14 | tls:
15 | secretName: ddinstagram-com
16 |
17 | ---
18 | apiVersion: cert-manager.io/v1
19 | kind: Certificate
20 | metadata:
21 | name: ddinstagram-com
22 | spec:
23 | secretName: ddinstagram-com
24 | commonName: ddinstagram.com
25 | dnsNames:
26 | - ddinstagram.com
27 | issuerRef:
28 | name: letsencrypt-prod
29 | kind: ClusterIssuer
30 |
31 | ---
32 | apiVersion: v1
33 | kind: Service
34 | metadata:
35 | name: instafix
36 | labels:
37 | app: instafix
38 | spec:
39 | selector:
40 | app: instafix
41 | ports:
42 | - port: 3000
--------------------------------------------------------------------------------
/Caddyfile:
--------------------------------------------------------------------------------
1 | # The Caddyfile is an easy way to configure your Caddy web server.
2 | #
3 | # Unless the file starts with a global options block, the first
4 | # uncommented line is always the address of your site.
5 | #
6 | # To use your own domain name (with automatic HTTPS), first make
7 | # sure your domain's A/AAAA DNS records are properly pointed to
8 | # this machine's public IP, then replace ":80" below with your
9 | # domain name.
10 |
11 | domain.com, www.domain.com {
12 | # Another common task is to set up a reverse proxy:
13 | reverse_proxy localhost:3000
14 | }
15 |
16 | d.domain.com {
17 | reverse_proxy localhost:3000
18 | rewrite * ?direct=true
19 | }
20 |
21 | g.domain.com {
22 | reverse_proxy localhost:3000
23 | rewrite * ?gallery=true
24 | }
25 |
26 | # Refer to the Caddy docs for more information:
27 | # https://caddyserver.com/docs/caddyfile
28 |
--------------------------------------------------------------------------------
/handlers/videos.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | scraper "instafix/handlers/scraper"
5 | "net/http"
6 | "strconv"
7 | "strings"
8 |
9 | "github.com/go-chi/chi/v5"
10 | )
11 |
12 | var VideoProxyAddr string
13 |
14 | func Videos(w http.ResponseWriter, r *http.Request) {
15 | postID := chi.URLParam(r, "postID")
16 | mediaNum, err := strconv.Atoi(chi.URLParam(r, "mediaNum"))
17 | if err != nil {
18 | http.Error(w, err.Error(), http.StatusInternalServerError)
19 | return
20 | }
21 |
22 | item, err := scraper.GetData(postID)
23 | if err != nil {
24 | http.Error(w, err.Error(), http.StatusInternalServerError)
25 | return
26 | }
27 |
28 | // Redirect to image URL
29 | if mediaNum > len(item.Medias) {
30 | return
31 | }
32 | videoURL := item.Medias[max(1, mediaNum)-1].URL
33 |
34 | // Redirect to proxy if not TelegramBot in User-Agent
35 | if strings.Contains(r.Header.Get("User-Agent"), "TelegramBot") {
36 | http.Redirect(w, r, videoURL, http.StatusFound)
37 | return
38 | }
39 | http.Redirect(w, r, VideoProxyAddr+videoURL, http.StatusFound)
40 | }
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/handlers/scraper/cache.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "os"
5 |
6 | "github.com/cespare/xxhash/v2"
7 | "github.com/elastic/go-freelru"
8 | bolt "go.etcd.io/bbolt"
9 | )
10 |
11 | var DB *bolt.DB
12 | var LRU *freelru.SyncedLRU[string, bool]
13 |
14 | func hashStringXXHASH(s string) uint32 {
15 | return uint32(xxhash.Sum64String(s))
16 | }
17 |
18 | func InitDB() {
19 | db, err := bolt.Open("cache.db", 0600, nil)
20 | if err != nil {
21 | panic(err)
22 | }
23 |
24 | // Create buckets
25 | err = db.Update(func(tx *bolt.Tx) error {
26 | tx.CreateBucketIfNotExists([]byte("data"))
27 | tx.CreateBucketIfNotExists([]byte("ttl"))
28 | return nil
29 | })
30 | if err != nil {
31 | panic(err)
32 | }
33 |
34 | DB = db
35 | }
36 |
37 | func InitLRU(maxEntries int) {
38 | // Initialize LRU for grid caching
39 | lru, err := freelru.NewSynced[string, bool](uint32(maxEntries), hashStringXXHASH)
40 | if err != nil {
41 | panic(err)
42 | }
43 |
44 | lru.SetOnEvict(func(key string, value bool) {
45 | os.Remove(key)
46 | })
47 |
48 | // Fill LRU with existing files
49 | dir, err := os.ReadDir("static")
50 | if err != nil {
51 | panic(err)
52 | }
53 | for _, d := range dir {
54 | if !d.IsDir() {
55 | lru.Add("static/"+d.Name(), true)
56 | }
57 | }
58 |
59 | LRU = lru
60 | }
61 |
--------------------------------------------------------------------------------
/utils/strings.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import "unicode/utf8"
4 |
5 | // Substr returns a substring of a given string, starting at the specified index
6 | // and with a specified length.
7 | // It handles UTF-8 encoded strings.
8 | // Taken from https://github.com/goravel/framework/
9 | func Substr(str string, start int, length ...int) string {
10 | // Convert the string to a rune slice for proper handling of UTF-8 encoding.
11 | runes := []rune(str)
12 | strLen := utf8.RuneCountInString(str)
13 | end := strLen
14 | // Check if the start index is out of bounds.
15 | if start >= strLen {
16 | return ""
17 | }
18 |
19 | // If the start index is negative, count backwards from the end of the string.
20 | if start < 0 {
21 | start = strLen + start
22 | if start < 0 {
23 | start = 0
24 | }
25 | }
26 |
27 | if len(length) > 0 {
28 | if length[0] >= 0 {
29 | end = start + length[0]
30 | } else {
31 | end = strLen + length[0]
32 | }
33 | }
34 |
35 | // If the length is 0, return the substring from start to the end of the string.
36 | if len(length) == 0 {
37 | return string(runes[start:])
38 | }
39 |
40 | // Handle the case where lenArg is negative and less than start
41 | if end < start {
42 | return ""
43 | }
44 |
45 | if end > strLen {
46 | end = strLen
47 | }
48 |
49 | // Return the substring.
50 | return string(runes[start:end])
51 | }
52 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | FROM --platform=$BUILDPLATFORM golang:1.23 as app-builder
4 |
5 | # Set destination for COPY
6 | WORKDIR /app
7 |
8 | # Download Go modules
9 | COPY go.mod go.sum ./
10 | RUN go mod download
11 |
12 | # Copy the source code. Note the slash at the end, as explained in
13 | # https://docs.docker.com/engine/reference/builder/#copy
14 | COPY *.go ./
15 | COPY handlers/ ./handlers/
16 | COPY handlers/scraper/ ./handlers/scraper/
17 | # NO-OP in case handlers/scraper/ was already copied previously
18 | RUN true
19 | COPY utils/ ./utils/
20 | COPY views/ ./views/
21 |
22 | # This is the architecture you’re building for, which is passed in by the builder.
23 | # Placing it here allows the previous steps to be cached across architectures.
24 | ARG TARGETARCH
25 |
26 | # Build
27 | RUN GOOS=linux GOARCH=$TARGETARCH go build -tags netgo,osusergo -ldflags '-extldflags "-static"'
28 |
29 | # Run in scratch container
30 | FROM scratch
31 | # the test program:
32 | COPY --from=app-builder /app/instafix /instafix
33 | # the tls certificates:
34 | # NB: this pulls directly from the upstream image, which already has ca-certificates:
35 | COPY --from=alpine:latest /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
36 |
37 | # Optional:
38 | # To bind to a TCP port, runtime parameters must be supplied to the docker command.
39 | # But we can document in the Dockerfile what ports
40 | # the application is going to listen on by default.
41 | # https://docs.docker.com/engine/reference/builder/#expose
42 | EXPOSE 3000
43 |
44 | # Run the app
45 | ENTRYPOINT ["/instafix"]
46 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Wikidepia/InstaFix
2 | on:
3 | push:
4 | workflow_dispatch:
5 | env:
6 | REGISTRY: ghcr.io
7 | IMAGE_NAME: ${{ github.repository }}
8 | jobs:
9 | build-and-push-image:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: read
13 | packages: write
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
17 |
18 | - name: Setup QEMU
19 | uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf # v3.2.0
20 | with:
21 | platforms: arm, arm64, amd64
22 |
23 | - name: Setup BuildX
24 | uses: docker/setup-buildx-action@aa33708b10e362ff993539393ff100fa93ed6a27 # v3.5.0
25 |
26 | - name: Log in to the Container registry
27 | uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 # v3.3.0
28 | with:
29 | registry: ${{ env.REGISTRY }}
30 | username: ${{ github.actor }}
31 | password: ${{ secrets.GITHUB_TOKEN }}
32 |
33 | - name: Extract metadata (tags, labels) for Docker
34 | id: meta
35 | uses: docker/metadata-action@8e5442c4ef9f78752691e2d8f8d19755c6f78e81 # v5.5.1
36 | with:
37 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
38 |
39 | - name: Build and push Docker image
40 | uses: docker/build-push-action@5176d81f87c23d6fc96624dfdbcd9f3830bbe445 # v6.5.0
41 | with:
42 | context: .
43 | platforms: linux/arm, linux/arm64, linux/amd64
44 | push: true
45 | tags: ${{ steps.meta.outputs.tags }}
46 | labels: ${{ steps.meta.outputs.labels }}
47 |
--------------------------------------------------------------------------------
/views/embed.jade:
--------------------------------------------------------------------------------
1 | :go:func Embed(v *model.ViewsData)
2 |
3 | :go:import "instafix/views/model"
4 |
5 | mixin for(golang)
6 | #cmd Precompile jade templates to #{golang} code.
7 |
8 | doctype html
9 | html(lang='en')
10 | head
11 | meta(charset="utf-8")
12 | meta(name='theme-color', content='#CE0071')
13 |
14 | if (v.Card != "")
15 | meta(name='twitter:card', content=v.Card)
16 | if (v.Title != "")
17 | meta(name='twitter:title', content=v.Title)
18 | if (v.ImageURL != "")
19 | meta(name='twitter:image', content=v.ImageURL)
20 | if (v.VideoURL != "")
21 | meta(name='twitter:player:width', content=v.Width)
22 | meta(name='twitter:player:height', content=v.Height)
23 | meta(name='twitter:player:stream', content=v.VideoURL)
24 | meta(name='twitter:player:stream:content_type', content='video/mp4')
25 |
26 | if (v.VideoURL != "" || v.ImageURL != "")
27 | meta(property='og:site_name', content='InstaFix')
28 |
29 | meta(property='og:url', content=v.URL)
30 | meta(property='og:description', content=v.Description)
31 |
32 | if (v.ImageURL != "")
33 | meta(property='og:image', content=v.ImageURL)
34 |
35 | if (v.VideoURL != "")
36 | meta(property='og:video', content=v.VideoURL)
37 | meta(property='og:video:secure_url', content=v.VideoURL)
38 | meta(property='og:video:type', content='video/mp4')
39 | meta(property='og:video:width', content=v.Width)
40 | meta(property='og:video:height', content=v.Height)
41 |
42 | if (v.OEmbedURL != "")
43 | link(rel='alternate', href!=v.OEmbedURL, type='application/json+oembed', title=v.Title)
44 |
45 | meta(http-equiv='refresh', content=`0; url = ${v.URL}`)
46 |
47 | body
48 | | Redirecting you to the post in a moment.
49 | a(href=v.URL) Or click here.
50 |
--------------------------------------------------------------------------------
/views/home.jade:
--------------------------------------------------------------------------------
1 | :go:func Home()
2 |
3 | mixin for(golang)
4 | #cmd Precompile jade templates to #{golang} code.
5 |
6 | doctype html
7 | html(lang="en")
8 |
9 | head
10 | meta(charset="utf-8")
11 | meta(name="viewport", content="width=device-width, initial-scale=1")
12 | meta(property="og:title", content="InstaFix")
13 | meta(property="og:site_name", content="InstaFix")
14 | meta(property="og:description", content="Fix Instagram embeds in Discord (and Telegram!)")
15 | title InstaFix
16 | link(rel="icon", href="data:image/svg+xml,")
17 | link(rel="stylesheet", href="https://cdn.jsdelivr.net/npm/@picocss/pico@1.5.13/css/pico.min.css")
18 |
19 | body
20 | main.container(style="max-width: 35rem")
21 | hgroup
22 | h1 InstaFix
23 | h2 Fix Instagram embeds in Discord (and Telegram!)
24 | p InstaFix serves fixed Instagram image and video embeds. Heavily inspired by fxtwitter.com.
25 |
26 | section
27 | header
28 | h3(style="margin-bottom: 4px") How to Use
29 | ul
30 | li Add dd before instagram.com to fix embeds, or
31 | li To get direct media embed, add `d.dd` before `instagram.com`.
32 | video(src="https://user-images.githubusercontent.com/72781956/168544556-31009b0e-62e8-4d4c-909b-434ad146e118.mp4", controls="controls", muted="muted", style="width: 100%; max-height: 100%")
33 | | Your browser does not support the video tag.
34 | hr
35 | small: a(href="https://github.com/Wikidepia/InstaFix", target="_blank") Source code available in GitHub!
36 | br
37 | small • Instagram is a trademark of Instagram, Inc. This app is not affiliated with Instagram, Inc.
38 |
--------------------------------------------------------------------------------
/views/home.jade.go:
--------------------------------------------------------------------------------
1 | // Code generated by "jade.go"; DO NOT EDIT.
2 |
3 | package views
4 |
5 | import (
6 | "io"
7 | )
8 |
9 | const (
10 | home__0 = `
InstaFix
InstaFix
Fix Instagram embeds in Discord (and Telegram!)
InstaFix serves fixed Instagram image and video embeds. Heavily inspired by fxtwitter.com.
How to Use
Add dd before instagram.com to fix embeds, or
`
11 | home__1 = `
Source code available in GitHub! • Instagram is a trademark of Instagram, Inc. This app is not affiliated with Instagram, Inc.`
12 | )
13 |
14 | func Home(wr io.Writer) {
15 | buffer := &WriterAsBuffer{wr}
16 |
17 | buffer.WriteString(home__0)
18 |
19 | buffer.WriteString(`To get direct media embed, add ` + "`" + `d.dd` + "`" + ` before ` + "`" + `instagram.com` + "`" + `.`)
20 | buffer.WriteString(home__1)
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # InstaFix
2 |
3 | > Instagram is a trademark of Instagram, Inc. This app is not affiliated with Instagram, Inc.
4 |
5 | InstaFix serves fixed Instagram image and video embeds. Heavily inspired by [fxtwitter.com](https://fxtwitter.com).
6 |
7 | ## How to use
8 |
9 | Add `dd` before `instagram.com` to show Instagram embeds, or
10 |
11 |
12 |
13 | ### Embed Media Only
14 |
15 | Add `d.dd` before `instagram.com` to show only the media.
16 |
17 |
18 |
19 | ### Gallery View
20 |
21 | Add `g.dd` before `instagram.com` to show only the author and the media, without any caption.
22 |
23 |
24 |
25 | ## Deploy InstaFix yourself (locally)
26 |
27 | 1. Clone the repository.
28 | 2. Install [Go](https://golang.org/doc/install).
29 | 3. Run `go build`.
30 | 4. Run `./instafix`.
31 |
32 | ## Deploy InstaFix yourself (cloud)
33 |
34 | 1. Pull the latest container image from GHCR and run it.
35 | `docker pull ghcr.io/wikidepia/instafix:main`
36 | 2. Run the pulled image with Docker (bound on port 3000):
37 | `docker run -d --restart=always -p 3000:3000 ghcr.io/wikidepia/instafix:main`
38 | 3. Optional: Use the Docker Compose file in [./scripts/docker-compose.yml](./scripts/docker-compose.yml).
39 | 4. Optional: Use a [Kubernetes Deployment file](./scripts/k8s/instafix-deployment.yaml) and a [Kubernetes Ingress configuration file](./scripts/k8s/instafix-ingress.yaml) to deploy to a Kubernetes cluster (with 10 replicas) by issuing `kubectl apply -f .` over the `./scripts/k8s/` folder. [TODO: CockroachDB is not shared between replicas at application level, extract Cockroach into its own Service and allow replicas to communicate to it].
40 |
41 | ## Using iOS shortcut (contributed by @JohnMcAnearney)
42 | You can use the iOS shortcut found here: [Embed in Discord](https://www.icloud.com/shortcuts/3412a4c344fd4c6f99924e525dd3c0a2), in order to quickly embed content using InstaFix. The shortcut works by grabbing the url of the Instagram content you're trying to share, automatically appends 'dd' to where it needs to be, copies this to your device's clipboard and opens Discord.
43 |
44 | Note: Once you've downloaded the shortcut, you will need to:
45 | 1. Update the value of YOUR_SERVER_URL_HERE by opening the shortcut in the Shortcuts App. The value for this can be a direct message URL or a server URL.
46 | 2. The shortcut will already be available in your share sheet. To test, go to Instagram -> share -> Tap on "Embed in Discord".
47 | 3. This will open Discord and now you simply paste the text into the custom chat you've setup!
48 | 4. Now edit your sharesheet to make it even quicker to use! Simply open your share sheet by sharing something from Instagram, click "Edit Actions..." and click the "+" button on "Embed in Discord"
49 |
50 |