Расширение API от Vk для стикеров на Elixir

image


Введение


Во Вк есть наборы стикеров, некоторые из которых даже бесплатные. Но во Вк нет ни какого публичного API для использования данного функционала на сторонних сайта. Задачка состоит в том, чтобы используя функциональный язык Elixir написать расширение над местом хранения стикеров во Вк в виде API.


По моему мнению имена методов, и параметры, которые они принимали были бы следующими. Общим пространством имён для коллекции API методов для работы со стикерами было бы ключевое слово stickers, а сами методы возможно выглядели бы так:


stickers.get — со следующими параметрами: pack_ids, pack_id, fields;
stickers.getById — со следующими параметрами: sticker_ids, sticker_id, fields.


Так как нет возможности создавать или редактировать стикеры, которые есть во Вк, данное API будет иметь только read-only методы. Честно, сложно угадывать, и не хочется подражать разработчикам социальной сети, по этому ограничусь только придумыванием имён методов. И не буду реализовывать API в стиле Вк, хоть это бы и добавило общей идентичности расширению.


Вот такие методы буду реализовывать для работы со стикерами:


Методы для наборов:


GET /packs
GET /packs/{id}
GET /packs/{id}/stickers

Методы для стикеров:


GET /stickers
GET /stickers/{id}
GET /stickers/{id}/pack

Реализация


Как написано выше, языком для написания программирования выбран Elixir. Базой данных в проекте будет выступать PostgreSQL и для взаимодействия с ней будут использованы Postgrex и Ecto. В качестве web-сервера будет использован Cowboy. За сериализацию данных в json-формат будет отвечать Poison. Вся поставленная задача довольно не объёмная и не сложная, по этому Phoenix использоваться не будет.


Для создания нового приложения используется команда mix new api_vk_stickers, она создаст базовую структуру, на основе которой будет строится расширение для API Вк.


Первым делам следует отредактировать файл mix.exs, который содержит базовую информацию о приложении и список используемых внешних зависимостей:


# mix.exs

defmodule ApiVkStickers.Mixfile do
  use Mix.Project

  # ...

  defp deps do
    [{:postgrex, "~> 0.13"},
     {:ecto, "~> 2.1.1"},
     {:cowboy, "~> 1.0.4"},
     {:plug, "~> 1.1.0"},
     {:poison, "~> 3.0"}]
  end
end

После редактирования списка зависимостей необходимо их все установить, для этого предназначена команда mix deps.get.


Теперь приступим к написанию логики самого расширения. Структура проекта будет следующая:


models/
  pack.ex
  sticker.ex
decorators/
  pack_decorator.ex
  sticker_decorator.ex
encoders/
  packs_encoder.ex
  stickers_encoder.ex
finders/
  packs_finder.ex
  stickers_finder.ex
parsers/
  ids_param_parser.ex
controllers/
  packs_controller.ex
  stickers_controller.ex
router.ex

models


Модели создаются с использованием модуля Ecto.Schema. В модели Pack вместе с полем title будет ещё несколько дополнительных не обязательных полей.


Структура модели задаётся с помощью выражения schema/2, как аргумент она принимает имя источника, то есть название таблицы. Поля задаются в теле schema/2 с помощью выражения filed/3. filed/3 принимает название поля, тип поля (по умолчанию :string) и дополнительные не обязательные функции (по умолчанию []).


Для определение связи один-ко-многим используется выражение has_many/3.


# pack.ex

defmodule ApiVkStickers.Pack do
  use Ecto.Schema

  schema "packs" do
    field :title
    field :author
    field :slug

    has_many :stickers, ApiVkStickers.Sticker
  end
end

Для противоположной связи один-к-одному предназначено выражение belongs_to/3.


Код Sticker
# sticker.ex

defmodule ApiVkStickers.Sticker do
  use Ecto.Schema

  schema "stickers" do
    field :src, :map, virtual: true

    belongs_to :pack, ApiVkStickers.Pack
  end
end

decorators


В Эликсире по понятным причинам объектов нет, но всё же логика расширения моделей будет размещена в модулях с суффиксом _decorator. API на ровне с атрибутами полученными из базы данных также будут возвращать несколько дополнительных атрибутов. Для наборов это будет коллекция обложек в двух размерах и url места, где можно добавить себе данный набор во Вк.


