📜 ⬆️ ⬇️

Expansion of API from Vk for stickers on Elixir

image


Introduction


There are sticker sets in VK, some of which are even free. But in VK there is not any public API for using this functionality on third-party sites. The task is to use the functional language Elixir to write an extension above the storage location of stickers in VK as an API .


In my opinion, the names of the methods, and the parameters that they took would be next. The common namespace for the collection of API methods for working with stickers would be the 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 possibility to create or edit stickers that are in VK, this API will have only read-only methods. Honestly, it is difficult to guess, and do not want to imitate the developers of a social network, for this I will confine myself only to inventing the names of methods. And I will not implement the API in the style of VK, even though it would add a common 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}/stickers 

Methods for stickers:


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

Implementation


As written above, Elixir is selected as the programming language. The database in the project will be PostgreSQL and Postgrex and Ecto will be used to interact with it. Cowboy will be used as a web server. For the serialization of data in json- format will be responsible Poison . The whole task is not quite voluminous and not difficult, for this Phoenix will not be used.


To create a new application, use the mix new api_vk_stickers , it will create a basic structure, on the basis of which an extension for the VK API will be built.


The first mix.exs do is edit the mix.exs file, which 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 end 

After editing the list of dependencies, you need to install them all, for this the mix deps.get command is mix deps.get .


Now let's start writing the logic of the extension itself. The project structure 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.ex 

models


Models are created using the Ecto.Schema module. In the Pack model, along with the title field, there will be some more optional non-mandatory fields.


The model structure is specified using the expression schema/2 , as an argument it takes the name of the source, that is, the name of the table. Fields are specified in the body of schema/2 using the expression filed/3 . filed/3 accepts the field name, field type (default :string ) and optional optional functions (default [] ).


To define a one-to-many relationship, use the expression 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 

For the opposite one-to-one relation, the expression belongs_to/3 intended.


Sticker Code
 # 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


For obvious reasons, there are no objects in the Elixir, but the logic of model expansion will be placed in modules with the _decorator suffix. 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 yourself this set in 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 end 

For stickers, additional attributes will be a collection of image addresses in four variations.


StickerDecorator Code
 # 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


Serializers will be responsible for converting attributes to json format. First of all, 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 Poison.Encoder.Map module. The PacksEncoder module will have one public call/1 method.


 # 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 

The serializer for stickers will be identical.


StickersEncoder Code
 # 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


In order not to store the query logic to the database in the controllers, the fader will be used (sorry, searchers). They will also be two, according to the number of models. The ender for sets will have three basic functions: all/1 - getting the collection of sets, one/1 - getting one set and by_ids/1 - getting the collection of sets according to the transferred 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 

Similar functions will be provided by the filer by stickers, with the exception of the third function by_pack_id/1 , which returns a collection of stickers not by their id , but by their pack_id .


StickersFinder Code
 # 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


This service is necessary due to the fact that the practice of passing parameters to the url of a GET request was not known in such a way that the Plug automatically presents me an array. And in general, somehow I created some variable for the passed id set, without specifying the parameters to be received in the Plug.Router get/3 Plug.Router 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


The controllers will be based on the Plug.Router module, whose DSL will resemble the Sinatra framework to many. But before proceeding to the controllers themselves, it is necessary to assemble a module that will be responsible for the routes.


Router.ex code
 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 

The controllers, in fact, will also be the same routing modules, but in the soul there is a belief that placing these modules in the controllers folder was the right decision.


 # 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 Code
 # 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 

Result


$ 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":" "} 

Afterword


PostgreSQL can be removed from the project. In this case, all data on sticker sets will be stored in the code, including data on the interval of stickers belonging to them. The project is not much simpler, but in the database speed, you will not be completely sure.


  1. If you are interested in the functional programming language Elixir or you are just sympathetic, then I advise you to join the Wunsh && Elixir and ProElixir Telegram chat rooms.


  2. In the domestic Elixir community, a single platform begins to appear in the face of the Wunsh.ru project. Now the guys are writing a new version of the site. But they already have a subscription to the newsletter. 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 advise the article: Creating an Elixir application using an example. From initialization to publication https://habrahabr.ru/post/317444/ .


')

Source: https://habr.com/ru/post/318918/


All Articles