Back to Home

Vk API extension for stickers on Elixir

api · vk · elixir · ecto · cowboy · poison

Vk API extension for stickers on Elixir

    image


    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}/stickers

    Methods for stickers:


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

    Implementation


    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
    end

    After 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.ex

    models


    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
    end

    For a one-to-one opposite relationship, an expression is intended belongs_to/3.


    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


    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
    end

    For stickers, the 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, 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
    end

    The serializer for the 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 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
    end

    Similar 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.


    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


    Данный сервис необходим из-за того, что не была познана практика передачи параметров в 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
    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-чатам Wunsh && Elixir и ProElixir.


    2. 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/ .

    Read Next