diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/event.cc | 141 | ||||
| -rw-r--r-- | src/event.hh | 21 | ||||
| -rw-r--r-- | src/event_main.cc | 233 |
3 files changed, 303 insertions, 92 deletions
diff --git a/src/event.cc b/src/event.cc index 8606819..02343c9 100644 --- a/src/event.cc +++ b/src/event.cc @@ -15,6 +15,10 @@ public: ~EventImpl() override { } + int64_t id() const override { + return id_; + } + const std::string& name() const override { return name_; } @@ -45,44 +49,41 @@ public: start_ = start; } - void going(std::set<std::string>* going, - std::set<std::string>* not_going) const override { - if (going) { - going->clear(); - going->insert(going_.begin(), going_.end()); - } - if (not_going) { - not_going->clear(); - not_going->insert(not_going_.begin(), not_going_.end()); - } + void going(std::vector<Going>* going) const override { + going->assign(going_.begin(), going_.end()); } bool is_going(const std::string& name) const override { - return going_.count(name) > 0; + for (const auto& going : going_) { + if (going.name == name) { + return going.is_going; + } + } + return false; } - void update_going(const std::string& name, bool going) override { - auto it = going_.find(name); - if (it != going_.end()) { - if (going) return; - going_.erase(it); - not_going_.insert(name); - going_uptodate_ = false; - } else { - it = not_going_.find(name); - if (it != not_going_.end()) { - if (!going) return; - going_.insert(name); - not_going_.erase(it); - going_uptodate_ = false; - } else { - if (going) { - going_.insert(name); - } else { - not_going_.insert(name); + void update_going(const std::string& name, bool is_going, + const std::string& note) override { + for (auto it = going_.begin(); it != going_.end(); ++it) { + if (it->name == name) { + if (it->is_going == is_going && it->note == note) { + return; + } + going_.erase(it); + break; + } + } + std::vector<Going>::iterator it; + if (is_going) { + for (it = going_.begin(); it != going_.end(); ++it) { + if (!it->is_going) { + break; } } + } else { + it = going_.end(); } + going_.emplace(it, name, is_going, note, time(NULL)); going_uptodate_ = false; } @@ -110,11 +111,10 @@ public: bool remove() override { if (new_) return true; DB::Transaction transaction(db_); - if (!db_->remove(kEventTable, - DB::Condition("id", DB::Condition::EQUAL, id_))) - return false; - if (!db_->remove(kEventGoingTable, - DB::Condition("event", DB::Condition::EQUAL, id_))) + if (db_->remove(kEventTable, + DB::Condition("id", DB::Condition::EQUAL, id_)) < 0 || + db_->remove(kEventGoingTable, + DB::Condition("event", DB::Condition::EQUAL, id_)) < 0) return false; if (transaction.commit()) { new_ = true; @@ -156,52 +156,55 @@ private: } } - bool store_going(const std::set<std::string>& names, bool going) const { - for (const auto& name : names) { + bool store_going() { + if (going_uptodate_) return true; + DB::Transaction transaction(db_); + if (db_->remove(kEventGoingTable, + DB::Condition("event", DB::Condition::EQUAL, id_)) < 0) + return false; + for (const auto& going : going_) { auto editor = db_->insert(kEventGoingTable); editor->set("event", id_); - editor->set("name", name); - editor->set("going", going); + editor->set("name", going.name); + editor->set("is_going", going.is_going); + editor->set("note", going.note); + editor->set("added", static_cast<int64_t>(going.added)); if (!editor->commit()) { return false; } } - return true; - } - - bool store_going() { - if (going_uptodate_) return true; - DB::Transaction transaction(db_); - if (!db_->remove(kEventGoingTable, - DB::Condition("event", DB::Condition::EQUAL, id_))) - return false; - if (!store_going(going_, true) || !store_going(not_going_, false)) - return false; if (transaction.commit()) { going_uptodate_ = true; return true; } return false; } + bool load_going() { going_.clear(); - not_going_.clear(); going_uptodate_ = true; + std::vector<DB::OrderBy> order_by; + order_by.push_back(DB::OrderBy("is_going", false)); + order_by.push_back(DB::OrderBy("added")); + order_by.push_back(DB::OrderBy("name")); auto snapshot = db_->select(kEventGoingTable, DB::Condition("event", DB::Condition::EQUAL, - id_)); + id_), + order_by); if (!snapshot) return true; do { - bool is_going; std::string name; - if (!snapshot->get(0, &name) || !snapshot->get(1, &is_going)) + bool is_going; + std::string note; + int64_t added; + if (!snapshot->get(0, &name) || !snapshot->get(1, &is_going) || + !snapshot->get(3, &added)) return false; - if (is_going) { - going_.insert(name); - } else { - not_going_.insert(name); - } + if (!snapshot->get(2, ¬e)) + note = ""; + going_.emplace_back(name, is_going, note, + static_cast<time_t>(added)); } while (snapshot->next()); return !snapshot->bad(); } @@ -214,10 +217,16 @@ private: time_t start_; bool new_; bool going_uptodate_; - std::set<std::string> going_; - std::set<std::string> not_going_; + std::vector<Going> going_; }; +std::shared_ptr<DB::Snapshot> open(std::shared_ptr<DB> db) { + std::vector<DB::OrderBy> order_by; + order_by.push_back(DB::OrderBy("start")); + order_by.push_back(DB::OrderBy("name")); + return db->select(kEventTable, DB::Condition(), order_by); +} + } // namespace // static @@ -231,14 +240,15 @@ bool Event::setup(DB* db) { decl.clear(); decl.push_back(std::make_pair("event", DB::NotNull(DB::Type::INT64))); decl.push_back(std::make_pair("name", DB::NotNull(DB::Type::STRING))); - decl.push_back(std::make_pair("going", DB::NotNull(DB::Type::BOOL))); + decl.push_back(std::make_pair("is_going", DB::NotNull(DB::Type::BOOL))); + decl.push_back(std::make_pair("note", DB::Type::STRING)); + decl.push_back(std::make_pair("added", DB::NotNull(DB::Type::INT64))); return db->insert_table(kEventGoingTable, decl); } // static std::unique_ptr<Event> Event::next(std::shared_ptr<DB> db) { - auto snapshot = - db->select(kEventTable, DB::Condition(), DB::OrderBy("start")); + auto snapshot = open(db); if (snapshot) { do { auto ev = new EventImpl(db); @@ -253,8 +263,7 @@ std::unique_ptr<Event> Event::next(std::shared_ptr<DB> db) { // static std::vector<std::unique_ptr<Event>> Event::all(std::shared_ptr<DB> db) { - auto snapshot = - db->select(kEventTable, DB::Condition(), DB::OrderBy("start")); + auto snapshot = open(db); std::vector<std::unique_ptr<Event>> ret; if (snapshot) { do { diff --git a/src/event.hh b/src/event.hh index 425a70f..8919429 100644 --- a/src/event.hh +++ b/src/event.hh @@ -2,7 +2,6 @@ #define EVENT_HH #include <memory> -#include <set> #include <string> #include <vector> @@ -12,8 +11,22 @@ class DB; class Event { public: + struct Going { + std::string name; + bool is_going; + std::string note; + time_t added; + + Going(const std::string& name, bool is_going, const std::string& note, + time_t added) + : name(name), is_going(is_going), note(note), added(added) { + } + }; + virtual ~Event() {} + virtual int64_t id() const = 0; + virtual const std::string& name() const = 0; virtual void set_name(const std::string& name) = 0; @@ -23,11 +36,11 @@ public: virtual time_t start() const = 0; virtual void set_start(time_t start) = 0; - virtual void going(std::set<std::string>* going, - std::set<std::string>* not_going) const = 0; + virtual void going(std::vector<Going>* going) const = 0; virtual bool is_going(const std::string& name) const = 0; - virtual void update_going(const std::string& name, bool going) = 0; + virtual void update_going(const std::string& name, bool is_going, + const std::string& note = std::string()) = 0; virtual bool store() = 0; diff --git a/src/event_main.cc b/src/event_main.cc index 99b7d1a..afde933 100644 --- a/src/event_main.cc +++ b/src/event_main.cc @@ -42,9 +42,13 @@ std::shared_ptr<DB> open(const std::string& channel) { *it = '.'; } } - auto db = SQLite3::open(DB_PATH + tmp + ".db"); - if (!db) { + auto db = SQLite3::open(DB_PATH "/" + tmp + ".db"); + if (!db || db->bad()) { Http::response(200, "Unable to open database"); + db.reset(); + } else if (!Event::setup(db.get())) { + Http::response(200, "Unable to setup database"); + db.reset(); } return std::move(db); } @@ -55,11 +59,150 @@ bool parse(const std::string& text, std::vector<std::string>* args) { return false; } +const double ONE_DAY_IN_SEC = 24.0 * 60.0 * 60.0; +const double ONE_WEEK_IN_SEC = ONE_DAY_IN_SEC * 7.0; +// It's OK that we ignore leap years here +const double ONE_YEAR_IN_SEC = 365 * ONE_DAY_IN_SEC; + +std::string format_date(time_t date) { + time_t now = time(NULL); + struct tm _t; + struct tm* t = localtime_r(&date, &_t); + double diff = difftime(date, now); + char tmp[100]; + if (diff <= ONE_DAY_IN_SEC) { + // Same day, just show time + strftime(tmp, sizeof(tmp), "%H:%M", t); + } else if (diff <= ONE_WEEK_IN_SEC) { + // Inside a week, show day and time + strftime(tmp, sizeof(tmp), "%A %H:%M", t); + } else if (diff <= ONE_YEAR_IN_SEC / 2.0) { + // Inside a year, show date, day and time + strftime(tmp, sizeof(tmp), "%A %d/%m %H:%M", t); + } else { + strftime(tmp, sizeof(tmp), "%Y-%m-%d %H:%M", t); + } + return tmp; +} + +void signal_channel(const std::string& str) { + +} + +void signal_event(const std::unique_ptr<Event>& event) { + std::ostringstream ss; + ss << event->name() << " @ " << format_date(event->start()) << std::endl; + if (!event->text().empty()) { + ss << event->text() << std::endl; + } + ss << std::endl; + ss << "Use /going to join the event" << std::endl; + signal_channel(ss.str()); +} + +bool parse_time(const std::string& value, time_t* date) { + struct tm _t, _tmp; + time_t now = time(NULL); + localtime_r(&now, &_t); + _tmp = _t; + auto ptr = strptime(value.c_str(), "%H:%M", &_tmp); + if (ptr && !*ptr) goto done; + _tmp = _t; + ptr = strptime(value.c_str(), "%A %H:%M", &_tmp); + if (ptr && !*ptr) { + // Given the weekday, figure out distance to "now" + int days = _tmp.tm_wday - _t.tm_wday; + if (days <= 0) { + days += 7; + } + time_t tmp = mktime(&_tmp) + days * ONE_DAY_IN_SEC; + localtime_r(&tmp, &_tmp); + goto done; + } + _tmp = _t; + ptr = strptime(value.c_str(), "%d/%m %H:%M", &_tmp); + if (ptr && !*ptr) goto done; + _tmp = _t; + ptr = strptime(value.c_str(), "%A %d/%m %H:%M", &_tmp); + if (ptr && !*ptr) goto done; + _tmp = _t; + ptr = strptime(value.c_str(), "%Y-%m-%d %H:%M", &_tmp); + if (ptr && !*ptr) goto done; + return false; + done: + *date = mktime(&_tmp); + return true; +} + bool create(const std::string& channel, std::map<std::string, std::string>& data) { std::vector<std::string> args; - if (!parse(data["text"], &args) || args.empty()) return true; - + if (!parse(data["text"], &args)) return true; + if (args.size() < 2) { + Http::response(200, "Usage: /create NAME START [TEXT]"); + return true; + } + std::string name, text; + time_t start; + name = args.front(); + args.erase(args.begin()); + text = args.front(); + args.erase(args.begin()); + while (true) { + if (parse_time(text, &start)) break; + if (args.empty()) { + Http::response(200, "Couldn't figure out when to start the event, try [DAY|DATE] HH:MM"); + return true; + } + text.push_back(' '); + text.append(args.front()); + args.erase(args.begin()); + } + auto db = open(channel); + if (!db) return true; + auto event = Event::create(db, name, start); + if (!event) { + Http::response(200, "Unable to create event"); + return true; + } + if (!args.empty()) { + std::string text(args.front()); + args.erase(args.begin()); + for (const auto& arg : args) { + text.push_back(' '); + text.append(arg); + } + event->set_text(text); + } + if (!event->store()) { + Http::response(200, "Unable to store event"); + return true; + } + auto next_event = Event::next(db); + if (next_event->id() == event->id()) { + signal_event(next_event); + } + Http::response(200, "Event created"); + return true; +} + +template<typename Iterator> +bool append_indexes(Iterator begin, Iterator end, + std::vector<unsigned long>* out) { + for (auto it = begin; it != end; ++it) { + try { + size_t end; + auto tmp = std::stoul(*it, &end); + if (end != it->size()) { + Http::response(200, "Bad index: " + *it); + return false; + } + out->push_back(tmp); + } catch (std::invalid_argument& e) { + Http::response(200, "Bad index: " + *it); + return false; + } + } return true; } @@ -71,19 +214,8 @@ bool cancel(const std::string& channel, if (args.empty()) { indexes.push_back(0); } else { - for (const auto& arg : args) { - try { - size_t end; - auto tmp = std::stoul(arg, &end); - if (end != arg.size()) { - Http::response(200, "Bad index: " + arg); - return true; - } - indexes.push_back(tmp); - } catch (std::invalid_argument& e) { - Http::response(200, "Bad index: " + arg); - return true; - } + if (!append_indexes(args.begin(), args.end(), &indexes)) { + return true; } std::sort(indexes.begin(), indexes.end(), std::greater<unsigned long>()); @@ -95,6 +227,8 @@ bool cancel(const std::string& channel, if (indexes.front() >= events.size()) { if (events.empty()) { Http::response(200, "There are no events"); + } else if (events.size() == 1) { + Http::response(200, "There is only one event"); } else { std::ostringstream ss; ss << "There are only " << events.size() << " events"; @@ -102,9 +236,14 @@ bool cancel(const std::string& channel, } return true; } - bool signal_channel = false; + std::string signal; for (const auto& index : indexes) { - if (index == 0) signal_channel = true; + if (index == 0) { + std::ostringstream ss; + ss << "Event canceled: " << events[index]->name() << " @ " + << format_date(events[index]->start()); + signal = ss.str(); + } events[index]->remove(); } if (indexes.size() > 1) { @@ -112,8 +251,8 @@ bool cancel(const std::string& channel, } else { Http::response(200, "Event removed"); } - if (signal_channel) { - + if (!signal.empty()) { + signal_channel(signal); } return true; } @@ -130,7 +269,57 @@ bool show(const std::string& channel, std::map<std::string, std::string>& data) { std::vector<std::string> args; if (!parse(data["text"], &args)) return true; - + std::vector<unsigned long> indexes; + if (args.empty()) { + indexes.push_back(0); + } else { + if (!append_indexes(args.begin(), args.end(), &indexes)) { + return true; + } + } + auto db = open(channel); + if (!db) return true; + auto events = Event::all(db); + std::ostringstream ss; + for (const auto& index : indexes) { + if (indexes.size() > 1) { + ss << '(' << index << ") "; + } + if (index >= events.size()) { + if (events.empty()) { + ss << "There are no events" << std::endl; + } else { + ss << "No such event: " << index << std::endl; + } + } else { + ss << events[index]->name() << " @ " + << format_date(events[index]->start()) << std::endl; + const auto& text = events[index]->text(); + if (!text.empty()) { + ss << text << std::endl; + } + std::vector<Event::Going> going; + events[index]->going(&going); + auto it = going.begin(); + for (; it != going.end(); ++it) { + if (!it->is_going) break; + ss << it->name; + if (!it->note.empty()) { + ss << ": " << it->note; + } + ss << std::endl; + } + if (it != going.end()) ss << std::endl; + for (; it != going.end(); ++it) { + ss << it->name << ": not going"; + if (!it->note.empty()) { + ss << ' ' << it->note; + } + ss << std::endl; + } + } + } + Http::response(200, ss.str()); return true; } |
