Go로 단축 URL 자동 정리

오늘날 웹의 성가신 것 중 하나는 - 아니요, 이번에는 JavaScript에 대해 장황하게 말하지 않겠습니다. 약속합니다! - 단축기, 리디렉션, utm_* 등과 같은 추적 매개변수로 가득 차 있어 그늘이 없는 방식으로 URL을 공유하기가 상대적으로 어렵습니다.

웹 브라우저를 계속 사용하고 있다면 더 이상 문제가 되지 않습니다. 리디렉터의 전부는 아니더라도 대부분을 제거하는 다양한 브라우저 확장이 도착하면 이 문제가 해결됩니다. 여전히 다루지 않는 것은 시스템 클립보드입니다. 대화 상대에게 다음과 같은 링크 보내기

https://bit.ly/3hXl0mS

아마도 이것을 보내는 것보다 덜 매력적인 옵션일 것입니다:

https://dev.to/tux0r/properly-validating-e-mail-addresses-3lpj

채워야 할 틈새 시장이 있습니다. 그리고 그렇게 하는 것은 비교적 쉽습니다! Go를 사용해 봅시다. 왜 안 될까요?

우리의 애플리케이션은 두 부분으로 구성됩니다. 하나는 클립보드를 감시하고(사용자가 원할 때마다 중지할 수 있음), 다른 하나는 URL을 처리하고 단축 및 추적 해제합니다. 두 번째는 더 복잡합니다.

첫 번째 단계: 클리너 기능.



단축된 URL에는 bit.lyt.co와 같은 실제 리다이렉터와 자체 링크를 시도하는 회사에 속하는 의사 리디렉터(예: 대부분의 대형 미디어 기업. 애플리케이션은 둘 다 감지해야 합니다. 운 좋게도 Go에는 다양한 네트워크 기능이 있으므로 외부 패키지 없이 URL의 실제 대상을 결정하는 함수를 작성할 수 있습니다.

func ExpandUrl(url string) (string, error) {
    // URL expander with x509 checks disabled.
    expandedUrl := url

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
    }

    client := &http.Client{
        CheckRedirect: func(req *http.Request, via []*http.Request) error {
            expandedUrl = req.URL.String()
            return nil
        },
        Transport: tr,
    }

    req, err := http.NewRequest(http.MethodGet, url, nil)
    if err != nil {
        return "", err
    }

    resp, err := client.Do(req)
    if err != nil {
        return "", err
    }

    defer resp.Body.Close()

    return expandedUrl, nil
}

이것은 실제 URL을 반환하거나 문제가 있는 경우 오류를 반환합니다. 사이트 자체의 유효성은 우리가 달성하고자 하는 것과 관련이 없기 때문에 인증서 확인을 건너뛸 것입니다.

물론 알려진 단축기 목록을 유지할 수도 있습니다. URL이 리디렉션임을 이미 알고 있다면 서버를 적극적으로 호출할 필요가 없습니다.

var shortenerList = []string{
    "bit.ly", "buff.ly", "dlvr.it",
    "goo.gl", "youtu.be", "tinyurl.com",
    "ow.ly", "amzn.to", "ift.tt", "zpr.io",
    "apple.co", "mol.im", "redd.it",
    "shar.es", "is.gd", "dld.bz",
    "trib.al", "fb.me", "tumblr.co",
    "cutt.ly", "app.link", "twib.in",
    "kko.to", "rsci.app.link", "upflow.co",
    "snip.ly", "lnk.to", "1jux.net", "gscoff.co",
}

이 목록은 불완전하지만 직접 확장하는 방법을 볼 수 있습니다. 그런데 목록 내부의 주석은 허용되므로 자유롭게 구조를 추가할 수 있습니다.

목록에 대해 이야기하면서 추적 매개변수도 제거하고 싶습니다. 이를 위해 애플리케이션은 어떤 매개변수가 원하지 않는지 알아야 합니다. 오늘 현재 내 것은 다음과 같습니다.

var urlParamBlacklist = []string{
    "wtmc", "WT.mc_id", "wt_zmc",

    "ocid", "xid",

    "at_medium", "at_campaign", "at_custom1", "at_custom2",
    "at_custom3", "at_custom4",

    "utm_source", "utm_medium", "utm_campaign", "utm_term",
    "utm_content", "utm_name", "utm_referrer", "utm_brand",
    "utm_social-type", "utm_kxconfid",

    "guce_referrer", "guce_referrer_sig", "guccounter",

    "ga_source", "ga_medium", "ga_term", "ga_content",
    "ga_campaign", "ga_place",

    "fb_action_ids", "fb_action_types", "fb_source", "fb_ref",
    "fbclid", "fbc",

    "hmb_campaign", "hmb_medium", "hmb_source",

    "newsticker", "CMP", "feature", "camp", "cid", "source",
    "ns_campaign", "ns_mchannel", "ito", "xg_source", "__tn__",
    "__twitter_impression", "share", "ncid", "rnd", "feed_id",
    "_unique_id", "GEPC", "pt", "xtor", "wtrid", "medium", "sara_ecid",
    "from", "inApp", "ssm", "campaign", "mbid", "s_campaign", "rss_id",
    "cmpid", "s_cid", "mv2", "scid", "sdid", "s_iid", "ssm",
    "spi_ref", "referrerlane",

    "share_bandit_exp", "share_bandit_var",

    "igshid", "idpartenaire",

    "aff_code", "affID",

    "recruited_by_id", "recruiter",
}

