summaryrefslogtreecommitdiff
path: root/Userland/Utilities/fortune.cpp
blob: 9353595b2b1fb4f38b73dafd107cf24624521f0c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
/*
 * Copyright (c) 2021, Ben Wiederhake <BenWiederhake.GitHub@gmx.de>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/ByteBuffer.h>
#include <AK/JsonArray.h>
#include <AK/JsonObject.h>
#include <AK/JsonValue.h>
#include <AK/Optional.h>
#include <AK/Random.h>
#include <AK/Vector.h>
#include <LibCore/ArgsParser.h>
#include <LibCore/DateTime.h>
#include <LibCore/File.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

class Quote {
public:
    static Optional<Quote> try_parse(const JsonValue& value)
    {
        if (!value.is_object())
            return {};
        auto& entry = value.as_object();
        Quote q;
        if (!entry.has("quote") || !entry.has("author") || !entry.has("utc_time") || !entry.has("url"))
            return {};
        // From here on, trust that it's probably fine.
        q.m_quote = entry.get("quote").as_string();
        q.m_author = entry.get("author").as_string();
        // It is sometimes parsed as u32, sometimes as u64, depending on how large the number is.
        q.m_utc_time = entry.get("utc_time").to_number<u64>();
        q.m_url = entry.get("url").as_string();
        if (entry.has("context"))
            q.m_context = entry.get("context").as_string();

        return q;
    }

    const String& quote() const { return m_quote; }
    const String& author() const { return m_author; }
    const u64& utc_time() const { return m_utc_time; }
    const String& url() const { return m_url; }
    const Optional<String>& context() const { return m_context; }

private:
    Quote() = default;

    String m_quote;
    String m_author;
    u64 m_utc_time;
    String m_url;
    Optional<String> m_context;
};

static Vector<Quote> parse_all(const JsonArray& array)
{
    Vector<Quote> quotes;
    for (size_t i = 0; i < array.size(); ++i) {
        Optional<Quote> q = Quote::try_parse(array[i]);
        if (!q.has_value()) {
            warnln("WARNING: Could not parse quote #{}!", i);
        } else {
            quotes.append(q.value());
        }
    }
    return quotes;
}

int main(int argc, char** argv)
{
    if (pledge("stdio rpath", nullptr) < 0) {
        perror("pledge");
        return 1;
    }

    const char* path = "/res/fortunes.json";

    Core::ArgsParser args_parser;
    args_parser.set_general_help("Open a fortune cookie, receive a free quote for the day!");
    args_parser.add_positional_argument(path, "Path to JSON file with quotes (/res/fortunes.json by default)", "path", Core::ArgsParser::Required::No);
    args_parser.parse(argc, argv);

    auto file = Core::File::construct(path);
    if (!file->open(Core::OpenMode::ReadOnly)) {
        warnln("Couldn't open {} for reading: {}", path, file->error_string());
        return 1;
    }

    if (pledge("stdio", nullptr) < 0) {
        perror("pledge");
        return 1;
    }
    if (unveil(nullptr, nullptr) < 0) {
        perror("unveil");
        return 1;
    }

    auto file_contents = file->read_all();
    auto json = JsonValue::from_string(file_contents);
    if (!json.has_value()) {
        warnln("Couldn't parse {} as JSON", path);
        return 1;
    }
    if (!json->is_array()) {
        warnln("{} does not contain an array of quotes", path);
        return 1;
    }

    const auto quotes = parse_all(json->as_array());
    if (quotes.is_empty()) {
        warnln("{} does not contain any valid quotes", path);
        return 1;
    }

    u32 i = get_random_uniform(quotes.size());
    const auto& chosen_quote = quotes[i];
    auto datetime = Core::DateTime::from_timestamp(chosen_quote.utc_time());

    outln(); // Tasteful spacing

    out("\033]8;;{}\033\\", chosen_quote.url());         // Begin link
    out("\033[34m({})\033[m", datetime.to_string());     // Datetime
    out(" \033[34;1m<{}>\033[m", chosen_quote.author()); // Author
    out(" \033[32m{}\033[m", chosen_quote.quote());      // Quote itself
    out("\033]8;;\033\\");                               // End link
    outln();

    if (chosen_quote.context().has_value())
        outln("\033[38;5;242m({})\033[m", chosen_quote.context().value()); // Some context

    outln(); // Tasteful spacing

    return 0;
}