Расширение 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-приложения на примере. От инициализации до публикации.

ссылка на оригинал статьи https://habrahabr.ru/post/318918/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *