Skip to main content

netget/server/dict/
actions.rs

1//! DICT protocol actions: what the model is told, and how its answers become wire bytes.
2//!
3//! Every action is rendered by [`super::wire`], so the model supplies words, database names and
4//! free text and never a status line, a quote or a dot. The executor has no per-connection
5//! state; the one piece of state that changes rendering — `OPTION MIME` — is applied by the
6//! session loop afterwards (see `wire::apply_mime`).
7
8use super::wire;
9use crate::llm::actions::{
10    protocol_trait::{ActionResult, Protocol, Server},
11    ActionDefinition, Parameter,
12};
13use crate::protocol::log_template::LogTemplate;
14use crate::protocol::EventType;
15use crate::state::app_state::AppState;
16use anyhow::{anyhow, Context, Result};
17use serde_json::{json, Value};
18use std::sync::LazyLock;
19
20pub struct DictProtocol;
21
22impl DictProtocol {
23    pub fn new() -> Self {
24        Self
25    }
26}
27
28impl Default for DictProtocol {
29    fn default() -> Self {
30        Self::new()
31    }
32}
33
34impl Protocol for DictProtocol {
35    fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
36        vec![
37            crate::llm::actions::ParameterDefinition {
38                name: "first_byte_timeout_secs".to_string(),
39                type_hint: "number".to_string(),
40                description: "Seconds a connected peer may send no command after the 220 \
41                              banner before the server closes it. Default 300, the window a \
42                              `manual` rule gives a human to answer one event - the peer may be \
43                              NetGet's own TCP client with the banner parked for its operator. \
44                              Lower it for a listener exposed to strangers."
45                    .to_string(),
46                required: false,
47                example: json!(300),
48                default: Some(serde_json::json!(super::FIRST_COMMAND_TIMEOUT.as_secs())),
49            },
50            crate::llm::actions::ParameterDefinition {
51                name: "idle_timeout_secs".to_string(),
52                type_hint: "number".to_string(),
53                description: "Seconds the server waits for the next command after answering \
54                              one. Default 300: an interactive DICT session issues several \
55                              lookups down one connection, and dict(1) itself sends QUIT as \
56                              soon as it has its answer."
57                    .to_string(),
58                required: false,
59                example: json!(300),
60                default: Some(serde_json::json!(super::IDLE_TIMEOUT.as_secs())),
61            },
62        ]
63    }
64    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
65        Vec::new()
66    }
67    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
68        vec![
69            send_dict_definitions_action(),
70            send_dict_matches_action(),
71            send_dict_databases_action(),
72            send_dict_strategies_action(),
73            send_dict_text_action(),
74            send_dict_error_action(),
75            close_connection_action(),
76        ]
77    }
78    fn protocol_name(&self) -> &'static str {
79        "DICT"
80    }
81    fn get_event_types(&self) -> Vec<EventType> {
82        vec![
83            DICT_DEFINE_EVENT.clone(),
84            DICT_MATCH_EVENT.clone(),
85            DICT_SHOW_EVENT.clone(),
86        ]
87    }
88    fn stack_name(&self) -> &'static str {
89        "ETH>IP>TCP>DICT"
90    }
91    fn keywords(&self) -> Vec<&'static str> {
92        vec!["dict", "dictionary", "rfc 2229", "dict.org"]
93    }
94    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
95        use crate::protocol::metadata::{
96            DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
97        };
98
99        ProtocolMetadataV2::builder()
100            .state(DevelopmentState::Beta)
101            // 2628 is unprivileged, and so is every port a test picks.
102            .privilege_requirement(PrivilegeRequirement::None)
103            .well_known_port(2628)
104            .implementation(
105                "Hand-written RFC 2229 line protocol over tokio TCP: 220 banner with msg-id, \
106                 RFC 2229 parameter quoting, dot-stuffed text blocks and OPTION MIME rendered \
107                 by NetGet",
108            )
109            .llm_control(
110                "The dictionary: definitions (DEFINE), matches (MATCH), the database and \
111                 strategy lists, database and server information, and 5xx refusals",
112            )
113            .e2e_testing(
114                "tests/server/dict/real_client_test.rs drives the real dict(1) client \
115                 (dict 1.13, Debian/Ubuntu package `dict`, `brew install dict`): a lookup, \
116                 `-m -s prefix`, `-D`, `-S`, `-i`, `-I` and `-M`, asserting on what dict(1) \
117                 parsed and printed. The test fails, never skips, when the binary is absent. \
118                 tests/server/dict/e2e_test.rs covers the mocked-model path on a raw socket.",
119            )
120            .notes(
121                "Implements CLIENT, DEFINE, MATCH, SHOW DB/DATABASES, SHOW STRAT/STRATEGIES, \
122                 SHOW INFO, SHOW SERVER, STATUS, HELP, OPTION MIME and QUIT. AUTH, SASLAUTH and \
123                 SASLRESP answer 502; any other OPTION answers 503; an unknown command answers \
124                 500 without consulting the model. CLIENT, STATUS, HELP, OPTION and QUIT are \
125                 answered by NetGet itself. The model is the dictionary: there are no real \
126                 databases, and `!`/`*` are passed to the model as given. Command lines are \
127                 capped at the RFC's 1024 bytes including CRLF. On backend failure, a reply \
128                 that does not fit the command, or no reply at all, the peer gets `420 Server \
129                 temporarily unavailable` and the connection closes. No pcap oracle: this \
130                 Wireshark build has no DICT dissector.",
131            )
132            .max_inbound_bytes(wire::MAX_LINE_BYTES)
133            .build()
134    }
135    fn description(&self) -> &'static str {
136        "DICT dictionary server (RFC 2229) - the model is the dictionary"
137    }
138    fn example_prompt(&self) -> &'static str {
139        "DICT server on port 2628 - a dictionary of invented words, with a 'fantasy' database"
140    }
141    fn group_name(&self) -> &'static str {
142        "Core"
143    }
144    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
145        use crate::llm::actions::StartupExamples;
146        StartupExamples::new(
147            json!({
148                "type": "open_server",
149                "port": 2628,
150                "base_stack": "dict",
151                "instruction": "DICT server with one database 'fantasy' (Fantasy Lexicon) that \
152                                defines invented words in the style of an old encyclopedia"
153            }),
154            json!({
155                "type": "open_server",
156                "port": 2628,
157                "base_stack": "dict",
158                "event_handlers": [{
159                    "event_pattern": "dict_define",
160                    "handler": {
161                        "type": "script",
162                        "language": "python",
163                        "code": "import json, sys\nword = json.load(sys.stdin)['event'].get('word', '')\nprint(json.dumps({'actions': [{'type': 'send_dict_definitions', 'word': word, 'definitions': [{'database': 'echo', 'database_description': 'Echo Dictionary', 'text': word + '\\n  n. the word you asked about'}]}]}))"
164                    }
165                }, {
166                    "event_pattern": "dict_show",
167                    "handler": {
168                        "type": "static",
169                        "actions": [{
170                            "type": "send_dict_databases",
171                            "databases": [{"name": "echo", "description": "Echo Dictionary"}]
172                        }]
173                    }
174                }]
175            }),
176            json!({
177                "type": "open_server",
178                "port": 2628,
179                "base_stack": "dict",
180                "event_handlers": [{
181                    "event_pattern": "dict_define",
182                    "handler": {
183                        "type": "static",
184                        "actions": [{
185                            "type": "send_dict_definitions",
186                            "word": "netget",
187                            "definitions": [{
188                                "database": "tech",
189                                "database_description": "Technical Terms",
190                                "text": "netget\n  n. a server whose every reply is decided by a model"
191                            }]
192                        }]
193                    }
194                }]
195            }),
196        )
197    }
198}
199
200impl Server for DictProtocol {
201    fn spawn(
202        &self,
203        ctx: crate::protocol::SpawnContext,
204    ) -> std::pin::Pin<
205        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
206    > {
207        Box::pin(async move {
208            let secs = |name: &str| -> anyhow::Result<Option<u64>> {
209                Ok(ctx
210                    .startup_params
211                    .as_ref()
212                    .map(|p| p.get_optional_u64(name))
213                    .transpose()?
214                    .flatten())
215            };
216            let first_byte_timeout_secs = secs("first_byte_timeout_secs")?;
217            let idle_timeout_secs = secs("idle_timeout_secs")?;
218
219            crate::server::dict::DictServer::spawn_with_llm_actions(
220                ctx.legacy_listen_addr(),
221                ctx.llm_client,
222                ctx.state,
223                ctx.status_tx,
224                ctx.server_id,
225                first_byte_timeout_secs,
226                idle_timeout_secs,
227            )
228            .await
229        })
230    }
231
232    fn execute_action(&self, action: Value) -> Result<ActionResult> {
233        let action_type = action
234            .get("type")
235            .and_then(|v| v.as_str())
236            .context("Missing 'type' field in action")?;
237
238        let rendered = match action_type {
239            "send_dict_definitions" => render_definitions_action(&action)?,
240            "send_dict_matches" => render_matches_action(&action)?,
241            "send_dict_databases" => {
242                wire::render_databases(&pairs(&action, "databases", "name", "description")?)
243            }
244            "send_dict_strategies" => {
245                wire::render_strategies(&pairs(&action, "strategies", "name", "description")?)
246            }
247            "send_dict_text" => {
248                let code = code_of(&action)?;
249                let text = action.get("text").and_then(Value::as_str).unwrap_or("");
250                wire::render_text(code, text).ok_or_else(|| {
251                    anyhow!(
252                        "send_dict_text code must be 112 (SHOW INFO), 113 (HELP) or 114 \
253                         (SHOW SERVER), got {code}"
254                    )
255                })?
256            }
257            "send_dict_error" => {
258                let code = code_of(&action)?;
259                let message = action.get("message").and_then(Value::as_str).unwrap_or("");
260                wire::render_error(code, message).ok_or_else(|| {
261                    let allowed: Vec<String> = wire::ERROR_CODES
262                        .iter()
263                        .map(|(c, _)| c.to_string())
264                        .collect();
265                    anyhow!(
266                        "send_dict_error code must be one of {}, got {code}",
267                        allowed.join(", ")
268                    )
269                })?
270            }
271            "close_connection" => return Ok(ActionResult::CloseConnection),
272            _ => return Err(anyhow!("Unknown DICT action: {}", action_type)),
273        };
274        Ok(ActionResult::Output(rendered.into_bytes()))
275    }
276}
277
278fn code_of(action: &Value) -> Result<u16> {
279    let code = action
280        .get("code")
281        .and_then(|v| {
282            v.as_u64()
283                .or_else(|| v.as_str().and_then(|s| s.trim().parse().ok()))
284        })
285        .context("Missing or non-numeric 'code' parameter")?;
286    u16::try_from(code).map_err(|_| anyhow!("code {code} is not a DICT status code"))
287}
288
289fn str_field<'a>(item: &'a Value, key: &str) -> &'a str {
290    item.get(key).and_then(Value::as_str).unwrap_or("")
291}
292
293/// `[{<name_key>, <desc_key>}]` → pairs, refusing an entry with no name — an empty atom would
294/// render as `""`, a listing line naming nothing.
295fn pairs(
296    action: &Value,
297    list: &str,
298    name_key: &str,
299    desc_key: &str,
300) -> Result<Vec<(String, String)>> {
301    let items = match action.get(list) {
302        None | Some(Value::Null) => return Ok(Vec::new()),
303        Some(v) => v
304            .as_array()
305            .with_context(|| format!("'{list}' must be an array"))?,
306    };
307    items
308        .iter()
309        .map(|item| {
310            let name = str_field(item, name_key).trim();
311            if name.is_empty() {
312                return Err(anyhow!(
313                    "every entry in '{list}' needs a non-empty '{name_key}'"
314                ));
315            }
316            Ok((name.to_string(), str_field(item, desc_key).to_string()))
317        })
318        .collect()
319}
320
321fn render_definitions_action(action: &Value) -> Result<String> {
322    let default_word = action.get("word").and_then(Value::as_str).unwrap_or("");
323    let items = match action.get("definitions") {
324        None | Some(Value::Null) => Vec::new(),
325        Some(v) => v
326            .as_array()
327            .context("'definitions' must be an array")?
328            .clone(),
329    };
330    let mut definitions = Vec::with_capacity(items.len());
331    for item in &items {
332        let word = item
333            .get("word")
334            .and_then(Value::as_str)
335            .unwrap_or(default_word);
336        if word.trim().is_empty() {
337            return Err(anyhow!(
338                "send_dict_definitions needs 'word' (the headword each 151 line names) - copy \
339                 it from the dict_define event"
340            ));
341        }
342        let database = str_field(item, "database").trim();
343        if database.is_empty() {
344            return Err(anyhow!("every definition needs a non-empty 'database'"));
345        }
346        definitions.push(wire::Definition {
347            word: word.to_string(),
348            database: database.to_string(),
349            database_description: str_field(item, "database_description").to_string(),
350            text: str_field(item, "text").to_string(),
351        });
352    }
353    Ok(wire::render_definitions(&definitions))
354}
355
356fn render_matches_action(action: &Value) -> Result<String> {
357    let matches = pairs(action, "matches", "database", "word")?;
358    for (_, word) in &matches {
359        if word.trim().is_empty() {
360            return Err(anyhow!("every match needs a non-empty 'word'"));
361        }
362    }
363    Ok(wire::render_matches(&matches))
364}
365
366fn send_dict_definitions_action() -> ActionDefinition {
367    ActionDefinition {
368        name: "send_dict_definitions".to_string(),
369        description: "Answer DEFINE with one or more definitions. NetGet writes the 150/151/250 \
370                      status lines and the dot-terminated text blocks; give plain text. An \
371                      empty 'definitions' list answers 552 (no match)."
372            .to_string(),
373        parameters: vec![
374            Parameter {
375                name: "word".to_string(),
376                type_hint: "string".to_string(),
377                description: "The headword the definitions are for - normally the event's \
378                              'word'. A definition may override it with its own 'word'."
379                    .to_string(),
380                required: true,
381            },
382            Parameter {
383                name: "definitions".to_string(),
384                type_hint: "array".to_string(),
385                description: "Array of {database, database_description, text}: the database \
386                              name (a short atom such as 'wn'), its human description, and the \
387                              definition text (may span several lines)."
388                    .to_string(),
389                required: true,
390            },
391        ],
392        example: json!({
393            "type": "send_dict_definitions",
394            "word": "serendipity",
395            "definitions": [{
396                "database": "wn",
397                "database_description": "WordNet (r) 3.0",
398                "text": "serendipity\n    n 1: good luck in making unexpected discoveries"
399            }]
400        }),
401        log_template: Some(
402            LogTemplate::new()
403                .with_info("-> DICT definitions of {word}")
404                .with_debug("DICT send_dict_definitions: word={word}"),
405        ),
406    }
407}
408
409fn send_dict_matches_action() -> ActionDefinition {
410    ActionDefinition {
411        name: "send_dict_matches".to_string(),
412        description: "Answer MATCH with the words that match. NetGet writes the 152 block and \
413                      the 250. An empty 'matches' list answers 552 (no match)."
414            .to_string(),
415        parameters: vec![Parameter {
416            name: "matches".to_string(),
417            type_hint: "array".to_string(),
418            description: "Array of {database, word}".to_string(),
419            required: true,
420        }],
421        example: json!({
422            "type": "send_dict_matches",
423            "matches": [
424                {"database": "wn", "word": "serene"},
425                {"database": "wn", "word": "serendipity"}
426            ]
427        }),
428        log_template: Some(
429            LogTemplate::new()
430                .with_info("-> DICT matches")
431                .with_debug("DICT send_dict_matches"),
432        ),
433    }
434}
435
436fn send_dict_databases_action() -> ActionDefinition {
437    ActionDefinition {
438        name: "send_dict_databases".to_string(),
439        description: "Answer SHOW DB with the databases this dictionary offers (110 block). An \
440                      empty list answers 554 (no databases present)."
441            .to_string(),
442        parameters: vec![Parameter {
443            name: "databases".to_string(),
444            type_hint: "array".to_string(),
445            description: "Array of {name, description}".to_string(),
446            required: true,
447        }],
448        example: json!({
449            "type": "send_dict_databases",
450            "databases": [
451                {"name": "wn", "description": "WordNet (r) 3.0"},
452                {"name": "jargon", "description": "The Jargon File"}
453            ]
454        }),
455        log_template: Some(
456            LogTemplate::new()
457                .with_info("-> DICT database list")
458                .with_debug("DICT send_dict_databases"),
459        ),
460    }
461}
462
463fn send_dict_strategies_action() -> ActionDefinition {
464    ActionDefinition {
465        name: "send_dict_strategies".to_string(),
466        description: "Answer SHOW STRAT with the match strategies this dictionary supports \
467                      (111 block). An empty list answers 555 (no strategies available)."
468            .to_string(),
469        parameters: vec![Parameter {
470            name: "strategies".to_string(),
471            type_hint: "array".to_string(),
472            description: "Array of {name, description}".to_string(),
473            required: true,
474        }],
475        example: json!({
476            "type": "send_dict_strategies",
477            "strategies": [
478                {"name": "exact", "description": "Match headwords exactly"},
479                {"name": "prefix", "description": "Match prefixes"}
480            ]
481        }),
482        log_template: Some(
483            LogTemplate::new()
484                .with_info("-> DICT strategy list")
485                .with_debug("DICT send_dict_strategies"),
486        ),
487    }
488}
489
490fn send_dict_text_action() -> ActionDefinition {
491    ActionDefinition {
492        name: "send_dict_text".to_string(),
493        description: "Answer SHOW INFO <db> (code 112) or SHOW SERVER (code 114) with free \
494                      text. NetGet writes the status line, the dot-terminated block and the 250."
495            .to_string(),
496        parameters: vec![
497            Parameter {
498                name: "code".to_string(),
499                type_hint: "number".to_string(),
500                description: "112 for SHOW INFO, 114 for SHOW SERVER (113 is HELP)".to_string(),
501                required: true,
502            },
503            Parameter {
504                name: "text".to_string(),
505                type_hint: "string".to_string(),
506                description: "The information text; may span several lines".to_string(),
507                required: true,
508            },
509        ],
510        example: json!({
511            "type": "send_dict_text",
512            "code": 112,
513            "text": "WordNet 3.0\nA lexical database of English."
514        }),
515        log_template: Some(
516            LogTemplate::new()
517                .with_info("-> DICT {code} text")
518                .with_debug("DICT send_dict_text: code={code}"),
519        ),
520    }
521}
522
523fn send_dict_error_action() -> ActionDefinition {
524    ActionDefinition {
525        name: "send_dict_error".to_string(),
526        description: "Refuse the command with a DICT 5xx status: 550 unknown database, 551 \
527                      unknown strategy, 552 no match, 554 no databases, 555 no strategies, \
528                      530 access denied, 501 bad parameters, 502 not implemented. Any other \
529                      code is rejected."
530            .to_string(),
531        parameters: vec![
532            Parameter {
533                name: "code".to_string(),
534                type_hint: "number".to_string(),
535                description: "A 5xx DICT status code".to_string(),
536                required: true,
537            },
538            Parameter {
539                name: "message".to_string(),
540                type_hint: "string".to_string(),
541                description: "One line of text; omit to use the RFC's own wording".to_string(),
542                required: false,
543            },
544        ],
545        example: json!({
546            "type": "send_dict_error",
547            "code": 550,
548            "message": "Invalid database, use \"SHOW DB\" for list of databases"
549        }),
550        log_template: Some(
551            LogTemplate::new()
552                .with_info("-> DICT {code} {message}")
553                .with_debug("DICT send_dict_error: code={code}"),
554        ),
555    }
556}
557
558fn close_connection_action() -> ActionDefinition {
559    ActionDefinition {
560        name: "close_connection".to_string(),
561        description: "Close the DICT connection after any reply".to_string(),
562        parameters: vec![],
563        example: json!({"type": "close_connection"}),
564        log_template: Some(
565            LogTemplate::new()
566                .with_info("DICT connection closed")
567                .with_debug("DICT close_connection"),
568        ),
569    }
570}
571
572/// `DEFINE database word`
573pub static DICT_DEFINE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
574    EventType::new(
575        "dict_define",
576        "Client sent DEFINE: look up a word. database is a name from your database list, '*' \
577         (all databases) or '!' (stop at the first database that has it).",
578        json!({
579            "type": "send_dict_definitions",
580            "word": "serendipity",
581            "definitions": [{
582                "database": "wn",
583                "database_description": "WordNet (r) 3.0",
584                "text": "serendipity\n    n 1: good luck in making unexpected discoveries"
585            }]
586        }),
587    )
588    .with_parameters(vec![
589        Parameter {
590            name: "database".to_string(),
591            type_hint: "string".to_string(),
592            description: "Database name, '*' or '!'".to_string(),
593            required: true,
594        },
595        Parameter {
596            name: "word".to_string(),
597            type_hint: "string".to_string(),
598            description: "The word to define".to_string(),
599            required: true,
600        },
601    ])
602    .with_log_template(
603        LogTemplate::new()
604            .with_info("DICT DEFINE {database} {word}")
605            .with_debug("DICT dict_define: database={database} word={word}"),
606    )
607    .with_actions(vec![
608        send_dict_definitions_action(),
609        send_dict_error_action(),
610        close_connection_action(),
611    ])
612    .with_alternative_example(json!({"type": "send_dict_error", "code": 552}))
613});
614
615/// `MATCH database strategy word`
616pub static DICT_MATCH_EVENT: LazyLock<EventType> = LazyLock::new(|| {
617    EventType::new(
618        "dict_match",
619        "Client sent MATCH: list headwords matching word under strategy ('.' means your \
620         default strategy). database is a name, '*' or '!'.",
621        json!({
622            "type": "send_dict_matches",
623            "matches": [{"database": "wn", "word": "serendipity"}]
624        }),
625    )
626    .with_parameters(vec![
627        Parameter {
628            name: "database".to_string(),
629            type_hint: "string".to_string(),
630            description: "Database name, '*' or '!'".to_string(),
631            required: true,
632        },
633        Parameter {
634            name: "strategy".to_string(),
635            type_hint: "string".to_string(),
636            description: "Strategy name, or '.' for the server default".to_string(),
637            required: true,
638        },
639        Parameter {
640            name: "word".to_string(),
641            type_hint: "string".to_string(),
642            description: "The word to match against".to_string(),
643            required: true,
644        },
645    ])
646    .with_log_template(
647        LogTemplate::new()
648            .with_info("DICT MATCH {database} {strategy} {word}")
649            .with_debug("DICT dict_match: database={database} strategy={strategy} word={word}"),
650    )
651    .with_actions(vec![
652        send_dict_matches_action(),
653        send_dict_error_action(),
654        close_connection_action(),
655    ])
656    .with_alternative_example(json!({"type": "send_dict_error", "code": 551}))
657});
658
659/// `SHOW DB` / `SHOW STRAT` / `SHOW INFO database` / `SHOW SERVER`
660pub static DICT_SHOW_EVENT: LazyLock<EventType> = LazyLock::new(|| {
661    EventType::new(
662        "dict_show",
663        "Client sent SHOW. what = 'databases' (answer send_dict_databases), 'strategies' \
664         (send_dict_strategies), 'info' (send_dict_text code 112 about 'database') or 'server' \
665         (send_dict_text code 114).",
666        json!({
667            "type": "send_dict_databases",
668            "databases": [{"name": "wn", "description": "WordNet (r) 3.0"}]
669        }),
670    )
671    .with_parameters(vec![
672        Parameter {
673            name: "what".to_string(),
674            type_hint: "string".to_string(),
675            description: "What is being shown".to_string(),
676            required: true,
677        }
678        .with_choices(["databases", "strategies", "info", "server"]),
679        Parameter {
680            name: "database".to_string(),
681            type_hint: "string".to_string(),
682            description: "The database SHOW INFO asks about (only when what is 'info')".to_string(),
683            required: false,
684        },
685    ])
686    .with_log_template(
687        LogTemplate::new()
688            .with_info("DICT SHOW {what}")
689            .with_debug("DICT dict_show: what={what}"),
690    )
691    .with_actions(vec![
692        send_dict_databases_action(),
693        send_dict_strategies_action(),
694        send_dict_text_action(),
695        send_dict_error_action(),
696        close_connection_action(),
697    ])
698    .with_alternative_example(json!({
699        "type": "send_dict_text",
700        "code": 114,
701        "text": "NetGet DICT server\nThe model is the dictionary."
702    }))
703});