GenNNTP behaviour (GenNNTP v0.18.0) View Source

The NNTP client and server library.

This module provides both the behaviour for an NNTP server, and the client API to interact with a NNTP server.

Example

The GenNNTP behaviour abstracts the common NNTP client-server interaction. Developers are only required to implement the callbacks and functionality they are interested in to respond to client's commands, per specs.

Let's start with a code example and then explore the available callbacks. Imagine we want a NNTP server that serves content from a directory.

defmodule NServ do
  @behaviour GenNNTP

  # Callbacks

  @impl true
  def init(datadir) do
    {:ok, datadir}
  end

  @impl true
  def handle_CAPABILITIES(datadir) do
    {:ok, ["READER"], datadir}
  end

  @impl true
  def handle_HELP(datadir) do
    {:ok, "This is some help text", datadir}
  end

end

# Start the server
{:ok, pid} = GenNNTP.start(NServ, "priv", port: 6791)

# This is the client
{:ok, socket} = GenNNTP.connect("localhost", 6791)

{:ok, response} = GenNNTP.command(socket, "CAPABILITIES")
#=> {:ok, "101 Capability list:\r\nVERSION 2\r\nREADER"}

We start our NServ by calling start/3, passing the module with the server implementation and its initial argument (a path to a folder containing the data to serve from). We can primarily interact with the server by sending a command to the socket returned after connectiong to the server.

Every time you do a GenNNTP.command/2, the client will send a command that must be handled by one of the callbacks defined in the GenNNTP, based on the issued command. There are many callbckas to be implemented when you use a GenNNTP. The required callbacks are init/1, handle_CAPABILITIES/1 and handle_HELP/1. Other callbacks are optional in the sense that they are still required to be implemented when your server has the capability for them. For example, if your server has "READER" capability, you MUST provide these callbacks: handle_GROUP/2, handle_LISTGROUP/2, handle_NEXT/2, handle_LAST/2, handle_ARTICLE/2, handle_HEAD/2, handle_BODY/2, handle_STAT/2.

Link to this section Summary

Functions

Sends a command and receives server's response.

Starts a NNTP server with a callback module.

Stops a NNTP server by its reference.

Callbacks

Invoked when a client selects an article.

Invoked when a client selects body of an article.

Invoked when a client asks for the server's capabilities.

Invoked when a client selects a newsgroup.

Invoked when a client selects headers of an article.

Invoked when a client wants summary of the server.

Invoked when a client selects the previous article in the current newsgroup.

Invoked when a client selects a newsgroup.

Invoked when a client selects the next article in the current newsgroup.

Invoked when a client checks if an article exists.

Invoked when an uknown command is asked by the client.

Invoked when a client is connecting to the server.

Link to this section Types

Specs

option() :: :gen_nntp.option()

Link to this section Functions

Link to this function

command(socket, command, args \\ [])

View Source

Sends a command and receives server's response.

Link to this function

connect(address \\ "localhost", port \\ 119, options \\ [])

View Source

Connects to a NNTP server.

Link to this function

start(module, args, options)

View Source

Specs

start(module(), any(), [option()]) :: :gen_nntp.on_start()

Starts a NNTP server with a callback module.

Similar to starting a gen_server.

Specs

stop(module()) :: :ok

Stops a NNTP server by its reference.

The reference is usually the callback module.

Link to this section Callbacks

Link to this callback

handle_ARTICLE(arg, state)

View Source (optional)

Specs

