Расширение API от Vk для стикеров на Elixir
Введение
Во Вк есть наборы стикеров, некоторые из которых даже бесплатные. Но во Вк нет ни какого публичного 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.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
Для стикеров дополнительным атрибутами будет коллекция адресов картинок в четырёх вариациях.
# 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
Сериализатор для стикеров будет идентичен.
# 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
.
# 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. Но прежде чем приступить к самим контроллерам, необходимо собрать модуль который будет отвечать за маршруты.
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
# 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
Результат
[{"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":"Андрей Яковенко"}]
[{"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/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":"Андрей Яковенко"}
[{"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}]
[{"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}]
[{"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}]
{"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}
{"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. В таком случае все данные о наборах стикеров будут храниться в коде включая данные об интервале принадлежащих им стикеров. Проект не сильно упростится, но в скорость базы данных вы уже не уткнётесь точно.
- Если вам интересен функциональный язык программирования Elixir или вы просто сочувствующий то советую вам присоединиться к Telegram-каналу про Elixir.
- У отечественного Elixir сообщества начинает появляться единая площадка в лице проекта Wunsh.ru. Сейчас ребята во всю пишут новую версию сайта. Но уже у них есть подписка на рассылку. В ней нет ничего нелегального, раз в недельку будет приходить письмо с подборкой статей про Elixir на русском языке.
Если вам интересна тема создания своих приложений на Elixir, могу посоветовать статью: Создание Elixir-приложения на примере. От инициализации до публикации.