# pack_decorator.ex

defmodule ApiVkStickers.PackDecorator do
  @storage_url "https://vk.com/images/store/stickers"
  @shop_url "https://vk.com/stickers"

  def source_urls(pack) do
    id = pack.id

    %{small: "#{@storage_url}/#{id}/preview1_296.jpg",
      large: "#{@storage_url}/#{id}/preview1_592.jpg"}
  end

  def showcase_url(pack) do
    "#{@shop_url}/#{pack.slug}"
  end
end

Для стикеров дополнительным атрибутами будет коллекция адресов картинок в четырёх вариациях.


Код StickerDecorator
# sticker_decorator.ex

defmodule ApiVkStickers.StickerDecorator do
  @storage_url "https://vk.com/images/stickers"

  def source_urls(sticker) do
    id = sticker.id

    %{thumb: "#{@storage_url}/#{id}/64.png",
      small: "#{@storage_url}/#{id}/128.png",
      medium: "#{@storage_url}/#{id}/256.png",
      large: "#{@storage_url}/#{id}/512.png"}
  end
end

encoders


Сериализаторы будут ответственны за преобразование атрибутов в json-формат. Первым делом из модели будет создан ассоциативный массив с базовыми атрибутами, а затем в него будут добавлены экстра атрибуты полученные из декораторов. Последним шагом будет преобразование массива в JSON с помощью модуля Poison.Encoder.Map. Модуль PacksEncoder будет иметь один публичный метод call/1.


# packs_encoder.ex

defmodule ApiVkStickers.PacksEncoder do
  alias ApiVkStickers.PackDecorator

  defimpl Poison.Encoder, for: ApiVkStickers.Pack do
    def encode(pack, options) do
      Map.take(pack, [:id, :title, :author])
      |> Map.put(:source_urls, PackDecorator.source_urls(pack))
      |> Map.put(:showcase_url, PackDecorator.showcase_url(pack))
      |> Poison.Encoder.Map.encode(options)
    end
  end

  def call(stickers) do
    Poison.encode!(stickers)
  end
end

Сериализатор для стикеров будет идентичен.


Код StickersEncoder
# stickers_encoder.ex

defmodule ApiVkStickers.StickersEncoder do
  alias ApiVkStickers.StickerDecorator

  defimpl Poison.Encoder, for: ApiVkStickers.Sticker do
    def encode(sticker, options) do
      Map.take(sticker, [:id, :pack_id])
      |> Map.put(:source_urls, StickerDecorator.source_urls(sticker))
      |> Poison.Encoder.Map.encode(options)
    end
  end

  def call(stickers) do
    Poison.encode!(stickers)
  end
end

finders


Для того чтобы не хранить логику запросов в базу данных в контроллерах, будут использованы файндеры (простите, искатели). Их будет также два, по количеству моделей. Файндер по наборам будет иметь три базовые функции: all/1 — получение коллекции наборов, one/1 — получение одного набора и by_ids/1 — получение коллекции наборов согласно переданным id.


# packs_finder.ex

defmodule ApiVkStickers.PacksFinder do
  import Ecto.Query

  alias ApiVkStickers.{Repo, Pack}

  def all(query \\ Pack) do
    Repo.all(from p in query, order_by: p.id)
  end

  def one(id) do
    Repo.get(Pack, id)
  end

  def by_ids(ids) do
    all(from p in Pack, where: p.id in ^ids)
  end
end

Похожими функциями будет обладать файндер по стикерам, за исключением третьей функции by_pack_id/1, которая возвращает коллекцию стикеров не по их id, а по их pack_id.


Код StickersFinder
# stickers_finder.ex

defmodule ApiVkStickers.StickersFinder do
  import Ecto.Query

  alias ApiVkStickers.{Repo, Sticker}

  def all(query \\ Sticker) do
    Repo.all(from s in query, order_by: s.id)
  end

  def one(id) do
    Repo.get(Sticker, id)
  end

  def by_pack_ids(pack_ids) do
    all(from s in Sticker, where: s.pack_id in ^pack_ids)
  end
end

parsers


Данный сервис необходим из-за того, что не была познана практика передачи параметров в url GET-запроса таким образом, чтобы Plug автоматически представлял мне массив. И вообще как-то создавал для переданного набора id какую-то переменную, без указания принимаемых параметров в выражении get/3 модуля Plug.Router.