handle_ARTICLE(arg, state()) ::
  {:ok, {number, :gen_nttp.article()}, state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when number: non_neg_integer(),
     arg: :gen_nttp.message_id() | {number, group :: String.t()}

Invoked when a client selects an article.

The arg is the argument used to specify the article to retrieve. It has 2 forms:

  • A message-id.
  • A 2-tuple containing the article number and the group name.

Returning {:ok, { number, article }, new_state} sends the new current article number and the entire article to the client and continues the loop with new state new_state. A full article has message-id, the headers, and a body.

Returning {:ok, false, new_state} sends 423 response to tell the client of unavailable article and continues the loop with new state new_state.

Returning {:error, reason} to respond with reason and closes the client.

Note that this callback is not invoked when currently selected newsgroup is invalid, or the current article number is invalid. GenNNTP handles those cases with appropriate response code. GenNNTP also handles the ARTICLE command with no argument, by taking the current article number instead before passing it to this callback.

Link to this callback

handle_BODY(arg, state)

View Source (optional)

Specs

handle_BODY(arg, state()) ::
  {:ok, {number, :gen_nttp.article()}, state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when number: non_neg_integer(),
     arg: :gen_nttp.message_id() | {number, group :: String.t()}

Invoked when a client selects body of an article.

This callback behaves identically to the handle_ARTICLE/2 except that only the body is returned in the article.

Link to this callback

handle_CAPABILITIES(state)

View Source

Specs

handle_CAPABILITIES(state()) :: {:ok, capabilities :: [String.t()], state()}

Invoked when a client asks for the server's capabilities.

state is the current state of the NNTP server.

The returning capabilities list is responded to the client, with "VERSION" always the first in the list. Server containues the loop with the new state.

Only standard capabilities are responded to the client. Invalid ones in the callback's return are ignored.

Link to this callback

handle_GROUP(group, state)

View Source (optional)

Specs

handle_GROUP(group, state()) ::
  {:ok,
   {group,
    [number: non_neg_integer(), low: non_neg_integer(), high: non_neg_integer()]},
   state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when group: String.t()

Invoked when a client selects a newsgroup.

group is the name of the newsgroup to be selected (e.g., "news.software").

Returning {:ok, group_summary, new_state} sends the group summary to client and continues the loop with new state new_state.

Returning {:ok, false, new_state} sends 411 response to tell the client of unavailable grop and continues the loop with new state new_state.

Returning {:error, reason} to respond with reason and closes the client.

Link to this callback

handle_HEAD(arg, state)

View Source (optional)

Specs

handle_HEAD(arg, state()) ::
  {:ok, {number, :gen_nttp.article()}, state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when number: non_neg_integer(),
     arg: :gen_nttp.message_id() | {number, group :: String.t()}

Invoked when a client selects headers of an article.

This callback behaves identically to the handle_ARTICLE/2 except that only the headers are returned in the article.

Specs

handle_HELP(state()) :: {:ok, help_text :: String.t(), state()}

Invoked when a client wants summary of the server.

Returning {:ok, help_test, new_state} to respond the help_text to the client and continues the loop with new state new_state.

Link to this callback

handle_LAST(arg, state)

View Source (optional)

Specs

handle_LAST(arg, state()) ::
  {:ok, {number, :gen_nttp.article()}, state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when number: non_neg_integer(),
     arg: :gen_nttp.message_id() | {number, group :: String.t()}

Invoked when a client selects the previous article in the current newsgroup.

The previous article in that newsgroup is the highest existing article number less than the current article number.

Returning {:ok, { number, article }, new_state} sends the new current article number and the message-id of that article to the client and continues the loop with new state new_state.

If number is the same as the current article number, a 422 is responded to tell the client of no next article in this group.

Returning {:ok, false, new_state} sends 422 response to tell the client of unavailable article and continues the loop with new state new_state.

Returning {:error, reason} to respond with reason and closes the client.

Note that this callback is not invoked when currently selected newsgroup is invalid, or the current article number is invalid. GenNNTP handles those cases with appropriate response code.

Link to this callback

handle_LISTGROUP(group, state)

View Source (optional)

Specs

handle_LISTGROUP(group, state()) ::
  {:ok,
   {group,
    [
      number: non_neg_integer(),
      low: non_neg_integer(),
      high: non_neg_integer(),
      numbers: [non_neg_integer()]
    ]}, state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when group: String.t()

Invoked when a client selects a newsgroup.

group is the name of the newsgroup to be selected (e.g., "news.software").

Returning {:ok, group_summary, new_state} sends the group summary to client and continues the loop with new state new_state. The group summary is similar to the one responded by "GROUP" command, but also has a list of article numbers in the newsgroup.

Returning {:ok, false, new_state} sends 411 response to tell the client of unavailable grop and continues the loop with new state new_state.

Returning {:error, reason} to respond with reason and closes the client.

Link to this callback

handle_NEXT(arg, state)

View Source (optional)

Specs

handle_NEXT(arg, state()) ::
  {:ok, {number, :gen_nttp.article()}, state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when number: non_neg_integer(),
     arg: :gen_nttp.message_id() | {number, group :: String.t()}

Invoked when a client selects the next article in the current newsgroup.

The next article in that newsgroup is the lowest existing article number greater than the current article number.

Returning {:ok, { number, article }, new_state} sends the new current article number and the message-id of that article to the client and continues the loop with new state new_state.

If number is the same as the current article number, a 421 is responded to tell the client of no next article in this group.

Returning {:ok, false, new_state} sends 421 response to tell the client of unavailable article and continues the loop with new state new_state.

Returning {:error, reason} to respond with reason and closes the client.

Note that this callback is not invoked when currently selected newsgroup is invalid, or the current article number is invalid. GenNNTP handles those cases with appropriate response code.

Link to this callback

handle_STAT(arg, state)

View Source (optional)

Specs

handle_STAT(arg, state()) ::
  {:ok, {number, :gen_nttp.article()}, state()}
  | {:ok, false, state()}
  | {:error, reason :: String.t(), state()}
when number: non_neg_integer(),
     arg: :gen_nttp.message_id() | {number, group :: String.t()}

Invoked when a client checks if an article exists.

This callback behaves identically to the handle_ARTICLE/2 except that only the message-id is returned in the article.

This callback allows the client to determine whether an article exists and in the second forms, what its message-id is, without having to process an arbitrary amount of text.

Link to this callback

handle_command(command, state)

View Source (optional)

Specs

handle_command(command :: String.t(), state()) ::
  {:reply, response :: any(), state()}
  | {:noreply, state()}
  | {:stop, reason :: any(), state()}
  | {:stop, reason :: any(), response :: any(), state()}

Invoked when an uknown command is asked by the client.

This optional callback can be used to handle commands not understood by the current GenNNTP implementation. If not defined, GenNNTP responds with 500.

The command is full command line sent by the client, minus the LFCR pair at the end. The callback can choose to reply or noreply to the command. The response does not need the ending LFCR pair.

Returning {:error, reason} to respond with reason and closes the client.

Specs

init(init_arg :: term()) ::
  {:ok, state()}
  | {:ok, state(), timeout()}
  | :ignore
  | {:stop, reason :: term()}

Invoked when a client is connecting to the server.

init_arg is the argument term (second argument) passed to start/3.

Returning {:ok, state} wll start the handshake to establish the socket.

Returning {:ok, state, timeout} is similar to {:ok, state}, except that it also sets a delay before establishing the handshake.

Returning :ignore will make the process exit normally without entering the loop, closing the socket.

Returning {:stop, reason} will cause the process to exit with reason reason without entering the loop, also closing the socket.