이제 우리에게 필요한 것은 URL을 매개변수로 받아 단축되지 않은 추적되지 않은 것을 반환하는 래퍼 함수입니다.

func contains(arr []string, str string) bool {
    // Array-containing check: Returns true if found.
    // I wish Go could do that natively. :-)
    for _, a := range arr {
        if a == str {
            return true
        }
    }
    return false
}

func processUrlItem(urlToCheck string) string {
    // Processes an URL item, returns the cleaned, unshortened
    // "expanded" URL.
    u, err := url.Parse(urlToCheck)
    if err != nil {
        log.Fatal(err)
    }

    // Some URL shorteners are not known to us (yet?).
    // Chances are that URLs with a path that ends in
    // "/SoMeSTRiNG123" are shortened. Catch them as well.
    re, _ := regexp.Compile("^/[A-Za-z0-9_-]{5,}$")
    potentialUrlShortener := re.MatchString(u.Path)

    if potentialUrlShortener || contains(shortenerList, u.Hostname()) {
        expandedUrl, err := ExpandUrl(urlToCheck)
        if err != nil {
            // Cannot reach the URL:
            return urlToCheck
        }

        // Overwrite the original URL by the expanded one:
        urlToCheck = expandedUrl

        // Parse again, just in case:
        u, err = url.Parse(urlToCheck)
        if err != nil {
            // Error in the updated domain:
            return urlToCheck
        }
    }

    // Remove tracking parameters:
    q := u.Query()
    for _, param := range urlParamBlacklist {
        q.Del(param)
        u.RawQuery = q.Encode()
    }

    return u.String()
}

작동 여부를 테스트하시겠습니까? 임시main() 함수 작성:

func main() {
    fmt.Printf("%s\n", processUrlItem("https://bit.ly/3hXl0mS"))
}

아무도 끔찍한 실수를 하지 않았다면 지금 터미널에 단축되지 않은 링크가 표시되어야 합니다.

다음 단계: 실제 적용.



이제 정리 기능이 작동하므로 클립보드를 둘러쌀 수 있습니다. 이것도 비교적 쉽습니다. 클립보드를 1초에 한 번 폴링하고 내용을 확인하고 발견된 모든 URL을 클리너에 전달하고 결과를 클립보드에 다시 쓸 수 있습니다. Go의 좋은 생태계는 이것을 훨씬 더 쉽게 해줍니다. 우리는 주석과 공백을 포함하여 전체 호출 애플리케이션을 50줄 미만으로 맞출 수 있습니다.

package main

import (
    "fmt"
    "net/url"
    "time"

    "github.com/getlantern/systray"
    "github.com/atotto/clipboard"
)

var (
    previousUrl string   // Avoid parsing it over and over again
)

func main() {
    go func() {
        for x := range time.Tick(time.Second) {
            clipped, err := clipboard.ReadAll()
            if err == nil && clipped != previousUrl {
                u, invalidUrl := url.Parse(clipped)
                if invalidUrl == nil && u.Host != "" {
                    // valid URL
                    fmt.Printf("[%s] Processing URL: %s\n", x, clipped)
                    previousUrl = processUrlItem(clipped)
                    clipboard.WriteAll(previousUrl)
                }   
            }
        }
    }()
    systray.Run(onReady, onExit)
}

func onReady() {
    systray.SetTitle("🧹")
    systray.SetTooltip("I'm cleaning URLs in the clipboard")
    mQuitOrig := systray.AddMenuItem("Quit", "Quit cleaning URLs")

    go func() {
        <-mQuitOrig.ClickedCh
        systray.Quit()
    }()
}

func onExit() {
    // Cleanup
    // This is pointless as of now, but might happen later.
}

트레이 아이콘도 함께 제공됩니다! (그림 이모티콘은 Windows에서는 작동하지 않지만 macOS에서는 멋지게 보입니다.)

작업 예



위의 글은 서론에서 말했던 문제에 대한 나의 작업의 결과이다. 코드를 직접 작성하기 전에 테스트하려면 저장소에서 바로 가져오세요.

% mkdir work
% fossil clone https://code.rosaelefanten.org/clipurlcleaner clip.fossil
% cd work ; fossil open ../clip.fossil

(GitHub 미러는 here. 입니다)

빌드 및 실행:

% go build
% ./clipurlcleaner >/dev/null &

macOS Catalina 및 Windows 10에서 솔루션을 테스트했으며 충분히 잘 작동합니다. Linux, BSD 또는 Unix에서 작동하는지 여부는 말할 수 없습니다. 작동하지 않는 경우 수정하도록 도와주세요.

코멘트?



직원들이 저를 싫어하기 때문에 슬프게도 여기 DEV에서 답변할 수 없습니다. 읽어 주셔서 감사합니다!

좋은 웹페이지 즐겨찾기