# ids_param_parser.ex

defmodule ApiVkStickers.IdsParamParser do
  def call(query_string, param_name \\ "ids") do
    ids = Plug.Conn.Query.decode(query_string)[param_name]

    if ids do
      String.split(ids, ",")
    end
  end
end

controllers


Контроллеры будут на основе модуля Plug.Router, DSL которого многим напомнит фреймворк Sinatra. Но прежде чем приступить к самим контроллерам, необходимо собрать модуль который будет отвечать за маршруты.


Код router.ex
defmodule ApiVkStickers.Router do
  use Plug.Router

  plug Plug.Logger
  plug :match
  plug :dispatch

  forward "/packs", to: ApiVkStickers.PacksController
  forward "/stickers", to: ApiVkStickers.StickersController

  match _ do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(404, ~s{"error":"not found"}))
  end
end

Контроллеры по сути дела тоже будут такими же маршрутными модулями, но в душе есть вера, в то, что размещение этих модулей в папку controllers было правильным решением.


# packs_controller

defmodule ApiVkStickers.PacksController do
  # ...

  get "/" do
    ids = IdsParamParser.call(conn.query_string)

    packs = if ids do
              PacksFinder.by_ids(ids)
            else
              PacksFinder.all
            end
      |> PacksEncoder.call

    send_json_resp(conn, packs)
  end

  get "/:id" do
    pack = PacksFinder.one(id)
           |> PacksEncoder.call

    send_json_resp(conn, pack)
  end

  get "/:id/stickers" do
    stickers = StickersFinder.by_pack_ids([id])
               |> StickersEncoder.call

    send_json_resp(conn, stickers)
  end

  # ...
end

Код StickersController
# stickers_controller

defmodule ApiVkStickers.StickersController do
  # ...

  get "/" do
    pack_ids = IdsParamParser.call(conn.query_string, "pack_ids")

    stickers = if pack_ids do
                 StickersFinder.by_pack_ids(pack_ids)
               else
                 StickersFinder.all
               end
      |> StickersEncoder.call

    send_json_resp(conn, stickers)
  end

  get "/:id" do
    sticker = StickersFinder.one(id)
              |> StickersEncoder.call

    send_json_resp(conn, sticker)
  end

  get "/:id/pack" do
    sticker = StickersFinder.one(id)

    pack = PacksFinder.one(sticker.pack_id)
           |> PacksEncoder.call

    send_json_resp(conn, pack)
  end

  # ...
end

Результат


