Files
aleesa-webapp-go/internal/flickr/flickr.go
eleksir f2fc168abe Replace prazdnikisegodnyaru with celebratoday, small underhood fixes
- Replace prazdnikisegodnyaru with celebratoday as first is banned in Russia
- Update .golangci.yml rule set
- Megatons of fixes for linter
- Update README.md
2026-02-10 20:05:13 +03:00

239 lines
8.2 KiB
Go

// Package flickr implements flickr.com service client.
package flickr
import (
"aleesa-webapp-go/internal/config"
"aleesa-webapp-go/internal/pcachedb"
"errors"
"strconv"
"context"
"fmt"
"math/rand/v2"
"net/url"
"strings"
"github.com/carlmjohnson/requests"
"gopkg.in/masci/flickr.v3"
"gopkg.in/masci/flickr.v3/test"
)
type (
// SearchResult структура, описывающая ответ api на поисковой запрос.
SearchResult struct { //nolint: govet // Идите в пень, мне так удобнее.
Photos struct { //nolint: govet // Идите в пень, мне так удобнее.
Page int64 `json:"page,omitempty"`
Pages int64 `json:"pages,omitempty"`
Perpage int64 `json:"perpage,omitempty"`
Total int64 `json:"total,omitempty"`
Photo []struct { //nolint: govet // Идите в пень, мне так удобнее.
Farm int64 `json:"farm,omitempty"`
ID string `json:"id,omitempty"`
Isfamily int64 `json:"isfamily,omitempty"`
Isfriend int64 `json:"isfriend,omitempty"`
Ispublic int64 `json:"ispublic,omitempty"`
Owner string `json:"owner,omitempty"`
Secret string `json:"secret,omitempty"`
Server string `json:"server,omitempty"`
Title string `json:"title,omitempty"`
} `json:"photo"`
} `json:"photos"`
Stat string `json:"stat,omitempty"`
}
)
// APIClientInit генерит и сохраняет token и token secret с помощью даденных пользователем key и secret.
// В процессе выдаём юзеру в консоль урл и запрос открыть его в браузере, чтобы получить verifier key, который нам
// понадобится в дальшейшем для генерации рабчочих token и token secret.
func APIClientInit(cfg *config.MyConfig) error {
var err error
client := flickr.NewFlickrClient(cfg.Flickr.Key, cfg.Flickr.Secret)
// Первым делом достанем request token.
tok, err := flickr.GetRequestToken(client)
if err != nil {
return fmt.Errorf("unable to get Request Token: %w", err)
}
// Соорудим авторизационный URL.
link, err := flickr.GetAuthorizeUrl(client, tok)
if err != nil {
return fmt.Errorf("unable to get auth URL: %w", err)
}
// А теперь ручные действия пользователя.
// Выдаём ему url, который надо ввести в адресную строку браузера, в браузере сгенерится verifier code, который нам
// нужен, чтобы вынуть из flicker api oauth token, secret, access token.
var oauthVerifier string
fmt.Println("Open your browser at this url:", link)
fmt.Print("Then, insert the code: ")
_, _ = fmt.Scanln(&oauthVerifier)
// Наконец, достаём Access Token и Aceess Token Secret и сохраняем их в базку на диске.
accessTok, err := flickr.GetAccessToken(client, tok, oauthVerifier)
if err != nil {
return fmt.Errorf("unable to make Aceess Token and Access Token Secret, API answers: %w", err)
}
fmt.Println("Successfully retrieved OAuth token and secret")
// Проверим, что всё работает.
resp, err := test.Login(client)
if err != nil {
return fmt.Errorf("unable to perform test login: %w", err)
}
fmt.Printf("Test login successful: %s, %s\n", resp.Status, resp.User)
// И где-то тут надо сохранить токен куда-то на диск.
if err = pcachedb.SaveKeyWithValue(cfg, "flickr", "token", accessTok.OAuthToken); err != nil {
return fmt.Errorf("unable to save Aceess Token: %w", err)
}
if err = pcachedb.SaveKeyWithValue(cfg, "flickr", "secret", accessTok.OAuthTokenSecret); err != nil {
return fmt.Errorf("unable to save Aceess Token Secret: %w", err)
}
return nil
}
// Populate заполняет кэш ключами.
func Populate(cfg *config.MyConfig) error {
if err := pcachedb.SaveKeyWithValue(cfg, "flickr", "token", cfg.Flickr.OAuthToken); err != nil {
return fmt.Errorf("unable to save Aceess Token: %w", err)
}
if err := pcachedb.SaveKeyWithValue(cfg, "flickr", "secret", cfg.Flickr.OAuthTokenSecret); err != nil {
return fmt.Errorf("unable to save Aceess Token Secret: %w", err)
}
return nil
}
// SearchByTags делает поисковый запрос по даденным тэгам.
func SearchByTags(cfg *config.MyConfig, tags []string) (string, error) {
var (
apiKey = cfg.Flickr.Key
apiSecret = cfg.Flickr.Secret
oauthToken = pcachedb.GetValue(cfg, "flickr", "token")
oauthSecret = pcachedb.GetValue(cfg, "flickr", "secret")
c = flickr.NewFlickrClient(apiKey, apiSecret)
userAgent = cfg.UserAgents[rand.IntN(len(cfg.UserAgents))]
ctx = context.Background()
tagsString = strings.Join(tags, ",")
)
// Заполняем поля для оаутха.
c.Init()
c.OAuthToken = oauthToken
c.OAuthTokenSecret = oauthSecret
// Добавляем параметры. В данном случае это всё, что описано в доке вот тут:
// https://www.flickr.com/services/api/flickr.photos.search.html
args := url.Values{
"format": {"json"},
"method": {"flickr.photos.search"},
"nojsoncallback": {"1"},
"content_types": {"0"},
"media": {"photos"},
"per_page": {"100"},
"tags": {tagsString},
}
c.Args = args
// Подписываем запрос oauth-говном.
c.OAuthSign()
// А мы не будем пользоваться flickr.DoGet() по 2-м причинам, во-первых он хочет xml, а это говно, а не язык
// разметки, а во-вторых, этот самый flickr.DoGet() валится с segfault-ом, просто потому что. Схема не такая, на
// которую он рассчитан. Но нам повезло, что можно просто вытащить подписанный url и заправить его в свой
// http-клиент.
link := c.GetUrl()
var response SearchResult
err := requests.
URL(link).
UserAgent(userAgent).
ToJSON(&response).
Fetch(ctx)
if response.Stat == "fail" {
return "", errors.New("search request returns no results and fails")
}
// Ну, вот мы и получили ответ на вопрос - а сколько же у нас результатов поиска всего, по заданному критерию.
// Теперь мы повторим запрос, но на cей раз попросим flickr выдать нам рандомный результат из этой кучи.
if err != nil {
return "", fmt.Errorf("unable to get results for tags %s from flickr api: %w", tagsString, err)
}
randomPage := rand.Int64N(response.Photos.Pages)
c.Args.Set("page", strconv.FormatInt(randomPage, 10))
// Подписываем запрос oauth-говном.
c.OAuthSign()
link = c.GetUrl()
err = requests.
URL(link).
UserAgent(userAgent).
ToJSON(&response).
Fetch(ctx)
if err != nil {
return "", fmt.Errorf("unable to get results for tags %s from flickr api: %w", tagsString, err)
}
if response.Stat == "fail" {
return "", errors.New("search request returns no results and fails")
}
// Частенько выборка не очень уникальна, много дублей. Чтобы почистить выборку, неплохо бы выбирать только те фотки,
// у которых response.Photos.Photo[exactResult].Title уникальный.
var u SearchResult
for i := range len(response.Photos.Photo) {
skip := false
for j := range len(u.Photos.Photo) {
if response.Photos.Photo[i].Title == u.Photos.Photo[j].Title {
skip = true
break
}
}
if skip {
continue
}
u.Photos.Photo = append(u.Photos.Photo, response.Photos.Photo[i])
}
exactResult := rand.IntN(len(u.Photos.Photo))
result := fmt.Sprintf(
"https://live.staticflickr.com/%s/%s_%s_z.jpg",
u.Photos.Photo[exactResult].Server,
u.Photos.Photo[exactResult].ID,
u.Photos.Photo[exactResult].Secret,
)
return result, err
}
/* vim: set ft=go noet ai ts=4 sw=4 sts=4: */