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