$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/packs'
[{"title":"Спотти", "source_urls":{"small":"https://vk.com/images/store/stickers/1/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/1/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/spotty", "id":1,"author":"Андрей Яковенко"}, {"title":"Персик", "source_urls":{"small":"https://vk.com/images/store/stickers/2/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/2/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/persik", "id":2,"author":"Елена Савченко"}, {"title":"Смайлы", "source_urls":{"small":"https://vk.com/images/store/stickers/3/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/3/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/smilies", "id":3,"author":"Елена Савченко"}, {"title":"Фруктовощи", "source_urls":{"small":"https://vk.com/images/store/stickers/4/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/4/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/fruitables", "id":4,"author":"Андрей Яковенко"}]

$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/packs/? ids=2,3'
[{"title":"Персик", "source_urls":{"small":"https://vk.com/images/store/stickers/2/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/2/preview1_592.jpg"},"showcase_url":"https://vk.com/stickers/persik", "id":2,"author":"Елена Савченко"}, {"title":"Смайлы", "source_urls":{"small":"https://vk.com/images/store/stickers/3/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/3/preview1_592.jpg"},"showcase_url":"https://vk.com/stickers/smilies", "id":3,"author":"Елена Савченко"}]

$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/packs/1'
{"title":"Спотти", "source_urls":{"small":"https://vk.com/images/store/stickers/1/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/1/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/spotty", "id":1,"author":"Андрей Яковенко"}

$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/packs/1/stickers'
[{"source_urls":{"thumb":"https://vk.com/images/stickers/1/64.png", "small":"https://vk.com/images/stickers/1/128.png", "medium":"https://vk.com/images/stickers/1/256.png", "large":"https://vk.com/images/stickers/1/512.png"}, "pack_id":1,"id":1},...,{"source_urls":{"thumb":"https://vk.com/images/stickers/48/64.png", "small":"https://vk.com/images/stickers/48/128.png", "medium":"https://vk.com/images/stickers/48/256.png", "large":"https://vk.com/images/stickers/48/512.png"}, "pack_id":1,"id":48}]

$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/stickers'
[{"source_urls":{"thumb":"https://vk.com/images/stickers/1/64.png", "small":"https://vk.com/images/stickers/1/128.png", "medium":"https://vk.com/images/stickers/1/256.png", "large":"https://vk.com/images/stickers/1/512.png"}, "pack_id":1,"id":1}, {"source_urls":{"thumb":"https://vk.com/images/stickers/2/64.png", "small":"https://vk.com/images/stickers/2/128.png", "medium":"https://vk.com/images/stickers/2/256.png", "large":"https://vk.com/images/stickers/2/512.png"}, "pack_id":1,"id":2}, {"source_urls":{"thumb":"https://vk.com/images/stickers/3/64.png", "small":"https://vk.com/images/stickers/3/128.png", "medium":"https://vk.com/images/stickers/3/256.png", "large":"https://vk.com/images/stickers/3/512.png"}, "pack_id":1,"id":3},...,{"source_urls":{"thumb":"https://vk.com/images/stickers/167/64.png", "small":"https://vk.com/images/stickers/167/128.png", "medium":"https://vk.com/images/stickers/167/256.png", "large":"https://vk.com/images/stickers/167/512.png"}, "pack_id":4,"id":167}, {"source_urls":{"thumb":"https://vk.com/images/stickers/168/64.png", "small":"https://vk.com/images/stickers/168/128.png", "medium":"https://vk.com/images/stickers/168/256.png", "large":"https://vk.com/images/stickers/168/512.png"}, "pack_id":4,"id":168}]

$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/stickers/? pack_ids=2,3'
[{"source_urls":{"thumb":"https://vk.com/images/stickers/49/64.png", "small":"https://vk.com/images/stickers/49/128.png", "medium":"https://vk.com/images/stickers/49/256.png", "large":"https://vk.com/images/stickers/49/512.png"},"pack_id":2,"id":49}, ..., {"source_urls":{"thumb":"https://vk.com/images/stickers/128/64.png", "small":"https://vk.com/images/stickers/128/128.png", "medium":"https://vk.com/images/stickers/128/256.png", "large":"https://vk.com/images/stickers/128/512.png"},"pack_id":3,"id":128}]

$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/stickers/1'
{"source_urls":{"thumb":"https://vk.com/images/stickers/1/64.png", "small":"https://vk.com/images/stickers/1/128.png", "medium":"https://vk.com/images/stickers/1/256.png", "large":"https://vk.com/images/stickers/1/512.png"}, "pack_id":1,"id":1}

$ curl -X GET --header 'Accept: application/json' 'http://localhost:4000/stickers/1/pack'
{"title":"Спотти", "source_urls":{"small":"https://vk.com/images/store/stickers/1/preview1_296.jpg", "large":"https://vk.com/images/store/stickers/1/preview1_592.jpg"}, "showcase_url":"https://vk.com/stickers/spotty", "id":1,"author":"Андрей Яковенко"}

Послесловие


Из проекта можно убрать PostgreSQL. В таком случае все данные о наборах стикеров будут храниться в коде включая данные об интервале принадлежащих им стикеров. Проект не сильно упростится, но в скорость базы данных вы уже не уткнётесь точно.


  1. Если вам интересен функциональный язык программирования Elixir или вы просто сочувствующий то советую вам присоединиться к Telegram-каналу про Elixir.
  2. У отечественного Elixir сообщества начинает появляться единая площадка в лице проекта Wunsh.ru. Сейчас ребята во всю пишут новую версию сайта. Но уже у них есть подписка на рассылку. В ней нет ничего нелегального, раз в недельку будет приходить письмо с подборкой статей про Elixir на русском языке.

Если вам интересна тема создания своих приложений на Elixir, могу посоветовать статью: Создание Elixir-приложения на примере. От инициализации до публикации.

Комментарии (0)

© Habrahabr.ru