├── test ├── test_helper.exs └── elixirplusreddit_test.exs ├── .gitignore ├── mix.lock ├── lib ├── elixirplusreddit.ex └── elixirplusreddit │ ├── pqueue.ex │ ├── requestbuilder.ex │ ├── api │ ├── authentication.ex │ ├── comment.ex │ ├── identity.ex │ ├── post.ex │ ├── inbox.ex │ ├── subreddit.ex │ └── user.ex │ ├── requestqueue.ex │ ├── scheduler.ex │ ├── config.ex │ ├── request.ex │ ├── parser.ex │ ├── tokenserver.ex │ ├── requestserver.ex │ └── paginator.ex ├── mix.exs └── README.md /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start() 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | /config/config.exs 5 | erl_crash.dump 6 | *.ez 7 | -------------------------------------------------------------------------------- /test/elixirplusreddit_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusRedditTest do 2 | use ExUnit.Case 3 | doctest ElixirPlusReddit 4 | 5 | test "the truth" do 6 | assert 1 + 1 == 2 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"earmark": {:hex, :earmark, "0.2.0"}, 2 | "ex_doc": {:hex, :ex_doc, "0.11.3"}, 3 | "httpotion": {:hex, :httpotion, "2.1.0"}, 4 | "ibrowse": {:git, "https://github.com/cmullaparthi/ibrowse.git", "ea3305d21f37eced4fac290f64b068e56df7de80", [tag: "v4.1.2"]}, 5 | "poison": {:hex, :poison, "1.5.0"}, 6 | "pqueue": {:git, "https://github.com/okeuday/pqueue.git", "3e8be6969eaf7d5d327716ad2175a2341dd004f3", []}} 7 | -------------------------------------------------------------------------------- /lib/elixirplusreddit.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit do 2 | use Application 3 | 4 | alias ElixirPlusReddit.RequestQueue 5 | alias ElixirPlusReddit.RequestServer 6 | alias ElixirPlusReddit.TokenServer 7 | 8 | def start(_type, _args) do 9 | import Supervisor.Spec, warn: false 10 | 11 | children = [ 12 | worker(RequestQueue, []), 13 | worker(RequestServer, []), 14 | worker(TokenServer, []) 15 | ] 16 | 17 | opts = [strategy: :one_for_one, name: ElixirPlusReddit.Supervisor] 18 | Supervisor.start_link(children, opts) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [app: :elixirplusreddit, 6 | version: "0.0.1", 7 | elixir: "~> 1.2", 8 | build_embedded: Mix.env == :prod, 9 | start_permanent: Mix.env == :prod, 10 | deps: deps] 11 | end 12 | 13 | def application do 14 | [applications: [:logger, :httpotion], 15 | mod: {ElixirPlusReddit, []}] 16 | end 17 | 18 | defp deps do 19 | [ 20 | {:pqueue, github: "okeuday/pqueue"}, 21 | {:ibrowse, github: "cmullaparthi/ibrowse", tag: "v4.1.2"}, 22 | {:httpotion, "~> 2.1.0"}, 23 | {:poison, "~> 1.5"}, 24 | {:earmark, "~> 0.1", only: :dev}, 25 | {:ex_doc, "~> 0.11", only: :dev} 26 | ] 27 | end 28 | 29 | end 30 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/pqueue.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.PQueue do 2 | 3 | @moduledoc """ 4 | A tiny wrapper around okeuday's pqueue library. 5 | """ 6 | 7 | @doc """ 8 | Create a new pqueue. 9 | """ 10 | 11 | def new do 12 | :pqueue.new 13 | end 14 | 15 | @doc """ 16 | Insert a request into a pqueue. 17 | """ 18 | 19 | def enqueue(pqueue, request, priority) do 20 | :pqueue.in(request, priority, pqueue) 21 | end 22 | 23 | @doc """ 24 | Return the next request in the pqueue and the new pqueue. 25 | """ 26 | 27 | def dequeue(pqueue) do 28 | :pqueue.out(pqueue) 29 | end 30 | 31 | @doc """ 32 | Return if the pqueue is empty. 33 | """ 34 | 35 | def is_empty?(pqueue) do 36 | :pqueue.is_empty(pqueue) 37 | end 38 | 39 | end 40 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/requestbuilder.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.RequestBuilder do 2 | 3 | @moduledoc """ 4 | A utility module for formatting request data. 5 | """ 6 | 7 | def format_get(from, tag, url, parse_strategy, priority) do 8 | %{ 9 | method: :get, 10 | from: from, 11 | tag: tag, 12 | url: url, 13 | parse_strategy: parse_strategy, 14 | priority: priority 15 | } 16 | end 17 | 18 | 19 | def format_get(from, tag, url, query, parse_strategy, priority) do 20 | %{ 21 | method: :get, 22 | from: from, 23 | tag: tag, 24 | url: url, 25 | query: query, 26 | parse_strategy: parse_strategy, 27 | priority: priority 28 | } 29 | end 30 | 31 | def format_post(from, tag, url, query, parse_strategy, priority) do 32 | %{ 33 | method: :post, 34 | from: from, 35 | tag: tag, 36 | url: url, 37 | query: query, 38 | parse_strategy: parse_strategy, 39 | priority: priority 40 | } 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/api/authentication.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.API.Authentication do 2 | 3 | @moduledoc """ 4 | Provides an interface for acquiring access tokens. Requesting a token 5 | is considered a special case and is NOT subject to rate limiting through 6 | the priority queue. 7 | """ 8 | 9 | alias ElixirPlusReddit.Config 10 | alias ElixirPlusReddit.Request 11 | alias ElixirPlusReddit.Parser 12 | 13 | @token_endpoint "https://www.reddit.com/api/v1/access_token" 14 | 15 | 16 | @doc """ 17 | Grants the client an access token. 18 | """ 19 | 20 | def request_token do 21 | creds = Config.credentials 22 | body = [grant_type: "password", username: creds[:username], password: creds[:password]] 23 | basic_auth = [basic_auth: {creds[:client_id], creds[:client_secret]}] 24 | retry_until_success(@token_endpoint, body, basic_auth) 25 | end 26 | 27 | defp retry_until_success(endpoint, body, basic_auth) do 28 | {resp, strategy} = Request.request_token(@token_endpoint, body, basic_auth) 29 | case resp.status_code do 30 | c when c in 500..599 -> retry_until_success(endpoint, body, basic_auth) 31 | _ -> Parser.parse(resp, strategy) 32 | end 33 | end 34 | 35 | 36 | end 37 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/requestqueue.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.RequestQueue do 2 | 3 | @moduledoc """ 4 | A request queue built on top of an Agent. Handles distributing 5 | requests to the request server. The queue is externalized from the server 6 | to avoid loss of important state and for handling timeouts. 7 | """ 8 | 9 | @request_queue __MODULE__ 10 | 11 | alias ElixirPlusReddit.PQueue 12 | 13 | def start_link do 14 | Agent.start_link(fn -> PQueue.new end, name: @request_queue) 15 | end 16 | 17 | def enqueue_request(%{priority: p} = request_data) do 18 | Agent.update(@request_queue, fn(request_queue) -> 19 | PQueue.enqueue(request_queue, request_data, p) 20 | end) 21 | end 22 | 23 | def peek_request do 24 | Agent.get(@request_queue, fn(request_queue) -> 25 | {{:value, request}, _request_queue} = PQueue.dequeue(request_queue) 26 | request 27 | end) 28 | end 29 | 30 | def dequeue_request do 31 | Agent.update(@request_queue, fn(request_queue) -> 32 | {{:value, _request}, request_queue} = PQueue.dequeue(request_queue) 33 | request_queue 34 | end) 35 | end 36 | 37 | def is_empty? do 38 | Agent.get(@request_queue, fn(request_queue) -> 39 | PQueue.is_empty?(request_queue) 40 | end) 41 | end 42 | 43 | end 44 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/scheduler.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.Scheduler do 2 | use GenServer 3 | 4 | @moduledoc """ 5 | A generic implementation for scheduling API calls on an interval. 6 | """ 7 | 8 | @scheduler __MODULE__ 9 | 10 | def schedule({module, function}, arguments, interval) do 11 | start_link({module, function}, arguments, interval) 12 | end 13 | 14 | def schedule({module, function}, interval) do 15 | start_link({module, function}, [], interval) 16 | end 17 | 18 | def start_link({module, function}, arguments, interval) do 19 | config = [ 20 | module: module, 21 | function: function, 22 | arguments: arguments, 23 | interval: interval 24 | ] 25 | 26 | GenServer.start_link(@scheduler, config, []) 27 | end 28 | 29 | def init(config) do 30 | process_request(config) 31 | schedule_request(config[:interval]) 32 | {:ok, config} 33 | end 34 | 35 | def handle_info(:next_request, config) do 36 | process_request(config) 37 | schedule_request(config[:interval]) 38 | {:noreply, config} 39 | end 40 | 41 | defp process_request([module: m, function: x, arguments: a, interval: _]) do 42 | apply(m, x, a) 43 | end 44 | 45 | defp schedule_request(interval) do 46 | Process.send_after(self, :next_request, interval) 47 | end 48 | 49 | end 50 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/config.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.Config do 2 | 3 | @moduledoc """ 4 | An interface for accessing configuration data from config.exs. 5 | """ 6 | 7 | @doc """ 8 | Return credentials from config.exs. Credentials are stored as a 9 | keyword list: 10 | 11 | [ 12 | username: username, 13 | password: password, 14 | client_id: client_id, 15 | client_secret: client_secret 16 | ] 17 | """ 18 | 19 | def credentials do 20 | Application.get_env(:elixirplusreddit, :creds) 21 | end 22 | 23 | @doc """ 24 | Return the client's user agent. 25 | """ 26 | 27 | def user_agent do 28 | Application.get_env(:elixirplusreddit, :user_agent) 29 | end 30 | 31 | @doc """ 32 | Manually configure credentials. 33 | """ 34 | 35 | def set_credentials(username, password, client_id, client_secret, user_agent) do 36 | creds = [ 37 | username: username, 38 | password: password, 39 | client_id: client_id, 40 | client_secret: client_secret 41 | ] 42 | 43 | Application.put_env(:elixirplusreddit, :creds, creds) 44 | Application.put_env(:elixirplusreddit, :user_agent, user_agent) 45 | end 46 | 47 | @doc """ 48 | Check if credentials are configured. 49 | """ 50 | 51 | def is_configured? do 52 | case Application.get_env(:elixirplusreddit, :creds) do 53 | nil -> false 54 | _ -> true 55 | end 56 | end 57 | 58 | end 59 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/request.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.Request do 2 | 3 | alias ElixirPlusReddit.Parser 4 | alias ElixirPlusReddit.TokenServer 5 | alias ElixirPlusReddit.Config 6 | 7 | @moduledoc """ 8 | A thin wrapper around HTTPotion's HTTP request function. All requests are processed inside 9 | of this module after being received from the request server. 10 | """ 11 | 12 | def request_token(url, query, options) do 13 | options = [body: encode_post_query(query), headers: headers] |> Keyword.merge(options) 14 | {HTTPotion.request(:post, url, options), :token} 15 | end 16 | 17 | def request(:get, url, parse_strategy) do 18 | options = [headers: headers(:token_required)] 19 | {HTTPotion.request(:get, url, options), parse_strategy} 20 | end 21 | 22 | def request(:get, url, query, parse_strategy) do 23 | url = encode_get_query(url, query) 24 | options = [headers: headers(:token_required)] 25 | {HTTPotion.request(:get, url, options), parse_strategy} 26 | end 27 | 28 | def request(:post, url, query, parse_strategy) do 29 | options = [body: encode_post_query(query), headers: headers(:token_required)] 30 | {HTTPotion.request(:post, url, options), parse_strategy} 31 | end 32 | 33 | defp headers(:token_required) do 34 | ["Authorization": TokenServer.token] |> Keyword.merge(headers) 35 | end 36 | 37 | defp headers do 38 | ["Content-Type": "application/x-www-form-urlencoded", 39 | "User-Agent": Config.user_agent] 40 | end 41 | 42 | defp encode_get_query(url, query) do 43 | "#{url}?#{URI.encode_query(query)}" 44 | end 45 | 46 | defp encode_post_query(query) do 47 | URI.encode_query(query) 48 | end 49 | 50 | end 51 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/parser.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.Parser do 2 | 3 | @moduledoc """ 4 | A parser built on top of Poison for decoding Reddit's various return 5 | structures. 6 | """ 7 | 8 | def parse(%HTTPotion.Response{body: body, status_code: code}, strategy) do 9 | case code do 10 | _ -> 11 | body = Poison.decode!(body, keys: :atoms) 12 | parse(body, strategy) 13 | end 14 | end 15 | 16 | def parse(resp, :token) do 17 | "bearer #{resp.access_token}" 18 | end 19 | 20 | def parse(resp, :trophies) do 21 | Enum.map(resp.data.trophies, fn(trophy) -> trophy.data end) 22 | end 23 | 24 | def parse(resp, :listing) when is_list(resp) do 25 | for r <- resp, do: parse(r, :listing) 26 | end 27 | 28 | def parse(resp, :listing) do 29 | parse_data(resp) 30 | end 31 | 32 | defp parse_data(resp) do 33 | # Recursively flatten the data member of any children 34 | if Map.has_key?(resp, :data) && Map.has_key?(resp.data, :children) do 35 | Map.update!(resp.data, :children, fn(children) -> 36 | Enum.map(children, fn(child) -> 37 | parse_data(child.data) 38 | end) 39 | end) 40 | else 41 | resp 42 | end 43 | end 44 | 45 | def parse(resp, :reply) do 46 | errors = resp.json.errors 47 | things = resp.json.data.things |> List.first |> Map.get(:data) 48 | Map.put(things, :errors, errors) 49 | end 50 | 51 | def parse(resp, :submission) do 52 | resp.json.data 53 | end 54 | 55 | def parse(resp, :compose) do 56 | resp.json 57 | end 58 | 59 | def parse(resp, :no_data) do 60 | resp 61 | end 62 | 63 | def parse(resp, _) do 64 | resp 65 | end 66 | 67 | end 68 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/tokenserver.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.TokenServer do 2 | use GenServer 3 | 4 | @moduledoc """ 5 | Handles distrubuting and acquiring access tokens. 6 | 7 | ### Details 8 | 9 | A new token is automatically acquired every 58 minutes. It is possible to manually 10 | acquire tokens but should only be used when you manually configure credentials or 11 | a request returns with the status code 401. (Expired token) 12 | """ 13 | 14 | alias ElixirPlusReddit.API.Authentication 15 | alias ElixirPlusReddit.Config 16 | 17 | @tokenserver __MODULE__ 18 | @token_interval 1000 * 60 * 58 19 | 20 | @doc """ 21 | Start the token server. A token is acquired inside of init/1. 22 | """ 23 | 24 | def start_link do 25 | GenServer.start_link(@tokenserver, [], name: @tokenserver) 26 | end 27 | 28 | @doc """ 29 | Returns the current access token. 30 | """ 31 | 32 | def token do 33 | GenServer.call(@tokenserver, :token) 34 | end 35 | 36 | @doc """ 37 | Issues a new token manually. 38 | """ 39 | 40 | # NOTE: The request is sent to the server with send so that it is handled by 41 | # the handle_info callback. 42 | 43 | def acquire_token do 44 | send(@tokenserver, :new_token) 45 | end 46 | 47 | def init(_) do 48 | case Config.is_configured? do 49 | true -> 50 | token_state = Authentication.request_token 51 | schedule_token_request 52 | {:ok, token_state} 53 | false -> 54 | {:ok, :no_token} 55 | end 56 | end 57 | 58 | def handle_call(:token, _from, token_state) do 59 | {:reply, token_state, token_state} 60 | end 61 | 62 | def handle_info(:new_token, _token_state) do 63 | token_state = Authentication.request_token 64 | schedule_token_request 65 | {:noreply, token_state} 66 | end 67 | 68 | defp schedule_token_request do 69 | Process.send_after(@tokenserver, :new_token, @token_interval) 70 | end 71 | 72 | end 73 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/requestserver.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.RequestServer do 2 | use GenServer 3 | 4 | @moduledoc """ 5 | Rate limits requests and distributes responses. 6 | """ 7 | 8 | alias ElixirPlusReddit.Request 9 | alias ElixirPlusReddit.Parser 10 | alias ElixirPlusReddit.RequestQueue 11 | 12 | @requestserver __MODULE__ 13 | @request_interval 1000 14 | @no_request_interval 500 15 | 16 | def start_link do 17 | GenServer.start_link(@requestserver, [], name: @requestserver) 18 | end 19 | 20 | def init(_) do 21 | schedule_request(@no_request_interval) 22 | {:ok, :no_state} 23 | end 24 | 25 | def handle_info(:next_request, :no_state) do 26 | case RequestQueue.is_empty? do 27 | true -> 28 | schedule_request(@no_request_interval) 29 | {:noreply, :no_state} 30 | false -> 31 | %{from: from, tag: tag} = request_data = RequestQueue.peek_request 32 | resp = retry_until_success(request_data) 33 | RequestQueue.dequeue_request 34 | send_response(from, {tag, resp}) 35 | schedule_request(@request_interval) 36 | {:noreply, :no_state} 37 | end 38 | end 39 | 40 | defp send_response(from, {tag, resp}) do 41 | send(from, {tag, resp}) 42 | end 43 | 44 | defp issue_request(%{method: :post} = request_data) do 45 | %{url: url, query: query, parse_strategy: strategy} = request_data 46 | Request.request(:post, url, query, strategy) 47 | end 48 | 49 | defp issue_request(%{method: :get, query: query} = request_data) do 50 | %{url: url, parse_strategy: strategy} = request_data 51 | Request.request(:get, url, query, strategy) 52 | end 53 | 54 | defp issue_request(%{method: :get} = request_data) do 55 | %{url: url, parse_strategy: strategy} = request_data 56 | Request.request(:get, url, strategy) 57 | end 58 | 59 | defp retry_until_success(request_data) do 60 | {resp, parse_strategy} = issue_request(request_data) 61 | case resp.status_code do 62 | 503 -> retry_until_success(request_data) 63 | _ -> resp |> Parser.parse(parse_strategy) 64 | end 65 | end 66 | 67 | defp schedule_request(interval) do 68 | Process.send_after(@requestserver, :next_request, interval) 69 | end 70 | 71 | end 72 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/paginator.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.Paginator do 2 | use GenServer 3 | 4 | @moduledoc """ 5 | A generic interface for paginating through listings. 6 | """ 7 | 8 | @paginator __MODULE__ 9 | 10 | def paginate({module, function}, arguments) do 11 | start_link({module, function}, arguments) 12 | end 13 | 14 | def start_link({module, function}, [from|arguments]) do 15 | config = [ 16 | from: from, 17 | module: module, 18 | function: function, 19 | arguments: arguments 20 | ] 21 | 22 | GenServer.start_link(@paginator, config, []) 23 | end 24 | 25 | def init(config) do 26 | process_request(config, self) 27 | {:ok, config} 28 | end 29 | 30 | def handle_info({tag, resp}, config) do 31 | send(config[:from], {tag, resp.children}) 32 | case resp.after do 33 | nil -> 34 | send(config[:from], {tag, :complete}) 35 | send(self(), :stop) 36 | {:noreply, config} 37 | id -> 38 | new_config = update_limit(config) 39 | cond do 40 | get_limit(new_config) <= 0 -> 41 | send(config[:from], {tag, :complete}) 42 | send(self(), :stop) 43 | {:noreply, new_config} 44 | true -> 45 | new_config = update_id(new_config, id) 46 | process_request(new_config, self) 47 | {:noreply, new_config} 48 | end 49 | end 50 | end 51 | 52 | def handle_info(:stop, config) do 53 | {:stop, :normal, config} 54 | end 55 | 56 | def terminate(:normal, _config) do 57 | :ok 58 | end 59 | 60 | defp process_request([from: _from, module: m, function: x, arguments: a], server) do 61 | apply(m, x, [server|a]) 62 | end 63 | 64 | defp get_limit(config) do 65 | Enum.reduce(config[:arguments], nil, fn(arg, acc) -> 66 | case is_list(arg) do 67 | true -> arg[:limit] 68 | false -> acc 69 | end 70 | end) 71 | end 72 | 73 | defp update_limit(config) do 74 | Keyword.update!(config, :arguments, fn(arguments) -> 75 | Enum.map(arguments, fn(arg) -> 76 | case is_list(arg) do 77 | true -> Keyword.update!(arg, :limit, fn(limit) -> limit - 100 end) 78 | false -> arg 79 | end 80 | end) 81 | end) 82 | end 83 | 84 | defp update_id(config, after_id) do 85 | Keyword.update!(config, :arguments, fn(arguments) -> 86 | Enum.map(arguments, fn(arg) -> 87 | case is_list(arg) do 88 | true -> Keyword.put(arg, :after, after_id) 89 | false -> arg 90 | end 91 | end) 92 | end) 93 | end 94 | 95 | end 96 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/api/comment.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.API.Comment do 2 | @moduledoc """ 3 | An interface for getting comment information. 4 | """ 5 | 6 | alias ElixirPlusReddit.RequestBuilder 7 | alias ElixirPlusReddit.RequestQueue 8 | alias ElixirPlusReddit.Paginator 9 | alias ElixirPlusReddit.Scheduler 10 | 11 | @comment __MODULE__ 12 | @comment_base "https://oauth.reddit.com/comments" 13 | @default_priority 0 14 | 15 | @doc """ 16 | Get an article's `new` comments. 17 | 18 | ### Parameters 19 | 20 | * `from`: The pid or name of the requester. 21 | * `tag`: Anything. 22 | * `article_id:` An article id. 23 | * `options`: The query. (Optional) 24 | * `priority`: The request's priority. (Optional) 25 | 26 | ### Options 27 | 28 | * `limit`: a value between 1-100 29 | * `after`: a fullname of a thing 30 | * `before`: a fullname of a thing 31 | 32 | ### Fields 33 | 34 | after 35 | before 36 | modhash 37 | children: 38 | author_flair_css_class 39 | gilded 40 | body_html 41 | quarantine 42 | report_reasons 43 | stickied 44 | created 45 | score 46 | user_reports 47 | over_18 48 | controversiality 49 | link_id 50 | edited 51 | downs 52 | mod_reports 53 | author_flair_text 54 | created_utc 55 | parent_id 56 | body 57 | likes 58 | ups 59 | distinguished 60 | link_author 61 | removal_reason 62 | replies 63 | name 64 | archived 65 | link_title 66 | subreddit 67 | author 68 | subreddit_id 69 | saved 70 | num_reports 71 | id 72 | score_hidden 73 | approved_by 74 | link_url 75 | banned_by 76 | """ 77 | 78 | def new_comments(from, tag, article_id) do 79 | listing(from, tag, article_id, [sort: :new], @default_priority) 80 | end 81 | 82 | def new_comments(from, tag, article_id, options, priority) do 83 | options = Keyword.put(options, :sort, :new) 84 | listing(from, tag, article_id, options, priority) 85 | end 86 | 87 | def new_comments(from, tag, article_id, options) when is_list(options) do 88 | options = Keyword.put(options, :sort, :new) 89 | listing(from, tag, article_id, options, @default_priority) 90 | end 91 | 92 | def new_comments(from, tag, article_id, priority) do 93 | listing(from, tag, article_id, [sort: :new], priority) 94 | end 95 | 96 | defp listing(from, tag, article_id, options, priority) do 97 | url = "#{@comment_base}/#{article_id}" 98 | request_data = RequestBuilder.format_get(from, tag, url, options, :listing, priority) 99 | RequestQueue.enqueue_request(request_data) 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/api/identity.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.API.Identity do 2 | 3 | @moduledoc """ 4 | An interface for getting information about the authenticated user. 5 | """ 6 | 7 | alias ElixirPlusReddit.RequestQueue 8 | alias ElixirPlusReddit.RequestBuilder 9 | alias ElixirPlusReddit.Scheduler 10 | 11 | @identity __MODULE__ 12 | @identity_base "https://oauth.reddit.com/api/v1/me" 13 | @default_priority 0 14 | 15 | @doc """ 16 | Get information about the authenticated user. 17 | 18 | ### Parameters 19 | 20 | * `from`: The pid or name of the requester. 21 | * `tag`: Anything. 22 | * `priority`: The request's priority. (Optional) 23 | 24 | ### Fields 25 | 26 | comment_karma 27 | created 28 | created_utc 29 | gold_creddits 30 | gold_expiration 31 | has_mail 32 | has_mod_mail 33 | has_verified_email 34 | hide_from_robots 35 | id 36 | inbox_count 37 | is_gold 38 | is_mod 39 | is_suspended 40 | link_karma 41 | name 42 | over_18 43 | suspension_expiration_utc 44 | """ 45 | 46 | def self_data(from, tag, priority \\ @default_priority) do 47 | request_data = RequestBuilder.format_get(from, tag, @identity_base, :no_data, priority) 48 | RequestQueue.enqueue_request(request_data) 49 | end 50 | 51 | @doc """ 52 | Get the authenticated user's preferences. 53 | 54 | ### Parameters 55 | 56 | * `from`: The pid or name of the requester. 57 | * `tag`: Anything. 58 | * `priority`: The request's priority. (Optional) 59 | 60 | ### Fields 61 | 62 | highlight_controversial 63 | over_18 64 | enable_default_themes 65 | show_promote 66 | numsites 67 | private_feeds 68 | min_comment_score 69 | public_votes 70 | label_nsfw 71 | domain_details 72 | media 73 | hide_locationbar 74 | show_snoovatar 75 | show_flair 76 | monitor_mentions 77 | num_comments 78 | threaded_messages 79 | organic 80 | show_link_flair 81 | show_trending 82 | highlight_new_comments 83 | default_comment_sort 84 | compress 85 | min_link_score 86 | newwindow 87 | creddit_autorenew 88 | show_gold_expiration 89 | collapse_left_bar 90 | lang 91 | force_https 92 | media_preview 93 | store_visits 94 | no_profanity 95 | use_global_defaults 96 | content_langs 97 | hide_downs 98 | ignore_suggested_sort 99 | collapse_read_messages 100 | mark_messages_read 101 | research 102 | default_theme_sr 103 | threaded_modmail 104 | hide_ups 105 | clickgadget 106 | email_messages 107 | beta 108 | hide_ads 109 | show_stylesheets 110 | legacy_search 111 | public_server_seconds 112 | hide_from_robots 113 | """ 114 | 115 | def prefs(from, tag, priority \\ @default_priority) do 116 | url = "#{@identity_base}/prefs" 117 | request_data = RequestBuilder.format_get(from, tag, url, :no_data, priority) 118 | RequestQueue.enqueue_request(request_data) 119 | end 120 | 121 | @doc """ 122 | Get a list of the authenticated user's trophies. 123 | 124 | ### Parameters 125 | 126 | * `from`: The pid or name of the requester. 127 | * `tag`: Anything. 128 | * `priority`: The request's priority. (Optional) 129 | 130 | ### Fields 131 | 132 | award_id 133 | description 134 | icon_40 135 | icon_70 136 | id 137 | name 138 | url 139 | """ 140 | 141 | def trophies(from, tag, priority \\ @default_priority) do 142 | url = "#{@identity_base}/trophies" 143 | request_data = RequestBuilder.format_get(from, tag, url, :trophies, priority) 144 | RequestQueue.enqueue_request(request_data) 145 | end 146 | 147 | @doc """ 148 | Get information about the authenticated user on an interval. This is commonly used 149 | to check for periodically checking for new messages. 150 | 151 | ### Parameters 152 | 153 | * `interval`: A value in milliseconds. 154 | 155 | ### Other 156 | 157 | Refer to `Identity.self_data` documentation for other parameter and field information. 158 | """ 159 | 160 | def stream_self_data(from, tag, interval) do 161 | Scheduler.schedule({@identity, :self_data}, [from, tag, @default_priority], interval) 162 | end 163 | 164 | def stream_self_data(from, tag, priority, interval) do 165 | Scheduler.schedule({@identity, :self_data}, [from, tag, priority], interval) 166 | end 167 | 168 | end 169 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/api/post.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.API.Post do 2 | 3 | @moduledoc """ 4 | An interface for various post actions. These functions are generally 5 | delegated to. 6 | """ 7 | 8 | alias ElixirPlusReddit.RequestQueue 9 | alias ElixirPlusReddit.RequestBuilder 10 | 11 | @comment_endpoint "https://oauth.reddit.com/api/comment" 12 | @submit_endpoint "https://oauth.reddit.com/api/submit" 13 | @compose_endpoint "https://oauth.reddit.com/api/compose" 14 | @default_priority 0 15 | 16 | @doc """ 17 | Reply to a comment, submission or private message. 18 | 19 | ### Parameters 20 | 21 | * `from`: The pid or name of the requester. 22 | * `tag`: Anything. 23 | * `id`: A comma separated list of message ids. 24 | * `text`: The reply body. 25 | * `priority`: The request's priority. (Optional) 26 | 27 | ### Fields 28 | 29 | distinguished 30 | saved 31 | replies 32 | link_id 33 | report_reasons 34 | likes 35 | banned_by 36 | subreddit_id 37 | body_html 38 | archived? 39 | gilded 40 | score_hidden 41 | body 42 | created_utc 43 | ups 44 | stickied 45 | edited 46 | num_reports 47 | mod_reports 48 | approved_by 49 | errors 50 | user_reports 51 | name 52 | controversiality 53 | parent_id 54 | removal_reason 55 | author_flair_text 56 | created 57 | id 58 | downs 59 | author 60 | subreddit 61 | author_flair_css_class 62 | score 63 | """ 64 | 65 | def reply(from, tag, id, text, priority \\ @default_priority) do 66 | query = [api_type: :json, text: text, thing_id: id] 67 | request_data = RequestBuilder.format_post(from, tag, @comment_endpoint, query, :reply, priority) 68 | RequestQueue.enqueue_request(request_data) 69 | end 70 | 71 | 72 | @doc """ 73 | Submit a url submission. 74 | 75 | ### Parameters 76 | 77 | * `from`: The pid or name of the requester. 78 | * `tag`: Anything. 79 | * `subreddit`: The name of a subreddit. 80 | * `title`: The submission's title. 81 | * `url`: The submission's url. 82 | * `send_replies?`: Send submission replies to inbox. (true, false) 83 | * `priority`: The request's priority. (Optional) 84 | 85 | ### Fields 86 | 87 | id 88 | name 89 | url 90 | errors 91 | """ 92 | 93 | def submit_url(from, tag, subreddit, title, url, send_replies?, priority \\ @default_priority) do 94 | query = [ 95 | api_type: :json, 96 | sr: subreddit, 97 | resubmit: true, 98 | kind: :link, 99 | sendreplies: send_replies?, 100 | title: title, 101 | url: url 102 | ] 103 | request_data = RequestBuilder.format_post(from, tag, @submit_endpoint, query, :submission, priority) 104 | RequestQueue.enqueue_request(request_data) 105 | end 106 | 107 | @doc """ 108 | Submit a text submission. 109 | 110 | ### Parameters 111 | 112 | * `from`: The pid or name of the requester. 113 | * `tag`: Anything. 114 | * `subreddit`: The name of a subreddit. 115 | * `title`: The submission's title. 116 | * `text`: The submission's text. 117 | * `send_replies?`: Send submission replies to inbox. (true, false) 118 | * `priority`: The request's priority. (Optional) 119 | 120 | ### Fields 121 | 122 | id 123 | name 124 | url 125 | errors 126 | """ 127 | 128 | def submit_text(from, tag, subreddit, title, text, send_replies?, priority \\ @default_priority) do 129 | query = [ 130 | api_type: :json, 131 | sr: subreddit, 132 | resubmit: true, 133 | kind: :self, 134 | sendreplies: send_replies?, 135 | title: title, 136 | text: text 137 | ] 138 | request_data = RequestBuilder.format_post(from, tag, @submit_endpoint, query, :submission, priority) 139 | RequestQueue.enqueue_request(request_data) 140 | end 141 | 142 | @doc """ 143 | Start a new private conversation. 144 | 145 | ### Parameters 146 | 147 | * `from`: The pid or name of the requester. 148 | * `tag`: Anything. 149 | * `to`: The user receiving the message. 150 | * `subject`: The message's subject. 151 | * `text`: The message's text. 152 | * `priority`: The request's priority. (Optional) 153 | 154 | ### Fields 155 | errors 156 | """ 157 | 158 | def compose(from, tag, to, subject, text, priority \\ @default_priority) do 159 | query = [api_type: :json, to: to, subject: subject, text: text] 160 | request_data = RequestBuilder.format_post(from, tag, @compose_endpoint, query, :compose, priority) 161 | RequestQueue.enqueue_request(request_data) 162 | end 163 | 164 | end 165 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/api/inbox.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.API.Inbox do 2 | 3 | @moduledoc """ 4 | An interface for getting inbox related information and performing 5 | inbox related actions. 6 | """ 7 | 8 | alias ElixirPlusReddit.RequestQueue 9 | alias ElixirPlusReddit.RequestBuilder 10 | alias ElixirPlusReddit.Paginator 11 | 12 | @message_base "https://oauth.reddit.com/message" 13 | @mark_read_endpoint "https://oauth.reddit.com/api/read_message" 14 | @default_priority 0 15 | 16 | defdelegate compose(from, 17 | tag, 18 | to, 19 | subject, 20 | text), to: ElixirPlusReddit.API.Post 21 | 22 | defdelegate compose(from, 23 | tag, 24 | to, 25 | subject, 26 | text, 27 | priority), to: ElixirPlusReddit.API.Post 28 | 29 | defdelegate reply(from, 30 | tag, 31 | id, 32 | text), to: ElixirPlusReddit.API.Post 33 | 34 | defdelegate reply(from, 35 | tag, 36 | id, 37 | text, 38 | priority), to: ElixirPlusReddit.API.Post 39 | 40 | @doc """ 41 | Mark a message or list a messages as read. 42 | 43 | ### Parameters 44 | 45 | * `from`: The pid or name of the requester. 46 | * `tag`: Anything. 47 | * `id`: A comma separated list of message ids. 48 | * `priority`: The request's priority. (Optional) 49 | 50 | ### Fields 51 | empty map (%{}) 52 | """ 53 | 54 | def mark_read(from, tag, id, priority \\ @default_priority) do 55 | request_data = RequestBuilder.format_post(from, tag, @mark_read_endpoint, [id: id], :ok, priority) 56 | RequestQueue.enqueue_request(request_data) 57 | end 58 | 59 | @doc """ 60 | Get the authenticated user's inbox. 61 | 62 | ### Parameters 63 | 64 | * `from`: The pid or name of the requester. 65 | * `tag`: Anything. 66 | * `options`: The query. (Optional) 67 | * `priority`: The request's priority. (Optional) 68 | 69 | ### Options 70 | 71 | * `mark`: true or false 72 | * `limit`: a value between 1-100 73 | * `after`: a fullname of a thing 74 | * `before`: a fullname of a thing 75 | 76 | ### Fields 77 | 78 | after 79 | before 80 | modhash 81 | children: 82 | author 83 | body 84 | body_html 85 | context 86 | created 87 | created_utc 88 | dest 89 | distinguished 90 | first_message 91 | first_message_name 92 | id 93 | likes 94 | link_title 95 | name 96 | new 97 | parent_id 98 | replies 99 | subject 100 | subreddit 101 | """ 102 | 103 | def inbox(from, tag) do 104 | listing(from, tag, [], :inbox, @default_priority) 105 | end 106 | 107 | def inbox(from, tag, options) when is_list(options) do 108 | listing(from, tag, options, :inbox, @default_priority) 109 | end 110 | 111 | def inbox(from, tag, priority) do 112 | listing(from, tag, [], :inbox, priority) 113 | end 114 | 115 | @doc """ 116 | Get the authenticated user's unread inbox. 117 | 118 | ### Parameters 119 | 120 | * `from`: The pid or name of the requester. 121 | * `tag`: Anything. 122 | * `options`: The query. (Optional) 123 | * `priority`: The request's priority. (Optional) 124 | 125 | ### Options 126 | 127 | * `mark`: true or false 128 | * `limit`: a value between 1-100 129 | * `after`: a fullname of a thing 130 | * `before`: a fullname of a thing 131 | 132 | ### Fields 133 | 134 | after 135 | before 136 | modhash 137 | children: 138 | author 139 | body 140 | body_html 141 | context 142 | created 143 | created_utc 144 | dest 145 | distinguished 146 | first_message 147 | first_message_name 148 | id 149 | likes 150 | link_title 151 | name 152 | new 153 | parent_id 154 | replies 155 | subject 156 | subreddit 157 | """ 158 | 159 | def unread(from, tag) do 160 | listing(from, tag, [], :unread, @default_priority) 161 | end 162 | 163 | def unread(from, tag, options) when is_list(options) do 164 | listing(from, tag, options, :unread, @default_priority) 165 | end 166 | 167 | def unread(from, tag, priority) do 168 | listing(from, tag, [], :unread, priority) 169 | end 170 | 171 | def unread(from, tag, options, priority) do 172 | listing(from, tag, options, :unread, priority) 173 | end 174 | 175 | @doc """ 176 | Get the authenticated user's sent inbox. 177 | 178 | ### Parameters 179 | 180 | * `from`: The pid or name of the requester. 181 | * `tag`: Anything. 182 | * `options`: The query. (Optional) 183 | * `priority`: The request's priority. (Optional) 184 | 185 | ### Options 186 | 187 | * `mark`: true or false 188 | * `limit`: a value between 1-100 189 | * `after`: a fullname of a thing 190 | * `before`: a fullname of a thing 191 | 192 | ### Fields 193 | 194 | after 195 | before 196 | modhash 197 | children: 198 | author 199 | body 200 | body_html 201 | context 202 | created 203 | created_utc 204 | dest 205 | distinguished 206 | first_message 207 | first_message_name 208 | id 209 | likes 210 | link_titleb 211 | name 212 | new 213 | parent_id 214 | replies 215 | subject 216 | subreddit 217 | """ 218 | 219 | def sent(from, tag) do 220 | listing(from, tag, [], :sent, @default_priority) 221 | end 222 | 223 | def sent(from, tag, options) when is_list(options) do 224 | listing(from, tag, options, :sent, @default_priority) 225 | end 226 | 227 | def sent(from, tag, priority) do 228 | listing(from, tag, [], :sent, priority) 229 | end 230 | 231 | defp listing(from, tag, options, endpoint, priority) do 232 | url = "#{@message_base}/#{endpoint}" 233 | request_data = RequestBuilder.format_get(from, tag, url, options, :listing, priority) 234 | RequestQueue.enqueue_request(request_data) 235 | end 236 | 237 | end 238 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # EPR - ElixirPlusReddit 2 | 3 | EPR is a wrapper for the Reddit API in Elixir. EPR strives to provide simple access to Reddit's API and a flexible 4 | interface. 5 | 6 | ## Installation 7 | 8 | Soon! 9 | 10 | ## Setting up your first script 11 | 12 | *Setting up your script on Reddit* 13 | 14 | 15 | 16 | *Configuring your credentials* 17 | 18 | Credentials can either be set inside of `config.exs` or be set manually after EPR is started. 19 | 20 | ```elixir 21 | config :elixirplusreddit, :creds, [ 22 | username: "username", 23 | password: "password", 24 | client_id: "[client_id]", 25 | client_secret: "[secret]" 26 | ] 27 | 28 | config :elixirplusreddit, user_agent: "something meaningful" 29 | ``` 30 | 31 | or 32 | 33 | ```elixir 34 | Config.set_credentials("username", 35 | "password", 36 | "[client_id]", 37 | "[secret]", 38 | "something meaningful here") 39 | ````` 40 | 41 | 42 | ## A gentle introduction 43 | 44 | Now that you've got your script set up, let's talk a little bit about how EPR works so we can start talking to 45 | Reddit! Everytime you make a request it is sent to EPR's request server and stuck inside of a queue. It is issued 46 | and processed at a later time. This is how rate limiting is implemented, requests are issued on an interval that 47 | complies with Reddit's API terms. (*Currently static, hoping to support dymamic ratelimiting in the future*) 48 | All requests, no matter what data you are requesting, expect a pid or name `from` and a tag `tag`. All `from` is, 49 | is who is asking for the data so the request server knows who to send it back to. A tag can be anything and is simply 50 | a way to pattern match on responses when they are received. This may seem cumbersome now but it starts to become 51 | much more intuitive in practice. If you followed the installation instructions and your script is set up, 52 | jump to your project's directory and run `iex -S mix`, if not go read the installation instructions and set up your script! 53 | 54 | Before we do anything, I suggest defining a couple of aliases, this is solely for the sake of your fingers and is not mandatory. 55 | 56 | ```elixir 57 | iex(1)> alias ElixirPlusReddit.TokenServer 58 | nil 59 | iex(2)> alias ElixirPlusReddit.API.Identity 60 | nil 61 | ``` 62 | 63 | When EPR is started a token is automatically acquired for us and a new one is acquired automatically when necessary, 64 | this can easily be verified. *NOTE: This assumes that credentials were set inside of config.exs, if they were set manually 65 | call* `TokenServer.acquire_token` *first. All subsequent tokens will be acquired automatically.* 66 | 67 | ```elixir 68 | iex(4)> TokenServer.token 69 | "bearer xxxxxxxx-xxxxxxxxxxxxxxxxxxx_xxxxxxx" 70 | ``` 71 | 72 | Okay, good. Now that we know we're authenticated and ready to make requests, let's ask Reddit about ourselves! 73 | 74 | ```elixir 75 | iex(5)> Identity.self_data(self(), :me) 76 | :ok 77 | ``` 78 | 79 | I'm sure most people who are reading this already know that `self` is just the current process's pid, `:me` is 80 | the value that is going to accompany our response. Hey, these are actually the `from` and `tag` parameters that 81 | I mentioned earlier. So if you're wondering where the hell our response is, assuming that the message didn't 82 | take a wrong turn and an ample amount of time has passed, it's in our mailbox. Let's flush it out! 83 | 84 | ```elixir 85 | iex(6)> flush 86 | {:me, 87 | %{comment_karma: 8, 88 | created: 1453238732.0, 89 | created_utc: 1453209932.0, 90 | gold_creddits: 0, 91 | gold_expiration: nil, 92 | has_mail: false, 93 | has_mod_mail: false 94 | has_verified_email: true, 95 | hide_from_robots: false, 96 | id: "txy38", 97 | inbox_count: 0, 98 | is_gold: false, 99 | is_mod: false, 100 | is_suspended: false, 101 | link_karma: 14, 102 | name: "elixirplusreddit", 103 | over_18: false, 104 | suspension_expiration_utc: nil}} 105 | ``` 106 | 107 | Oh look, that's me. As you can see, all keys are stored as atoms, this allows them to be accessed with the dot syntax. Once again, 108 | you probably already knew that but I'll show you that later anyways. Right now we have a bigger problem! This isn't particularly 109 | useful if there's no way to grab the information out of our mailbox. You know... So we can do stuff with it? Let's write a 110 | little utility function to do exactly that and try again. 111 | 112 | ```elixir 113 | iex(7)> capture = fn(tag) -> 114 | ...(7)> receive do 115 | ...(7)> {^tag, response} -> response 116 | ...(7)> after 117 | ...(7)> 5000 -> "Nope, nothing, nothing at all." 118 | ...(7)> end 119 | ...(7)> end 120 | #Function 121 | iex(8)> Identity.self_data(self(), :me) 122 | :ok 123 | iex(9)> response = capture.(:me) 124 | %{comment_karma: 9, 125 | created: 1453238732.0, 126 | created_utc: 1453209932.0, 127 | gold_creddits: 0, 128 | gold_expiration: nil, 129 | has_mail: false, 130 | has_mod_mail: false, 131 | has_verified_email: true, 132 | hide_from_robots: false, 133 | id: "txy38", 134 | inbox_count: 0, 135 | is_gold: false, 136 | is_mod: false, 137 | is_suspended: false, 138 | link_karma: 14, 139 | name: "elixirplusreddit", 140 | over_18: false, 141 | suspension_expiration_utc: nil} 142 | iex(10)> IO.puts("Hey, look at me, I am #{response.name}!") 143 | Hey, look at me, I am elixirplusreddit! 144 | :ok 145 | iex(11)> response = capture.(:me) 146 | "Nope, nothing, nothing at all." # After five very dramatic seconds. 147 | ``` 148 | 149 | The anonymous function `capture` takes a tag and matches it against our mailbox, if it finds something within five seconds 150 | it grabs it, otherwise we're left with a disappointing message. That's pretty much the gist of it, honestly. If you understand this 151 | you already have the proper foundation for making neat stuff. Next we'll take a brief tour of EPR's most important bits 152 | and then finally put it to practice and write a (useless) bot! 153 | 154 | ## A non-comprehensive tour of EPR 155 | 156 | Here we'll take a look at some of EPR's different functionality and discuss certain implementation details. If you're following 157 | along I would suggest reconfiguring your iex session before hopping into the next part. 158 | 159 | ```elixir 160 | iex(12)> IEx.configure(inspect: [limit: 5]) 161 | ``` 162 | 163 | We're going to dealing with larger data and this will make it truncate earlier (default is 25) instead of flooding your terminal. 164 | 165 | #### Comments and submissions 166 | 167 | I think this is the most natural place to begin. No matter what you're writing, you're probably going to have to deal with 168 | gathering comments or submissions from a user or a subreddit. EPR offers a variety of functions for achieving this, so let's 169 | just jump right in and give it a try. Our goal is to gather the last 100 comments posted to /r/Elixir. 170 | 171 | ```elixir 172 | iex(13)> alias ElixirPlusReddit.API.Subreddit 173 | nil 174 | iex(14)> Subreddit.new_comments(self(), :elixir_comments, :elixir, [limit: 100]) 175 | :ok 176 | ``` 177 | 178 | As before, `self()` is where the response is going to be sent, `:elixir_comments` is the tag that'll come with the response. 179 | `:elixir` and `[limit: 100]` are what we're interested. `:elixir` is the name of the subreddit, which can either be an atom or 180 | a string. `[limit: 100]` is our query options. Options differ based on what data you're requesting. The `limit` option specifies 181 | how many items we want from the listing. It's important to note that 25 is the default and is used when the limit you specify is 182 | greater than 100 or less than 0. Options are entirely optional (*ha*) and can be omitted, in this case, as you would expect, the 183 | default values that Reddit's API specifies will be used. Now, let's grab our response and see what's up. 184 | 185 | ```elixir 186 | iex(15)> response = capture.(:elixir_comments) 187 | %{after: "t1_xxxxxxx", 188 | before: nil, 189 | children: [%{author_flair_css_class: nil, ...}, %{...}, ...], 190 | modhash: nil} 191 | ``` 192 | 193 | Inside of the `children` field is where all of the comments and their data reside. Each child has many fields but we're interested in 194 | the author of the comment and the comment itself. They are stored in the fields `author` and `body` respectively. Let's check out 195 | who's talking about what! 196 | 197 | ```elixir 198 | iex(16)> Enum.each(response.children, fn(child) -> 199 | ...(16)> IO.puts("#{child.author}: #{child.body}\n") 200 | ...(16)> end) 201 | ``` 202 | 203 | I'm not going to show you the output because it's going to be very large but it's that simple. Now what if we had to gather the `next` 204 | 100 comments? One way would be to take our last response's after id from the `after` field and include it in our next request 205 | as an option with the key `after`. Let's do that. 206 | 207 | ```elixir 208 | iex(17)> Subreddit.new_comments(self(), :elixir_comments, :elixir, [limit: 100, after: response.after]) 209 | :ok 210 | ``` 211 | 212 | What we have acheived here is pagination. In practice, you would never do it this way because EPR has built in pagination 213 | and streaming. I saw no reason to preclude manual pagination and it's nice to know what's happening behind the scenes. Okay, let's 214 | talk about pagination and streaming now. Actually, you should experiment a bit first! Try to get a user's top submissions and print 215 | the upvote count and submission title. Type `h ElixirPlusReddit.API.User.top_submissions` in your shell to get started. Seriously, 216 | moving on now. 217 | 218 | #### Pagination and streaming 219 | 220 | 221 | ##### Pagination 222 | 223 | Often times you'll want to get more than 100 items from a listing. As demonstrated in the previous section, it isn't difficult to 224 | manually pass around the after id but it is not necessary. Let's try paginating through a user's top comments and then talk about 225 | what's happening. 226 | 227 | ```elixir 228 | iex(18)> alias ElixirPlusReddit.API.User 229 | nil 230 | iex(19)> {:ok, pid} = User.paginate_top_comments(self(), :comments, :hutsboR) # I'm hutsboR! 231 | {:ok, #PID} 232 | ``` 233 | 234 | The first thing you probably noticed is that when we invoke a paginator function a pid is returned with the `:ok` atom. This is 235 | because paginators are implemented on top of genservers, they chug away at your request in a separate process and send you data as it 236 | becomes available. When there's no more data left to be acquired the genserver will gracefully shutdown and clean itself up. To 237 | see this in action repeatedly call `Process.is_alive?(pid)`. 238 | 239 | Another thing you probably noticed is that I didn't specify any options, most notably a limit. Paginators specify a default limit 240 | of 1000, which is also the most items that you can fetch from a listing. Understand that this is not a limit imposed by EPR but 241 | by Reddit. If the resource you're fetching from has less items than the limit, it will give you everything that it has to offer. 242 | Generous right? Anyways, let's see what's in our mailbox. 243 | 244 | ```elixir 245 | iex(20)> response = capture.(:comments) 246 | [%{user_reports: [], banned_by: nil, link_id: "t3_xxxxxx", ...}, 247 | %{user_reports: [], banned_by: nil, ...}, %{user_reports: [], ...}, %{...}, 248 | ...] 249 | ``` 250 | 251 | That's a list of the top 100 comments. An important difference in return structure relative to our example in the last section 252 | is that paginators don't bother to return the before and after ids. (and a couple other fields we don't need) It simply returns 253 | what we referred to as `children` before. That's only the first 100 comments though, right? More comments have probably been 254 | delivered to our mailbox by now. Let's just flush them out. 255 | 256 | ```elixir 257 | iex(21)> flush 258 | {:comments, 259 | [%{user_reports: [], banned_by: nil, link_id: "t3_xxxxxx", ...}, 260 | %{user_reports: [], banned_by: nil, ...}, %{user_reports: [], ...}, %{...}, 261 | ...]} 262 | {:comments, 263 | [%{user_reports: [], banned_by: nil, link_id: "t3_xxxxxx", ...}, 264 | %{user_reports: [], banned_by: nil, ...}, %{user_reports: [], ...}, %{...}, 265 | ...]} 266 | {:comments, 267 | [%{user_reports: [], banned_by: nil, link_id: "t3_xxxxxx", ...}, 268 | %{user_reports: [], banned_by: nil, ...}, %{user_reports: [], ...}, %{...}, 269 | ...]} 270 | {:comments, 271 | [%{user_reports: [], banned_by: nil, link_id: "t3_xxxxxx", ...}, 272 | %{user_reports: [], banned_by: nil, ...}, %{user_reports: [], ...}, %{...}, 273 | ...]} 274 | {:comments, 275 | [%{user_reports: [], banned_by: nil, link_id: "t3_xxxxxx", ...}, 276 | %{user_reports: [], banned_by: nil, ...}, %{user_reports: [], ...}, %{...}, 277 | ...]} 278 | {:comments, 279 | [%{user_reports: [], banned_by: nil, link_id: "t3_xxxxxx", ...}, 280 | %{user_reports: [], banned_by: nil, ...}, %{user_reports: [], ...}, %{...}, 281 | ...]} 282 | {:comments, :complete} 283 | :ok 284 | ``` 285 | 286 | There's the rest of the comments. Wait, what's that at the bottom? That's actually the interesting bit. When a paginator 287 | is done paginating it sends one last dying message to let us know that is it has completed it's sole duty. This is important 288 | because otherwise there is no convenient, reliable way to tell if we've received all the data that we've asked for. Now let's talk 289 | about streaming, a simple but useful feature. 290 | 291 | ##### Streaming 292 | 293 | Unlike pagination, streaming is simply requesting the *same* data indefinitely on an interval. Remember the first example we did in there 294 | introduction section where we got collected a little bit of data about ourselves by calling `Identity.self_data`? The response 295 | structure has a field called `has_mail` which lets us know if we have some form of mail. This includes username mentions, comment and submission replies, messages and so forth. Now what if we write a bot where it's critical to respond to mail and we need to 296 | periodically check for mail? Well let's just take care of that, luckily that's built in. 297 | 298 | ```elixir 299 | iex(22)> Identity.stream_self_data(self(), :me, 30000) 300 | {:ok, #PID} 301 | ``` 302 | 303 | Notice the function name `stream_self_data`, some functions have stream implementations built in and their names 304 | are generally preceded by `stream`. The argument `30000` is how often in milliseconds that the request 305 | should be made. Do we have mail? 306 | 307 | ```elixir 308 | iex(23)> capture.(:me).has_mail 309 | false 310 | ``` 311 | 312 | Nope, not this time. That's okay, we'll check again (*and again and again and again*) later. Another and probably the most common 313 | use case of streams is checking a subreddit for new comments that need attention. The `Subreddit` and `User` 314 | modules only have two functions for streaming comments and submissions and they fetch data with the `new` sort option. 315 | In other words, there's no built in support for streaming `hot`, `top` or other sortings. I might add 316 | built in support in the future but I figured that they have very limited uses unless you're streaming them on a large interval. 317 | As an example, let's say we need to check for new submissions to /r/Elixir. 318 | 319 | ```elixir 320 | iex(24)> Subreddit.stream_submissions(self(), :elixir_submissions, :elixir, [limit: 10], 1000 * 60 * 10) 321 | {:ok, #PID} 322 | ``` 323 | 324 | Now, every 10 minutes we'll get the 10 `newest` submissions. It's important to understand that this doesn't know which submissions 325 | we've seen and as a consequence there is a chance we will see the same submissions. It is the programmer's responsibility to provide 326 | a way to handle duplicate submissions. As a rule of thumb, use a smaller limit and larger interval for relatively inactive subreddits and 327 | users. Pretty simple right? Next we'll take a quick peek at writing custom paginators and streams. 328 | 329 | ##### Implementing custom paginators and streams 330 | 331 | Implementing your own paginators and streams isn't difficult. Let's pretend that we lived in a world where there was no way to 332 | automatically paginate a user's newest comments. We know that `User.new_comments` exists to get a single chunk of a user's comments. 333 | This alone is all we need to expand it into a paginator. Let's do that. 334 | 335 | ```elixir 336 | iex(25)> Paginator.paginate({User, :new_comments}, [self(), :my_comments, :hutsboR, [limit: 1000], 0]) 337 | {:ok, #PID} 338 | ``` 339 | 340 | Let's break down the arguments. The tuple contains two elements, the first being the module name and the second being the function name. 341 | So it reads "The function `new_comments` from the module `User`". This is the resource that we will be paginating from. The next argument 342 | is well, `new_comments`'s arguments. We already know that `self` and `:my_comments` are the pid and the tag. 343 | `:hutsboR` is the username, `[limit: 1000]` is the list of options and `0` is.. Well, pretend you didn't 344 | see that. We'll discuss that later. Your paginator will only work if the function you're trying to turn into a paginator returns a `listing`. 345 | The function's return structure must have the `after` and `before` fields. You can always type `h ElixirPlusReddit.API. ...` in your shell 346 | to quickly and conveniently find out. I'm sure I don't need to tell you this but typically this would be wrapped in a function opposed to 347 | manually providing the username and other arguments. In fact, this is exactly how `User.paginate_new_comments` is implemented. Makes sense. 348 | 349 | Okay, okay, let's write a custom stream now. Let's imagine (*seriously, imagine, this is contrived*) that for some reason you need to receive a 350 | list of a user's trophies every so often. As we did with our paginator, we can build this on top of the existing API function `Identity.trophies`. This will look quite similar to our paginator, take a look. 351 | 352 | ```elixir 353 | iex(26)> Scheduler.schedule({Identity, :trophies}, [self(), :my_comments, 0], 10000) 354 | {:ok, #PID} 355 | ``` 356 | 357 | Not too different, no? Again, `Identity` is the module and `:trophies` is the function. We know `self` is our pid and `:my_comments` is the tag 358 | we chose. Again, that weird `0` which we will ignore. Like the other streams we wrote we need to provide a millisecond interval that the function should be invoked on, that's our `10000` argument, as expected. Notice the module name, `Scheduler`. Streams are built on top of a generic 359 | scheduling implementation that simply issues API requests on an interval, no magic, no internal state, nothing. Moving on, let's talk about 360 | the `0` argument. 361 | 362 | 363 | #### The *priority* queue 364 | 365 | I mentioned the request queue way back in the introduction but what I *forgot* to mention is that it's actually a priority queue. What does 366 | that mean, though? It means that you can force important requests to be served immediately while other requests scoff at you for allowing them 367 | to be put on the back burner. So, how do we specify a priority? The `0`s that I buttered you up with in the previous section is the requests' priorities and the default priority that EPR uses for all requests. The priority is not required, hence why we have never specified it outside of last section. (Like I said, I had to butter you up... and I needed a smooth seque.) The last argument of all API requests is an optionial priority. Requests with the same priority are taken care of in the order that they are placed in the queue. 368 | Valid priorities are the values `-20` through `20`, where the **lower the value the higher priority**. ... No, you're reading that correctly. This 369 | convention is inherited from okeuday's [pqueue implementation](https://github.com/okeuday/pqueue) and is used in many other pqueue 370 | implementations. Why? I don't really know. This concept a little more difficult to illustrate than others but is possible with paginators. 371 | Time to get to work, I hope you're fast. 372 | 373 | ```elixir 374 | iex(27)> higher_priority = -1 375 | -1 376 | iex(28)> Subreddit.paginate_comments(self(), :pri_demo_one, :learnprogramming) 377 | {:ok, #PID} 378 | iex(29)> flush 379 | {:pri_demo_one, 380 | [%{stickied: false, from_id: nil, permalink: "...", ...}, ...]} 381 | :ok 382 | iex(30)> Subreddit.paginate_comments(self(), :pri_demo_two, :programming, higher_priority) 383 | {:ok, #PID} 384 | iex(31)> flush # After a little while... 385 | {:pri_demo_one, 386 | [%{stickied: false, from_id: nil, permalink: "...", ...}, ...]} 387 | {:pri_demo_two, 388 | [%{stickied: false, from_id: nil, permalink: "...", ...}, ...]} 389 | {:pri_demo_two, 390 | [%{stickied: false, from_id: nil, permalink: "...", ...}, ...]} 391 | . 392 | . 393 | . 394 | {:pri_demo_two, :complete} 395 | {:pri_demo_one, 396 | [%{stickied: false, from_id: nil, permalink: "...", ...}, ...]} 397 | . 398 | . 399 | . 400 | {:pri_demo_one, :complete} 401 | :ok 402 | ``` 403 | 404 | Like I said, a little difficult to illustrate and the output is a little messy but as you can see that we queued up `:pri_demo_one` first but 405 | because `:pri_demo_two` has a higher priority it finished first. Often times including a priority isn't important but one example of when it's 406 | useful is streaming on a short interval and you need to reply to a comment or submission immediately. When it becomes necessary you will probably 407 | recognize it quickly, so know don't forget that it exists! We're getting close to the end, let's continue! 408 | 409 | #### Replies, submissions and messages 410 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/api/subreddit.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.API.Subreddit do 2 | 3 | @moduledoc """ 4 | An interface for getting subreddit information. 5 | """ 6 | 7 | alias ElixirPlusReddit.RequestQueue 8 | alias ElixirPlusReddit.RequestBuilder 9 | alias ElixirPlusReddit.Paginator 10 | alias ElixirPlusReddit.Scheduler 11 | 12 | @subreddit __MODULE__ 13 | @subreddit_base "https://oauth.reddit.com/r" 14 | @default_priority 0 15 | 16 | defdelegate submit_url(from, 17 | tag, 18 | subreddit, 19 | title, 20 | url, 21 | send_replies?), to: ElixirPlusReddit.API.Post 22 | 23 | defdelegate submit_url(from, 24 | tag, 25 | subreddit, 26 | title, 27 | url, 28 | send_replies?, 29 | priority), to: ElixirPlusReddit.API.Post 30 | 31 | defdelegate submit_text(from, 32 | tag, 33 | subreddit, 34 | title, 35 | text, 36 | send_replies?), to: ElixirPlusReddit.API.Post 37 | 38 | defdelegate submit_text(from, 39 | tag, 40 | subreddit, 41 | title, 42 | text, 43 | send_replies?, 44 | priority), to: ElixirPlusReddit.API.Post 45 | 46 | @doc """ 47 | Get a subreddit's `hot` submissions. 48 | 49 | ### Parameters 50 | 51 | * `from`: The pid or name of the requester. 52 | * `tag`: Anything. 53 | * `subreddit`: A subreddit name or id. 54 | * `options`: The query. (Optional) 55 | * `priority`: The request's priority. (Optional) 56 | 57 | ### Options 58 | 59 | * `limit`: a value between 1-100 60 | * `after`: a fullname of a thing 61 | * `before`: a fullname of a thing 62 | 63 | ### Fields 64 | 65 | after 66 | before 67 | modhash 68 | children: 69 | link_flair_css_class 70 | author_flair_css_class 71 | thumbnail 72 | gilded 73 | quarantine 74 | report_reasons 75 | selftext_html 76 | stickied 77 | domain 78 | created 79 | score 80 | num_comments 81 | secure_media_embed 82 | user_reports 83 | over_18 84 | suggested_sort 85 | url 86 | edited 87 | downs 88 | mod_reports 89 | author_flair_text 90 | hide_score 91 | created_utc 92 | from_kind 93 | likes 94 | is_self 95 | ups 96 | distinguished 97 | media 98 | selftext 99 | removal_reason 100 | name 101 | link_flair_text 102 | from 103 | archived 104 | subreddit 105 | hidden 106 | locked 107 | author 108 | subreddit_id 109 | visited 110 | saved 111 | media_embed 112 | from_id 113 | num_reports 114 | id 115 | secure_media 116 | permalink 117 | approved_by 118 | title 119 | clicked 120 | banned_by 121 | """ 122 | 123 | def hot(from, tag, subreddit) do 124 | listing(from, tag, subreddit, [], :hot, @default_priority) 125 | end 126 | 127 | def hot(from, tag, subreddit, options, priority) do 128 | listing(from, tag, subreddit, options, :hot, priority) 129 | end 130 | 131 | def hot(from, tag, subreddit, options) when is_list(options) do 132 | listing(from, tag, subreddit, options, :hot, @default_priority) 133 | end 134 | 135 | def hot(from, tag, subreddit, priority) do 136 | listing(from, tag, subreddit, [], :hot, priority) 137 | end 138 | 139 | @doc """ 140 | Get a subreddit's `new` submissions. 141 | 142 | ### Parameters 143 | 144 | * `from`: The pid or name of the requester. 145 | * `tag`: Anything. 146 | * `subreddit`: A subreddit name or id. 147 | * `options`: The query. (Optional) 148 | * `priority`: The request's priority. (Optional) 149 | 150 | ### Options 151 | 152 | * `limit`: a value between 1-100 153 | * `after`: a fullname of a thing 154 | * `before`: a fullname of a thing 155 | 156 | ### Fields 157 | 158 | after 159 | before 160 | modhash 161 | children: 162 | link_flair_css_class 163 | author_flair_css_class 164 | thumbnail 165 | gilded 166 | quarantine 167 | report_reasons 168 | selftext_html 169 | stickied 170 | domain 171 | created 172 | score 173 | num_comments 174 | secure_media_embed 175 | user_reports 176 | over_18 177 | suggested_sort 178 | url 179 | edited 180 | downs 181 | mod_reports 182 | author_flair_text 183 | hide_score 184 | created_utc 185 | from_kind 186 | likes 187 | is_self 188 | ups 189 | distinguished 190 | media 191 | selftext 192 | removal_reason 193 | name 194 | link_flair_text 195 | from 196 | archived 197 | subreddit 198 | hidden 199 | locked 200 | author 201 | subreddit_id 202 | visited 203 | saved 204 | media_embed 205 | from_id 206 | num_reports 207 | id 208 | secure_media 209 | permalink 210 | approved_by 211 | title 212 | clicked 213 | banned_by 214 | """ 215 | 216 | def new(from, tag, subreddit) do 217 | listing(from, tag, subreddit, [], :new, @default_priority) 218 | end 219 | 220 | def new(from, tag, subreddit, options, priority) do 221 | listing(from, tag, subreddit, options, :new, priority) 222 | end 223 | 224 | def new(from, tag, subreddit, options) when is_list(options) do 225 | listing(from, tag, subreddit, options, :new, @default_priority) 226 | end 227 | 228 | def new(from, tag, subreddit, priority) do 229 | listing(from, tag, subreddit, [], :new, priority) 230 | end 231 | 232 | @doc """ 233 | Get a subreddit's `rising` submissions. 234 | 235 | ### Parameters 236 | 237 | * `from`: The pid or name of the requester. 238 | * `tag`: Anything. 239 | * `subreddit`: A subreddit name or id. 240 | * `options`: The query. (Optional) 241 | * `priority`: The request's priority. (Optional) 242 | 243 | ### Options 244 | 245 | * `limit`: a value between 1-100 246 | * `after`: a fullname of a thing 247 | * `before`: a fullname of a thing 248 | 249 | ### Fields 250 | 251 | after 252 | before 253 | modhash 254 | children: 255 | link_flair_css_class 256 | author_flair_css_class 257 | thumbnail 258 | gilded 259 | quarantine 260 | report_reasons 261 | selftext_html 262 | stickied 263 | domain 264 | created 265 | score 266 | num_comments 267 | secure_media_embed 268 | user_reports 269 | over_18 270 | suggested_sort 271 | url 272 | edited 273 | downs 274 | mod_reports 275 | author_flair_text 276 | hide_score 277 | created_utc 278 | from_kind 279 | likes 280 | is_self 281 | ups 282 | distinguished 283 | media 284 | selftext 285 | removal_reason 286 | name 287 | link_flair_text 288 | from 289 | archived 290 | subreddit 291 | hidden 292 | locked 293 | author 294 | subreddit_id 295 | visited 296 | saved 297 | media_embed 298 | from_id 299 | num_reports 300 | id 301 | secure_media 302 | permalink 303 | approved_by 304 | title 305 | clicked 306 | banned_by 307 | """ 308 | 309 | def rising(from, tag, subreddit) do 310 | listing(from, tag, subreddit, [], :rising, @default_priority) 311 | end 312 | 313 | def rising(from, tag, subreddit, options, priority) do 314 | listing(from, tag, subreddit, options, :rising, priority) 315 | end 316 | 317 | def rising(from, tag, subreddit, options) when is_list(options) do 318 | listing(from, tag, subreddit, options, :rising, @default_priority) 319 | end 320 | 321 | def rising(from, tag, subreddit, priority) do 322 | listing(from, tag, subreddit, [], :rising, priority) 323 | end 324 | 325 | @doc """ 326 | Get a subreddit's `controversial` submissions. 327 | 328 | ### Parameters 329 | 330 | * `from`: The pid or name of the requester. 331 | * `tag`: Anything. 332 | * `subreddit`: A subreddit name or id. 333 | * `options`: The query. (Optional) 334 | * `priority`: The request's priority. (Optional) 335 | 336 | ### Options 337 | 338 | * `limit`: a value between 1-100 339 | * `t`: hour, day, week, month, year, all 340 | * `after`: a fullname of a thing 341 | * `before`: a fullname of a thing 342 | 343 | ### Fields 344 | 345 | after 346 | before 347 | modhash 348 | children: 349 | link_flair_css_class 350 | author_flair_css_class 351 | thumbnail 352 | gilded 353 | quarantine 354 | report_reasons 355 | selftext_html 356 | stickied 357 | domain 358 | created 359 | score 360 | num_comments 361 | secure_media_embed 362 | user_reports 363 | over_18 364 | suggested_sort 365 | url 366 | edited 367 | downs 368 | mod_reports 369 | author_flair_text 370 | hide_score 371 | created_utc 372 | from_kind 373 | likes 374 | is_self 375 | ups 376 | distinguished 377 | media 378 | selftext 379 | removal_reason 380 | name 381 | link_flair_text 382 | from 383 | archived 384 | subreddit 385 | hidden 386 | locked 387 | author 388 | subreddit_id 389 | visited 390 | saved 391 | media_embed 392 | from_id 393 | num_reports 394 | id 395 | secure_media 396 | permalink 397 | approved_by 398 | title 399 | clicked 400 | banned_by 401 | """ 402 | 403 | def controversial(from, tag, subreddit) do 404 | listing(from, tag, subreddit, [], :controversial, @default_priority) 405 | end 406 | 407 | def controversial(from, tag, subreddit, options, priority) do 408 | listing(from, tag, subreddit, options, :controversial, priority) 409 | end 410 | 411 | def controversial(from, tag, subreddit, options) when is_list(options) do 412 | listing(from, tag, subreddit, options, :controversial, @default_priority) 413 | end 414 | 415 | def controversial(from, tag, subreddit, priority) do 416 | listing(from, tag, subreddit, [], :controversial, priority) 417 | end 418 | 419 | @doc """ 420 | Get a subreddit's `top` submissions. 421 | 422 | ### Parameters 423 | 424 | * `from`: The pid or name of the requester. 425 | * `tag`: Anything. 426 | * `subreddit`: A subreddit name or id. 427 | * `options`: The query. (Optional) 428 | * `priority`: The request's priority. (Optional) 429 | 430 | ### Options 431 | 432 | * `limit`: a value between 1-100 433 | * `t`: hour, day, week, month, year, all 434 | * `after`: a fullname of a thing 435 | * `before`: a fullname of a thing 436 | 437 | ### Fields 438 | 439 | after 440 | before 441 | modhash 442 | children: 443 | link_flair_css_class 444 | author_flair_css_class 445 | thumbnail 446 | gilded 447 | quarantine 448 | report_reasons 449 | selftext_html 450 | stickied 451 | domain 452 | created 453 | score 454 | num_comments 455 | secure_media_embed 456 | user_reports 457 | over_18 458 | suggested_sort 459 | url 460 | edited 461 | downs 462 | mod_reports 463 | author_flair_text 464 | hide_score 465 | created_utc 466 | from_kind 467 | likes 468 | is_self 469 | ups 470 | distinguished 471 | media 472 | selftext 473 | removal_reason 474 | name 475 | link_flair_text 476 | from 477 | archived 478 | subreddit 479 | hidden 480 | locked 481 | author 482 | subreddit_id 483 | visited 484 | saved 485 | media_embed 486 | from_id 487 | num_reports 488 | id 489 | secure_media 490 | permalink 491 | approved_by 492 | title 493 | clicked 494 | banned_by 495 | """ 496 | 497 | def top(from, tag, subreddit) do 498 | listing(from, tag, subreddit, [], :top, @default_priority) 499 | end 500 | 501 | def top(from, tag, subreddit, options, priority) do 502 | listing(from, tag, subreddit, options, :top, priority) 503 | end 504 | 505 | def top(from, tag, subreddit, options) when is_list(options) do 506 | listing(from, tag, subreddit, options, :top, @default_priority) 507 | end 508 | 509 | def top(from, tag, subreddit, priority) do 510 | listing(from, tag, subreddit, [], :top, priority) 511 | end 512 | 513 | @doc """ 514 | Get a subreddit's `gilded` submissions. 515 | 516 | ### Parameters 517 | 518 | * `from`: The pid or name of the requester. 519 | * `tag`: Anything. 520 | * `subreddit`: A subreddit name or id. 521 | * `options`: The query. (Optional) 522 | * `priority`: The request's priority. (Optional) 523 | 524 | ### Options 525 | 526 | * `limit`: a value between 1-100 527 | * `after`: a fullname of a thing 528 | * `before`: a fullname of a thing 529 | 530 | ### Fields 531 | 532 | after 533 | before 534 | modhash 535 | children: 536 | link_flair_css_class 537 | author_flair_css_class 538 | thumbnail 539 | gilded 540 | quarantine 541 | report_reasons 542 | selftext_html 543 | stickied 544 | domain 545 | created 546 | score 547 | num_comments 548 | secure_media_embed 549 | user_reports 550 | over_18 551 | suggested_sort 552 | url 553 | edited 554 | downs 555 | mod_reports 556 | author_flair_text 557 | hide_score 558 | created_utc 559 | from_kind 560 | likes 561 | is_self 562 | ups 563 | distinguished 564 | media 565 | selftext 566 | removal_reason 567 | name 568 | link_flair_text 569 | from 570 | archived 571 | subreddit 572 | hidden 573 | locked 574 | author 575 | subreddit_id 576 | visited 577 | saved 578 | media_embed 579 | from_id 580 | num_reports 581 | id 582 | secure_media 583 | permalink 584 | approved_by 585 | title 586 | clicked 587 | banned_by 588 | """ 589 | 590 | def gilded(from, tag, subreddit) do 591 | listing(from, tag, subreddit, [], :gilded, @default_priority) 592 | end 593 | 594 | def gilded(from, tag, subreddit, options, priority) do 595 | listing(from, tag, subreddit, options, :gilded, priority) 596 | end 597 | 598 | def gilded(from, tag, subreddit, options) when is_list(options) do 599 | listing(from, tag, subreddit, options, :gilded, @default_priority) 600 | end 601 | 602 | def gilded(from, tag, subreddit, priority) do 603 | listing(from, tag, subreddit, [], :gilded, priority) 604 | end 605 | 606 | @doc """ 607 | Get a subreddit's most recent comments. 608 | 609 | ### Parameters 610 | 611 | * `from`: The pid or name of the requester. 612 | * `tag`: Anything. 613 | * `subreddit:` A subreddit name or id. 614 | * `options`: The query. (Optional) 615 | * `priority`: The request's priority. (Optional) 616 | 617 | ### Options 618 | 619 | * `limit`: a value between 1-100 620 | * `after`: a fullname of a thing 621 | * `before`: a fullname of a thing 622 | 623 | ### Fields 624 | 625 | after 626 | before 627 | modhash 628 | children: 629 | author_flair_css_class 630 | gilded 631 | body_html 632 | quarantine 633 | report_reasons 634 | stickied 635 | created 636 | score 637 | user_reports 638 | over_18 639 | controversiality 640 | link_id 641 | edited 642 | downs 643 | mod_reports 644 | author_flair_text 645 | created_utc 646 | parent_id 647 | body 648 | likes 649 | ups 650 | distinguished 651 | link_author 652 | removal_reason 653 | replies 654 | name 655 | archived 656 | link_title 657 | subreddit 658 | author 659 | subreddit_id 660 | saved 661 | num_reports 662 | id 663 | score_hidden 664 | approved_by 665 | link_url 666 | banned_by 667 | """ 668 | 669 | def comments(from, tag, subreddit) do 670 | listing(from, tag, subreddit, [], :comments, @default_priority) 671 | end 672 | 673 | def comments(from, tag, subreddit, options, priority) do 674 | listing(from, tag, subreddit, options, :comments, priority) 675 | end 676 | 677 | def comments(from, tag, subreddit, options) when is_list(options) do 678 | listing(from, tag, subreddit, options, :comments, @default_priority) 679 | end 680 | 681 | def comments(from, tag, subreddit, priority) do 682 | listing(from, tag, subreddit, [], :comments, priority) 683 | end 684 | 685 | @doc """ 686 | Get a subreddit's new submissions on an interval. 687 | 688 | ### Parameters 689 | 690 | * `interval`: A value in milliseconds. 691 | 692 | ### Other 693 | 694 | Refer to `Subreddit.new_submissions` documentation for other parameter, option and field information. 695 | """ 696 | 697 | def stream_submissions(from, tag, subreddit, interval) do 698 | Scheduler.schedule({@subreddit, :new}, [from, tag, subreddit, [], @default_priority], interval) 699 | end 700 | 701 | def stream_submissions(from, tag, subreddit, options, interval) when is_list(options) do 702 | Scheduler.schedule({@subreddit, :new}, [from, tag, subreddit, options, @default_priority], interval) 703 | end 704 | 705 | def stream_submissions(from, tag, subreddit, priority, interval) do 706 | Scheduler.schedule({@subreddit, :new}, [from, tag, subreddit, [] , priority], interval) 707 | end 708 | 709 | def stream_submissions(from, tag, subreddit, options, priority, interval) do 710 | Scheduler.schedule({@subreddit, :new}, [from, tag, subreddit, options, priority], interval) 711 | end 712 | 713 | @doc """ 714 | Paginate a subreddit's `hot` submissions. Pagination does NOT return the `before` and `after` fields, only the 715 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 716 | and the pagination process is gracefully terminated. 717 | 718 | ### Options 719 | 720 | * `limit`: a value between 1-1000 721 | 722 | ### Other 723 | 724 | Refer to `Subreddit.hot_submissions` documentation for other parameter, option and field information. 725 | """ 726 | 727 | def paginate_hot(from, tag, subreddit) do 728 | Paginator.paginate({@subreddit, :hot}, [from, tag, subreddit, [limit: 1000], @default_priority]) 729 | end 730 | 731 | def paginate_hot(from, tag, subreddit, options) when is_list(options) do 732 | Paginator.paginate({@subreddit, :hot}, [from, tag, subreddit, put_limit(options), @default_priority]) 733 | end 734 | 735 | def paginate_hot(from, tag, subreddit, priority) do 736 | Paginator.paginate({@subreddit, :hot}, [from, tag, subreddit, [limit: 1000], priority]) 737 | end 738 | 739 | def paginate_hot(from, tag, subreddit, options, priority) do 740 | Paginator.paginate({@subreddit, :hot}, [from, tag, subreddit, put_limit(options), priority]) 741 | end 742 | 743 | @doc """ 744 | Paginate a subreddit's `new` submissions. Pagination does NOT return the `before` and `after` fields, only the 745 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 746 | and the pagination process is gracefully terminated. 747 | 748 | ### Options 749 | 750 | * `limit`: a value between 1-1000 751 | 752 | ### Other 753 | 754 | Refer to `Subreddit.new_submissions` documentation for other parameter, option and field information. 755 | """ 756 | 757 | def paginate_new(from, tag, subreddit) do 758 | Paginator.paginate({@subreddit, :new}, [subreddit, [from, tag, limit: 1000], @default_priority]) 759 | end 760 | 761 | def paginate_new(from, tag, subreddit, options) when is_list(options) do 762 | Paginator.paginate({@subreddit, :new}, [from, tag, subreddit, put_limit(options), @default_priority]) 763 | end 764 | 765 | def paginate_new(from, tag, subreddit, priority) do 766 | Paginator.paginate({@subreddit, :new}, [from, tag, subreddit, [limit: 1000], priority]) 767 | end 768 | 769 | def paginate_new(from, tag, subreddit, options, priority) do 770 | Paginator.paginate({@subreddit, :new}, [from, tag, subreddit, put_limit(options), priority]) 771 | end 772 | 773 | @doc """ 774 | Paginate a subreddit's `rising` submissions. Pagination does NOT return the `before` and `after` fields, only the 775 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 776 | and the pagination process is gracefully terminated. 777 | 778 | ### Options 779 | 780 | * `limit`: a value between 1-1000 781 | 782 | ### Other 783 | 784 | Refer to `Subreddit.rising_submissions` documentation for other parameter, option and field information. 785 | """ 786 | 787 | def paginate_rising(from, tag, subreddit) do 788 | Paginator.paginate({@subreddit, :rising}, [from, tag, subreddit, [limit: 1000], @default_priority]) 789 | end 790 | 791 | def paginate_rising(from, tag, subreddit, options) when is_list(options) do 792 | Paginator.paginate({@subreddit, :rising}, [from, tag, subreddit, put_limit(options), @default_priority]) 793 | end 794 | 795 | def paginate_rising(from, tag, subreddit, priority) do 796 | Paginator.paginate({@subreddit, :rising}, [from, tag, subreddit, [limit: 1000], priority]) 797 | end 798 | 799 | def paginate_rising(from, tag, subreddit, options, priority) do 800 | Paginator.paginate({@subreddit, :rising}, [from, tag, subreddit, put_limit(options), priority]) 801 | end 802 | 803 | @doc """ 804 | Paginate a subreddit's `controversial` submissions. Pagination does NOT return the `before` and `after` fields, only the 805 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 806 | and the pagination process is gracefully terminated. 807 | 808 | ### Options 809 | 810 | * `limit`: a value between 1-1000 811 | 812 | ### Other 813 | 814 | Refer to `Subreddit.controversial_submissions` documentation for other parameter, option and field information. 815 | """ 816 | 817 | def paginate_controversial(from, tag, subreddit) do 818 | Paginator.paginate({@subreddit, :controversial}, [from, tag, subreddit, [limit: 1000], @default_priority]) 819 | end 820 | 821 | def paginate_controversial(from, tag, subreddit, options) when is_list(options) do 822 | Paginator.paginate({@subreddit, :controversial}, [from, tag, subreddit, put_limit(options), @default_priority]) 823 | end 824 | 825 | def paginate_controversial(from, tag, subreddit, priority) do 826 | Paginator.paginate({@subreddit, :controversial}, [from, tag, subreddit, [limit: 1000], priority]) 827 | end 828 | 829 | def paginate_controversial(from, tag, subreddit, options, priority) do 830 | Paginator.paginate({@subreddit, :controversial}, [from, tag, subreddit, put_limit(options), priority]) 831 | end 832 | 833 | @doc """ 834 | Paginate a subreddit's `top` submissions. Pagination does NOT return the `before` and `after` fields, only the 835 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 836 | and the pagination process is gracefully terminated. 837 | 838 | ### Options 839 | 840 | * `limit`: a value between 1-1000 841 | 842 | ### Other 843 | 844 | Refer to `Subreddit.top_submissions` documentation for other parameter, option and field information. 845 | """ 846 | 847 | def paginate_top(from, tag, subreddit) do 848 | Paginator.paginate({@subreddit, :top}, [from, tag, subreddit, [limit: 1000], @default_priority]) 849 | end 850 | 851 | def paginate_top(from, tag, subreddit, options) when is_list(options) do 852 | Paginator.paginate({@subreddit, :top}, [from, tag, subreddit, put_limit(options), @default_priority]) 853 | end 854 | 855 | def paginate_top(from, tag, subreddit, priority) do 856 | Paginator.paginate({@subreddit, :top}, [from, tag, subreddit, [limit: 1000], priority]) 857 | end 858 | 859 | def paginate_top(from, tag, subreddit, options, priority) do 860 | Paginator.paginate({@subreddit, :top}, [from, tag, subreddit, put_limit(options), priority]) 861 | end 862 | 863 | @doc """ 864 | Paginate a subreddit's `gilded` submissions. Pagination does NOT return the `before` and `after` fields, only the 865 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 866 | and the pagination process is gracefully terminated. 867 | 868 | ### Options 869 | 870 | * `limit`: a value between 1-1000 871 | 872 | ### Other 873 | 874 | Refer to `Subreddit.gilded_submissions` documentation for other parameter, option and field information. 875 | """ 876 | 877 | def paginate_gilded(from, tag, subreddit) do 878 | Paginator.paginate({@subreddit, :gilded}, [from, tag, subreddit, [limit: 1000], @default_priority]) 879 | end 880 | 881 | def paginate_gilded(from, tag, subreddit, options) when is_list(options) do 882 | Paginator.paginate({@subreddit, :gilded}, [from, tag, subreddit, put_limit(options), @default_priority]) 883 | end 884 | 885 | def paginate_gilded(from, tag, subreddit, priority) do 886 | Paginator.paginate({@subreddit, :gilded}, [from, tag, subreddit, [limit: 1000], priority]) 887 | end 888 | 889 | def paginate_gilded(from, tag, subreddit, options, priority) do 890 | Paginator.paginate({@subreddit, :gilded}, [from, tag, subreddit, put_limit(options), priority]) 891 | end 892 | 893 | @doc """ 894 | Get a subreddit's new comments on an interval. 895 | 896 | ### Parameters 897 | 898 | * `interval`: A value in milliseconds. 899 | 900 | ### Other 901 | 902 | Refer to `Subreddit.new_submissions` documentation for other parameter, option and field information. 903 | """ 904 | 905 | 906 | def stream_comments(from, tag, subreddit, interval) do 907 | Scheduler.schedule({@subreddit, :comments}, [from, tag, subreddit, [], @default_priority], interval) 908 | end 909 | 910 | def stream_comments(from, tag, subreddit, options, interval) when is_list(options) do 911 | Scheduler.schedule({@subreddit, :comments}, [from, tag, subreddit, options, @default_priority], interval) 912 | end 913 | 914 | def stream_comments(from, tag, subreddit, priority, interval) do 915 | Scheduler.schedule({@subreddit, :comments}, [from, tag, subreddit, [] , priority], interval) 916 | end 917 | 918 | def stream_comments(from, tag, subreddit, options, priority, interval) do 919 | Scheduler.schedule({@subreddit, :comments}, [from, tag, subreddit, options, priority], interval) 920 | end 921 | 922 | @doc """ 923 | Paginate a subreddit's comments. Pagination does NOT return the `before` and `after` fields, only the 924 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 925 | and the pagination process is gracefully terminated. 926 | 927 | ### Options 928 | 929 | * `limit`: a value between 1-1000 930 | 931 | ### Other 932 | 933 | Refer to `Subreddit.comments` documentation for other parameter, option and field information. 934 | """ 935 | 936 | def paginate_comments(from, tag, subreddit) do 937 | Paginator.paginate({@subreddit, :comments}, [from, tag, subreddit, [limit: 1000], @default_priority]) 938 | end 939 | 940 | def paginate_comments(from, tag, subreddit, options) when is_list(options) do 941 | Paginator.paginate({@subreddit, :comments}, [from, tag, subreddit, put_limit(options), @default_priority]) 942 | end 943 | 944 | def paginate_comments(from, tag, subreddit, priority) do 945 | Paginator.paginate({@subreddit, :comments}, [from, tag, subreddit, [limit: 1000], priority]) 946 | end 947 | 948 | def paginate_comments(from, tag, subreddit, options, priority) do 949 | Paginator.paginate({@subreddit, :comments}, [from, tag, subreddit, put_limit(options), priority]) 950 | end 951 | 952 | defp put_limit(options) do 953 | case Keyword.has_key?(options, :limit) do 954 | true -> options 955 | false -> Keyword.put(options, :limit, 1000) 956 | end 957 | end 958 | 959 | defp listing(from, tag, subreddit, options, endpoint, priority) do 960 | url = "#{@subreddit_base}/#{subreddit}/#{endpoint}" 961 | request_data = RequestBuilder.format_get(from, tag, url, options, :listing, priority) 962 | RequestQueue.enqueue_request(request_data) 963 | end 964 | 965 | end 966 | -------------------------------------------------------------------------------- /lib/elixirplusreddit/api/user.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirPlusReddit.API.User do 2 | 3 | @moduledoc """ 4 | An interface for getting user information. 5 | """ 6 | 7 | alias ElixirPlusReddit.RequestBuilder 8 | alias ElixirPlusReddit.RequestQueue 9 | alias ElixirPlusReddit.Paginator 10 | alias ElixirPlusReddit.Scheduler 11 | 12 | @user __MODULE__ 13 | @user_base "https://oauth.reddit.com/user" 14 | @default_priority 0 15 | 16 | @doc """ 17 | Get a user's `hot` comments. 18 | 19 | ### Parameters 20 | 21 | * `from`: The pid or name of the requester. 22 | * `tag`: Anything. 23 | * `username:` A username or id. 24 | * `options`: The query. (Optional) 25 | * `priority`: The request's priority. (Optional) 26 | 27 | ### Options 28 | 29 | * `limit`: a value between 1-100 30 | * `after`: a fullname of a thing 31 | * `before`: a fullname of a thing 32 | 33 | ### Fields 34 | 35 | after 36 | before 37 | modhash 38 | children: 39 | author_flair_css_class 40 | gilded 41 | body_html 42 | quarantine 43 | report_reasons 44 | stickied 45 | created 46 | score 47 | user_reports 48 | over_18 49 | controversiality 50 | link_id 51 | edited 52 | downs 53 | mod_reports 54 | author_flair_text 55 | created_utc 56 | parent_id 57 | body 58 | likes 59 | ups 60 | distinguished 61 | link_author 62 | removal_reason 63 | replies 64 | name 65 | archived 66 | link_title 67 | subreddit 68 | author 69 | subreddit_id 70 | saved 71 | num_reports 72 | id 73 | score_hidden 74 | approved_by 75 | link_url 76 | banned_by 77 | """ 78 | 79 | def hot_comments(from, tag, username) do 80 | listing(from, tag, username, [sort: :hot], :comments, @default_priority) 81 | end 82 | 83 | def hot_comments(from, tag, username, options, priority) do 84 | options = Keyword.put(options, :sort, :hot) 85 | listing(from, tag, username, options, :comments, priority) 86 | end 87 | 88 | def hot_comments(from, tag, username, options) when is_list(options) do 89 | options = Keyword.put(options, :sort, :hot) 90 | listing(from, tag, username, options, :comments, @default_priority) 91 | end 92 | 93 | def hot_comments(from, tag, username, priority) do 94 | listing(from, tag, username, [sort: :hot], :comments, priority) 95 | end 96 | 97 | @doc """ 98 | Get a user's `new` comments. 99 | 100 | ### Parameters 101 | 102 | * `from`: The pid or name of the requester. 103 | * `tag`: Anything. 104 | * `username:` A username or id. 105 | * `options`: The query. (Optional) 106 | * `priority`: The request's priority. (Optional) 107 | 108 | ### Options 109 | 110 | * `limit`: a value between 1-100 111 | * `after`: a fullname of a thing 112 | * `before`: a fullname of a thing 113 | 114 | ### Fields 115 | 116 | after 117 | before 118 | modhash 119 | children: 120 | author_flair_css_class 121 | gilded 122 | body_html 123 | quarantine 124 | report_reasons 125 | stickied 126 | created 127 | score 128 | user_reports 129 | over_18 130 | controversiality 131 | link_id 132 | edited 133 | downs 134 | mod_reports 135 | author_flair_text 136 | created_utc 137 | parent_id 138 | body 139 | likes 140 | ups 141 | distinguished 142 | link_author 143 | removal_reason 144 | replies 145 | name 146 | archived 147 | link_title 148 | subreddit 149 | author 150 | subreddit_id 151 | saved 152 | num_reports 153 | id 154 | score_hidden 155 | approved_by 156 | link_url 157 | banned_by 158 | """ 159 | 160 | def new_comments(from, tag, username) do 161 | listing(from, tag, username, [sort: :new], :comments, @default_priority) 162 | end 163 | 164 | def new_comments(from, tag, username, options, priority) do 165 | options = Keyword.put(options, :sort, :new) 166 | listing(from, tag, username, options, :comments, priority) 167 | end 168 | 169 | def new_comments(from, tag, username, options) when is_list(options) do 170 | options = Keyword.put(options, :sort, :new) 171 | listing(from, tag, username, options, :comments, @default_priority) 172 | end 173 | 174 | def new_comments(from, tag, username, priority) do 175 | listing(from, tag, username, [sort: :new], :comments, priority) 176 | end 177 | 178 | @doc """ 179 | Get a user's `top` comments. 180 | 181 | ### Parameters 182 | 183 | * `from`: The pid or name of the requester. 184 | * `tag`: Anything. 185 | * `username:` A username or id. 186 | * `options`: The query. (Optional) 187 | * `priority`: The request's priority. (Optional) 188 | 189 | ### Options 190 | 191 | * `limit`: a value between 1-100 192 | * `t`: hour, day, week, month, year, all 193 | * `after`: a fullname of a thing 194 | * `before`: a fullname of a thing 195 | 196 | ### Fields 197 | 198 | after 199 | before 200 | modhash 201 | children: 202 | author_flair_css_class 203 | gilded 204 | body_html 205 | quarantine 206 | report_reasons 207 | stickied 208 | created 209 | score 210 | user_reports 211 | over_18 212 | controversiality 213 | link_id 214 | edited 215 | downs 216 | mod_reports 217 | author_flair_text 218 | created_utc 219 | parent_id 220 | body 221 | likes 222 | ups 223 | distinguished 224 | link_author 225 | removal_reason 226 | replies 227 | name 228 | archived 229 | link_title 230 | subreddit 231 | author 232 | subreddit_id 233 | saved 234 | num_reports 235 | id 236 | score_hidden 237 | approved_by 238 | link_url 239 | banned_by 240 | """ 241 | 242 | def top_comments(from, tag, username) do 243 | listing(from, tag, username, [sort: :top], :comments, @default_priority) 244 | end 245 | 246 | def top_comments(from, tag, username, options, priority) do 247 | options = Keyword.put(options, :sort, :top) 248 | listing(from, tag, username, options, :comments, priority) 249 | end 250 | 251 | def top_comments(from, tag, username, options) when is_list(options) do 252 | options = Keyword.put(options, :sort, :top) 253 | listing(from, tag, username, options, :comments, @default_priority) 254 | end 255 | 256 | def top_comments(from, tag, username, priority) do 257 | listing(from, tag, username, [sort: :top], :comments, priority) 258 | end 259 | 260 | @doc """ 261 | Get a user's `controversial` comments. 262 | 263 | ### Parameters 264 | 265 | * `from`: The pid or name of the requester. 266 | * `tag`: Anything. 267 | * `username:` A username or id. 268 | * `options`: The query. (Optional) 269 | * `priority`: The request's priority. (Optional) 270 | 271 | ### Options 272 | 273 | * `limit`: a value between 1-100 274 | * `t`: hour, day, week, month, year, all 275 | * `after`: a fullname of a thing 276 | * `before`: a fullname of a thing 277 | 278 | ### Fields 279 | 280 | after 281 | before 282 | modhash 283 | children: 284 | author_flair_css_class 285 | gilded 286 | body_html 287 | quarantine 288 | report_reasons 289 | stickied 290 | created 291 | score 292 | user_reports 293 | over_18 294 | controversiality 295 | link_id 296 | edited 297 | downs 298 | mod_reports 299 | author_flair_text 300 | created_utc 301 | parent_id 302 | body 303 | likes 304 | ups 305 | distinguished 306 | link_author 307 | removal_reason 308 | replies 309 | name 310 | archived 311 | link_title 312 | subreddit 313 | author 314 | subreddit_id 315 | saved 316 | num_reports 317 | id 318 | score_hidden 319 | approved_by 320 | link_url 321 | banned_by 322 | """ 323 | 324 | def controversial_comments(from, tag, username) do 325 | listing(from, tag, username, [sort: :controversial], :comments, @default_priority) 326 | end 327 | 328 | def controversial_comments(from, tag, username, options, priority) do 329 | options = Keyword.put(options, :sort, :controversial) 330 | listing(from, tag, username, options, :comments, priority) 331 | end 332 | 333 | def controversial_comments(from, tag, username, options) when is_list(options) do 334 | options = Keyword.put(options, :sort, :controversial) 335 | listing(from, tag, username, options, :comments, @default_priority) 336 | end 337 | 338 | def controversial_comments(from, tag, username, priority) do 339 | listing(from, tag, username, [sort: :controversial], :comments, priority) 340 | end 341 | 342 | @doc """ 343 | Get a user's `hot` submissions. 344 | 345 | ### Parameters 346 | 347 | * `from`: The pid or name of the requester. 348 | * `tag`: Anything. 349 | * `subreddit`: A subreddit name or id. 350 | * `options`: The query. (Optional) 351 | * `priority`: The request's priority. (Optional) 352 | 353 | ### Options 354 | 355 | * `limit`: a value between 1-100 356 | * `after`: a fullname of a thing 357 | * `before`: a fullname of a thing 358 | 359 | ### Fields 360 | 361 | after 362 | before 363 | modhash 364 | children: 365 | link_flair_css_class 366 | author_flair_css_class 367 | thumbnail 368 | gilded 369 | quarantine 370 | report_reasons 371 | selftext_html 372 | stickied 373 | domain 374 | created 375 | score 376 | num_comments 377 | secure_media_embed 378 | user_reports 379 | over_18 380 | suggested_sort 381 | url 382 | edited 383 | downs 384 | mod_reports 385 | author_flair_text 386 | hide_score 387 | created_utc 388 | from_kind 389 | likes 390 | is_self 391 | ups 392 | distinguished 393 | media 394 | selftext 395 | removal_reason 396 | name 397 | link_flair_text 398 | from 399 | archived 400 | subreddit 401 | hidden 402 | locked 403 | author 404 | subreddit_id 405 | visited 406 | saved 407 | media_embed 408 | from_id 409 | num_reports 410 | id 411 | secure_media 412 | permalink 413 | approved_by 414 | title 415 | clicked 416 | banned_by 417 | """ 418 | 419 | def hot_submissions(from, tag, username) do 420 | listing(from, tag, username, [sort: :hot], :submitted, @default_priority) 421 | end 422 | 423 | def hot_submissions(from, tag, username, options, priority) do 424 | options = Keyword.put(options, :sort, :hot) 425 | listing(from, tag, username, options, :submitted, priority) 426 | end 427 | 428 | def hot_submissions(from, tag, username, options) when is_list(options) do 429 | options = Keyword.put(options, :sort, :hot) 430 | listing(from, tag, username, options, :submitted, @default_priority) 431 | end 432 | 433 | def hot_submissions(from, tag, username, priority) do 434 | listing(from, tag, username, [sort: :hot], :submitted, priority) 435 | end 436 | 437 | @doc """ 438 | Get a user's `new` submissions. 439 | 440 | ### Parameters 441 | 442 | * `from`: The pid or name of the requester. 443 | * `tag`: Anything. 444 | * `subreddit`: A subreddit name or id. 445 | * `options`: The query. (Optional) 446 | * `priority`: The request's priority. (Optional) 447 | 448 | ### Options 449 | 450 | * `limit`: a value between 1-100 451 | * `after`: a fullname of a thing 452 | * `before`: a fullname of a thing 453 | 454 | ### Fields 455 | 456 | after 457 | before 458 | modhash 459 | children: 460 | link_flair_css_class 461 | author_flair_css_class 462 | thumbnail 463 | gilded 464 | quarantine 465 | report_reasons 466 | selftext_html 467 | stickied 468 | domain 469 | created 470 | score 471 | num_comments 472 | secure_media_embed 473 | user_reports 474 | over_18 475 | suggested_sort 476 | url 477 | edited 478 | downs 479 | mod_reports 480 | author_flair_text 481 | hide_score 482 | created_utc 483 | from_kind 484 | likes 485 | is_self 486 | ups 487 | distinguished 488 | media 489 | selftext 490 | removal_reason 491 | name 492 | link_flair_text 493 | from 494 | archived 495 | subreddit 496 | hidden 497 | locked 498 | author 499 | subreddit_id 500 | visited 501 | saved 502 | media_embed 503 | from_id 504 | num_reports 505 | id 506 | secure_media 507 | permalink 508 | approved_by 509 | title 510 | clicked 511 | banned_by 512 | """ 513 | 514 | def new_submissions(from, tag, username) do 515 | listing(from, tag, username, [sort: :new], :submitted, @default_priority) 516 | end 517 | 518 | def new_submissions(from, tag, username, options, priority) do 519 | options = Keyword.put(options, :sort, :new) 520 | listing(from, tag, username, options, :submitted, priority) 521 | end 522 | 523 | def new_submissions(from, tag, username, options) when is_list(options) do 524 | options = Keyword.put(options, :sort, :new) 525 | listing(from, tag, username, options, :submitted, @default_priority) 526 | end 527 | 528 | def new_submissions(from, tag, username, priority) do 529 | listing(from, tag, username, [sort: :hot], :submitted, priority) 530 | end 531 | 532 | @doc """ 533 | Get a user's `top` submissions. 534 | 535 | ### Parameters 536 | 537 | * `from`: The pid or name of the requester. 538 | * `tag`: Anything. 539 | * `subreddit`: A subreddit name or id. 540 | * `options`: The query. (Optional) 541 | * `priority`: The request's priority. (Optional) 542 | 543 | ### Options 544 | 545 | * `limit`: a value between 1-100 546 | * `t`: hour, day, week, month, year, all 547 | * `after`: a fullname of a thing 548 | * `before`: a fullname of a thing 549 | 550 | ### Fields 551 | 552 | after 553 | before 554 | modhash 555 | children: 556 | link_flair_css_class 557 | author_flair_css_class 558 | thumbnail 559 | gilded 560 | quarantine 561 | report_reasons 562 | selftext_html 563 | stickied 564 | domain 565 | created 566 | score 567 | num_comments 568 | secure_media_embed 569 | user_reports 570 | over_18 571 | suggested_sort 572 | url 573 | edited 574 | downs 575 | mod_reports 576 | author_flair_text 577 | hide_score 578 | created_utc 579 | from_kind 580 | likes 581 | is_self 582 | ups 583 | distinguished 584 | media 585 | selftext 586 | removal_reason 587 | name 588 | link_flair_text 589 | from 590 | archived 591 | subreddit 592 | hidden 593 | locked 594 | author 595 | subreddit_id 596 | visited 597 | saved 598 | media_embed 599 | from_id 600 | num_reports 601 | id 602 | secure_media 603 | permalink 604 | approved_by 605 | title 606 | clicked 607 | banned_by 608 | """ 609 | 610 | def top_submissions(from, tag, username) do 611 | listing(from, tag, username, [sort: :top], :submitted, @default_priority) 612 | end 613 | 614 | def top_submissions(from, tag, username, options, priority) do 615 | options = Keyword.put(options, :sort, :top) 616 | listing(from, tag, username, options, :submitted, priority) 617 | end 618 | 619 | def top_submissions(from, tag, username, options) when is_list(options) do 620 | options = Keyword.put(options, :sort, :top) 621 | listing(from, tag, username, options, :submitted, @default_priority) 622 | end 623 | 624 | def top_submissions(from, tag, username, priority) do 625 | listing(from, tag, username, [sort: :top], :submitted, priority) 626 | end 627 | 628 | @doc """ 629 | Get a user's `controversial` submissions. 630 | 631 | ### Parameters 632 | 633 | * `from`: The pid or name of the requester. 634 | * `tag`: Anything. 635 | * `subreddit`: A subreddit name or id. 636 | * `options`: The query. (Optional) 637 | * `priority`: The request's priority. (Optional) 638 | 639 | ### Options 640 | 641 | * `limit`: a value between 1-100 642 | * `t`: hour, day, week, month, year, all 643 | * `after`: a fullname of a thing 644 | * `before`: a fullname of a thing 645 | 646 | ### Fields 647 | 648 | after 649 | before 650 | modhash 651 | children: 652 | link_flair_css_class 653 | author_flair_css_class 654 | thumbnail 655 | gilded 656 | quarantine 657 | report_reasons 658 | selftext_html 659 | stickied 660 | domain 661 | created 662 | score 663 | num_comments 664 | secure_media_embed 665 | user_reports 666 | over_18 667 | suggested_sort 668 | url 669 | edited 670 | downs 671 | mod_reports 672 | author_flair_text 673 | hide_score 674 | created_utc 675 | from_kind 676 | likes 677 | is_self 678 | ups 679 | distinguished 680 | media 681 | selftext 682 | removal_reason 683 | name 684 | link_flair_text 685 | from 686 | archived 687 | subreddit 688 | hidden 689 | locked 690 | author 691 | subreddit_id 692 | visited 693 | saved 694 | media_embed 695 | from_id 696 | num_reports 697 | id 698 | secure_media 699 | permalink 700 | approved_by 701 | title 702 | clicked 703 | banned_by 704 | """ 705 | 706 | def controversial_submissions(from, tag, username) do 707 | listing(from, tag, username, [sort: :controversial], :submitted, @default_priority) 708 | end 709 | 710 | def controversial_submissions(from, tag, username, options, priority) do 711 | options = Keyword.put(options, :sort, :controversial) 712 | listing(from, tag, username, options, :submitted, priority) 713 | end 714 | 715 | def controversial_submissions(from, tag, username, options) when is_list(options) do 716 | options = Keyword.put(options, :sort, :controversial) 717 | listing(from, tag, username, options, :submitted, @default_priority) 718 | end 719 | 720 | def controversial_submissions(from, tag, username, priority) do 721 | listing(from, tag, username, [sort: :controversial], :submitted, priority) 722 | end 723 | 724 | @doc """ 725 | Get a user's `new` comments on an interval. 726 | 727 | ### Parameters 728 | 729 | * `interval`: A value in milliseconds. 730 | 731 | ### Other 732 | 733 | Refer to `User.new_comments` documentation for other parameter, option and field information. 734 | """ 735 | 736 | def stream_comments(from, tag, username, interval) do 737 | Scheduler.schedule({@user, :new_comments}, [from, tag, username, [], @default_priority], interval) 738 | end 739 | 740 | def stream_comments(from, tag, username, options, interval) when is_list(options) do 741 | Scheduler.schedule({@user, :new_comments}, [from, tag, username, options, @default_priority], interval) 742 | end 743 | 744 | def stream_comments(from, tag, username, priority, interval) do 745 | Scheduler.schedule({@user, :new_comments}, [from, tag, username, [] , priority], interval) 746 | end 747 | 748 | def stream_comments(from, tag, username, options, priority, interval) do 749 | Scheduler.schedule({@user, :new_comments}, [from, tag, username, options, priority], interval) 750 | end 751 | 752 | @doc """ 753 | Get a user's `new` submissions on an interval. 754 | 755 | ### Parameters 756 | 757 | * `interval`: A value in milliseconds. 758 | 759 | ### Other 760 | 761 | Refer to `User.submissions` documentation for other parameter, option and field information. 762 | """ 763 | 764 | def stream_submissions(from, tag, username, interval) do 765 | Scheduler.schedule({@user, :submissions}, [from, tag, username, [], @default_priority], interval) 766 | end 767 | 768 | def stream_submissions(from, tag, username, options, interval) when is_list(options) do 769 | Scheduler.schedule({@user, :submissions}, [from, tag, username, options, @default_priority], interval) 770 | end 771 | 772 | def stream_submissions(from, tag, username, priority, interval) do 773 | Scheduler.schedule({@user, :submissions}, [from, tag, username, [] , priority], interval) 774 | end 775 | 776 | def stream_submissions(from, tag, username, options, priority, interval) do 777 | Scheduler.schedule({@user, :submissions}, [from, tag, username, options, priority], interval) 778 | end 779 | 780 | @doc """ 781 | Paginate a user's `hot` comments. Pagination does NOT return the `before` and `after` fields, only the 782 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 783 | and the pagination process is gracefully terminated. 784 | 785 | ### Options 786 | 787 | * `limit`: a value between 1-1000 788 | 789 | ### Other 790 | 791 | Refer to `User.hot_comments` documentation for other parameter, option and field information. 792 | 793 | """ 794 | 795 | def paginate_hot_comments(from, tag, username) do 796 | Paginator.paginate({@user, :hot_comments}, [from, tag, username, [limit: 1000], @default_priority]) 797 | end 798 | 799 | def paginate_hot_comments(from, tag, username, options) when is_list(options) do 800 | Paginator.paginate({@user, :hot_comments}, [from, tag, username, put_limit(options), @default_priority]) 801 | end 802 | 803 | def paginate_hot_comments(from, tag, username, priority) do 804 | Paginator.paginate({@user, :hot_comments}, [from, tag, username, [limit: 1000], priority]) 805 | end 806 | 807 | def paginate_hot_comments(from, tag, username, options, priority) do 808 | Paginator.paginate({@user, :hot_comments}, [from, tag, username, put_limit(options), priority]) 809 | end 810 | 811 | @doc """ 812 | Paginate a user's `new` comments. Pagination does NOT return the `before` and `after` fields, only the 813 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 814 | and the pagination process is gracefully terminated. 815 | 816 | ### Options 817 | 818 | * `limit`: a value between 1-1000 819 | 820 | ### Other 821 | 822 | Refer to `User.new_comments` documentation for other parameter, option and field information. 823 | """ 824 | 825 | def paginate_new_comments(from, tag, username) do 826 | Paginator.paginate({@user, :new_comments}, [from, tag, username, [limit: 1000], @default_priority]) 827 | end 828 | 829 | def paginate_new_comments(from, tag, username, options) when is_list(options) do 830 | Paginator.paginate({@user, :new_comments}, [from, tag, username, put_limit(options), @default_priority]) 831 | end 832 | 833 | def paginate_new_comments(from, tag, username, priority) do 834 | Paginator.paginate({@user, :new_comments}, [from, tag, username, [limit: 1000], priority]) 835 | end 836 | 837 | def paginate_new_comments(from, tag, username, options, priority) do 838 | Paginator.paginate({@user, :new_comments}, [from, tag, username, put_limit(options), priority]) 839 | end 840 | 841 | @doc """ 842 | Paginate a user's `top` comments. Pagination does NOT return the `before` and `after` fields, only the 843 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 844 | and the pagination process is gracefully terminated. 845 | 846 | ### Options 847 | 848 | * `limit`: a value between 1-1000 849 | 850 | ### Other 851 | 852 | Refer to `User.top_comments` documentation for other parameter, option and field information. 853 | """ 854 | 855 | def paginate_top_comments(from, tag, username) do 856 | Paginator.paginate({@user, :top_comments}, [from, tag, username, [limit: 1000], @default_priority]) 857 | end 858 | 859 | def paginate_top_comments(from, tag, username, options) when is_list(options) do 860 | Paginator.paginate({@user, :top_comments}, [from, tag, username, put_limit(options), @default_priority]) 861 | end 862 | 863 | def paginate_top_comments(from, tag, username, priority) do 864 | Paginator.paginate({@user, :top_comments}, [from, tag, username, [limit: 1000], priority]) 865 | end 866 | 867 | def paginate_top_comments(from, tag, username, options, priority) do 868 | Paginator.paginate({@user, :top_comments}, [from, tag, username, put_limit(options), priority]) 869 | end 870 | 871 | @doc """ 872 | Paginate a user's `controversial` comments. Pagination does NOT return the `before` and `after` fields, only the 873 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 874 | and the pagination process is gracefully terminated. 875 | 876 | ### Options 877 | 878 | * `limit`: a value between 1-1000 879 | 880 | ### Other 881 | 882 | Refer to `User.controversial_comments` documentation for other parameter, option and field information. 883 | """ 884 | 885 | def paginate_controversial_comments(from, tag, username) do 886 | Paginator.paginate({@user, :controversial_comments}, [from, tag, username, [limit: 1000], @default_priority]) 887 | end 888 | 889 | def paginate_controversial_comments(from, tag, username, options) when is_list(options) do 890 | Paginator.paginate({@user, :controversial_comments}, [from, tag, username, put_limit(options), @default_priority]) 891 | end 892 | 893 | def paginate_controversial_comments(from, tag, username, priority) do 894 | Paginator.paginate({@user, :controversial_comments}, [from, tag, username, [limit: 1000], priority]) 895 | end 896 | 897 | def paginate_controversial_comments(from, tag, username, options, priority) do 898 | Paginator.paginate({@user, :controversial_comments}, [from, tag, username, put_limit(options), priority]) 899 | end 900 | 901 | @doc """ 902 | Paginate a user's `hot` submissions. Pagination does NOT return the `before` and `after` fields, only the 903 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 904 | and the pagination process is gracefully terminated. 905 | 906 | ### Options 907 | 908 | * `limit`: a value between 1-1000 909 | 910 | ### Other 911 | 912 | Refer to `User.hot_submissions` documentation for other parameter, option and field information. 913 | """ 914 | 915 | def paginate_hot_submissions(from, tag, username) do 916 | Paginator.paginate({@user, :hot_submissions}, [from, tag, username, [limit: 1000], @default_priority]) 917 | end 918 | 919 | def paginate_hot_submissions(from, tag, username, options) when is_list(options) do 920 | Paginator.paginate({@user, :hot_submissions}, [from, tag, username, put_limit(options), @default_priority]) 921 | end 922 | 923 | def paginate_hot_submissions(from, tag, username, priority) do 924 | Paginator.paginate({@user, :hot_submissions}, [from, tag, username, [limit: 1000], priority]) 925 | end 926 | 927 | def paginate_hot_submissions(from, tag, username, options, priority) do 928 | Paginator.paginate({@user, :hot_submissions}, [from, tag, username, put_limit(options), priority]) 929 | end 930 | 931 | @doc """ 932 | Paginate a user's `new` submissions. Pagination does NOT return the `before` and `after` fields, only the 933 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 934 | and the pagination process is gracefully terminated. 935 | 936 | ### Options 937 | 938 | * `limit`: a value between 1-1000 939 | 940 | ### Other 941 | 942 | Refer to `User.new_submissions` documentation for other parameter, option and field information. 943 | """ 944 | 945 | def paginate_new_submissions(from, tag, username) do 946 | Paginator.paginate({@user, :new_submissions}, [from, tag, username, [limit: 1000], @default_priority]) 947 | end 948 | 949 | def paginate_new_submissions(from, tag, username, options) when is_list(options) do 950 | Paginator.paginate({@user, :new_submissions}, [from, tag, username, put_limit(options), @default_priority]) 951 | end 952 | 953 | def paginate_new_submissions(from, tag, username, priority) do 954 | Paginator.paginate({@user, :new_submissions}, [from, tag, username, [limit: 1000], priority]) 955 | end 956 | 957 | def paginate_new_submissions(from, tag, username, options, priority) do 958 | Paginator.paginate({@user, :new_submissions}, [from, tag, username, put_limit(options), priority]) 959 | end 960 | 961 | @doc """ 962 | Paginate a user's `top` submissions. Pagination does NOT return the `before` and `after` fields, only the 963 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 964 | and the pagination process is gracefully terminated. 965 | 966 | ### Options 967 | 968 | * `limit`: a value between 1-1000 969 | 970 | ### Other 971 | 972 | Refer to `User.top_submissions` documentation for other parameter, option and field information. 973 | """ 974 | 975 | def paginate_top_submissions(from, tag, username) do 976 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, [limit: 1000], @default_priority]) 977 | end 978 | 979 | def paginate_top_submissions(from, tag, username, options) when is_list(options) do 980 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, put_limit(options), @default_priority]) 981 | end 982 | 983 | def paginate_top_submissions(from, tag, username, priority) do 984 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, [limit: 1000], priority]) 985 | end 986 | 987 | def paginate_top_submissions(from, tag, username, options, priority) do 988 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, put_limit(options), priority]) 989 | end 990 | 991 | @doc """ 992 | Paginate a user's `controversial` submissions. Pagination does NOT return the `before` and `after` fields, only the 993 | `children` field. When pagination is complete a message is sent to `from` in the form of {`tag`, `:complete`} 994 | and the pagination process is gracefully terminated. 995 | 996 | ### Options 997 | 998 | * `limit`: a value between 1-1000 999 | 1000 | ### Other 1001 | 1002 | Refer to `User.contorversial_submissions` documentation for other parameter, option and field information. 1003 | """ 1004 | 1005 | def paginate_controversial_submissions(from, tag, username) do 1006 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, [limit: 1000], @default_priority]) 1007 | end 1008 | 1009 | def paginate_controversial_submissions(from, tag, username, options) when is_list(options) do 1010 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, put_limit(options), @default_priority]) 1011 | end 1012 | 1013 | def paginate_controversial_submissions(from, tag, username, priority) do 1014 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, [limit: 1000], priority]) 1015 | end 1016 | 1017 | def paginate_controversial_submissions(from, tag, username, options, priority) do 1018 | Paginator.paginate({@user, :top_submissions}, [from, tag, username, put_limit(options), priority]) 1019 | end 1020 | 1021 | defp put_limit(options) do 1022 | case Keyword.has_key?(options, :limit) do 1023 | true -> options 1024 | false -> Keyword.put(options, :limit, 1000) 1025 | end 1026 | end 1027 | 1028 | defp listing(from, tag, username, options, endpoint, priority) do 1029 | url = "#{@user_base}/#{username}/#{endpoint}" 1030 | request_data = RequestBuilder.format_get(from, tag, url, options, :listing, priority) 1031 | RequestQueue.enqueue_request(request_data) 1032 | end 1033 | 1034 | end 1035 | --------------------------------------------------------------------------------