Vk API extension for stickers on Elixir

Introduction
VK has sticker packs, some of which are even free. But VK doesn’t have any public API for using this functionality on third-party sites. The task is to use the Elixir functional language to write an extension over the sticker storage location in VK as an API .
In my opinion, the names of the methods, and the parameters that they would take would be as follows. The common namespace for the collection of API methods for working with stickers would be a keyword stickers, and the methods themselves would probably look like this:
stickers.get- with the following parameters: pack_ids, pack_id, fields; stickers.getById- with the following parameters: sticker_ids, sticker_id, fields.
Since there is no way to create or edit stickers that are in VK, this API will only have read-only methods. Honestly, it is difficult to guess, and I do not want to imitate the developers of a social network, so I will limit myself to inventing methods names. And I will not implement the API in the style of VK, although this would add a general identity to the extension.
These are the methods I will implement to work with stickers:
Methods for sets:
GET /packs
GET /packs/{id}
GET /packs/{id}/stickersMethods for stickers:
GET /stickers
GET /stickers/{id}
GET /stickers/{id}/packImplementation
As written above, Elixir is selected as the language for writing programming . Database in the project will act as PostgreSQL and to interact with it will be used Postgrexand Ecto. As a web server will be used Cowboy. For serialization of data in json- format will be responsible Poison. The whole task is rather not voluminous and not complicated, therefore Phoenixit will not be used.
To create a new application, a team is used mix new api_vk_stickers, it will create a basic structure on the basis of which the extension for the API Vk will be built .
The first case is to edit the file mix.exsthat contains basic information about the application and a list of external dependencies used:
# 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
endAfter editing the list of dependencies, you need to install them all, for this the command is intended mix deps.get.
Now let's write the logic of the extension itself. The structure of the project will be as follows:
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.exmodels
Models are created using the module Ecto.Schema. In the model, Packalong with the field, there titlewill be a few additional optional fields.
The structure of the model is set using the expression schema/2, as an argument it takes the name of the source, that is, the name of the table. Fields are set in the body schema/2using an expression filed/3. filed/3accepts field name, field type (default :string) and additional optional functions (default []).
An expression is used to define a one-to-many relationship 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
endFor a one-to-one opposite relationship, an expression is intended 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
enddecorators
In Elixir, for obvious reasons, there are no objects, but still the logic for expanding models will be placed in modules with a suffix _decorator. The API, along with the attributes obtained from the database, will also return several additional attributes. For sets, this will be a collection of covers in two sizes and the url of the place where you can add this set to yourself on VK.
# 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
endFor stickers, the additional attributes will be a collection of image addresses in four variations.
# 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
endencoders
Serializers will be responsible for converting attributes to json format. First, an associative array with basic attributes will be created from the model, and then extra attributes obtained from decorators will be added to it. The final step is to convert the array to JSON using the module Poison.Encoder.Map. The module PacksEncoderwill have one public method 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
endThe serializer for the stickers will be identical.
# 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
endfinders
In order not to store the logic of database queries in the controllers, we will use faders (sorry, crawlers). There will also be two, according to the number of models. The set finder will have three basic functions: all/1- receiving a collection of sets, one/1- receiving a single set, and by_ids/1- receiving a collection of sets according to the transferred ones 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
endSimilar functions will be possessed by the sticker fender, with the exception of the third function by_pack_id/1, which returns a collection of stickers not by them id, but by them 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
endparsers
Данный сервис необходим из-за того, что не была познана практика передачи параметров в urlGET-запроса таким образом, чтобы 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
endcontrollers
Контроллеры будут на основе модуля 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-чатам Wunsh && Elixir и ProElixir.
- The domestic Elixir community begins to appear a single platform in the face of the project Wunsh.ru . Now the guys are writing a new version of the site. But they already have a newsletter subscription. There is nothing illegal in it, once a week a letter will come with a selection of articles about Elixir in Russian.
If you are interested in the topic of creating your own applications on Elixir , I can recommend an article: Creating an Elixir application using an example. From initialization to publicationhttps://habrahabr.ru/post/317444/ .