Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions api/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,9 @@ target_link_libraries(smashcast_test PRIVATE ${TEST_LIB})
add_executable(streams_test
tests/StreamsTest.cpp
src/Streams.cpp
src/Users.cpp
src/Config.cpp
src/Strings.cpp
src/Channel.cpp
src/Status.cpp
src/APIClient.cpp)
Expand All @@ -188,3 +191,5 @@ add_test(angelthump angelthump_test)
add_test(okru okru_test)
add_test(smashcast smashcast_test)
add_test(streams streams_test)
add_test(NAME users COMMAND users_test)
set_tests_properties(users PROPERTIES WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}/tests)
6 changes: 6 additions & 0 deletions api/src/APIHTTPService.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ APIHTTPService::APIHTTPService(std::shared_ptr<DB> db) : db_(db) {
"stream_path": {"type": "string"},
"service": {"type": "string"},
"channel": {"type": "string"},
"stream_title_override": {"type": "string"},
"left_chat": {"type": "boolean"},
"show_hidden": {"type": "boolean"},
"show_dgg_chat": {"type": "boolean"},
Expand Down Expand Up @@ -145,6 +146,11 @@ void APIHTTPService::PostProfile(uWS::HttpResponse *res, HTTPRequest *req) {
json::StringRef(input["stream_path"]), &status));
}

if (status.Ok() && input.HasMember("stream_title_override")) {
status = newUser->SetStreamTitleOverride(
json::StringRef(input["stream_title_override"]));
}

if (status.Ok()) {
status = db_->GetUsers()->Save(newUser);
}
Expand Down
2 changes: 1 addition & 1 deletion api/src/DB.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class DB {
users_(std::make_shared<Users>(db_)),
banned_streams_(std::make_shared<BannedStreams>(db_)),
banned_ips_(std::make_shared<IPRanges>(db_, "banned_ip_ranges")),
streams_(std::make_shared<Streams>(db_)),
streams_(std::make_shared<Streams>(db_, users_)),
viewer_states_(std::make_shared<ViewerStates>(users_, streams_)) {}

inline std::shared_ptr<Users> GetUsers() { return users_; }
Expand Down
27 changes: 23 additions & 4 deletions api/src/Streams.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,22 @@

#include <algorithm>

#include "Users.h"

namespace rustla2 {

std::string Stream::GetTitleOverride() const {
if (!users_ || !channel_->HasStreamPath()) {
return "";
}
auto user = users_->GetByStreamPath(channel_->GetStreamPath());
// Old stream cards must not inherit a title after a profile changes source.
if (!user || !ChannelEqual{}(*user->GetChannel(), *channel_)) {
return "";
}
return user->GetStreamTitleOverride();
}

void Stream::WriteAPIJSON(
rapidjson::Writer<rapidjson::StringBuffer> *writer) const {
boost::shared_lock<boost::shared_mutex> read_lock(lock_);
Expand All @@ -29,6 +43,8 @@ void Stream::WriteAPIJSON(
writer->String(channel_->GetChannel());
writer->Key("title");
writer->String(title_);
writer->Key("title_override");
writer->String(GetTitleOverride());
writer->Key("thumbnail");
writer->String(thumbnail_);
writer->Key("url");
Expand All @@ -53,6 +69,8 @@ void Stream::WriteJSON(
writer->String(channel_->GetStreamPath());
writer->Key("title");
writer->String(title_);
writer->Key("title_override");
writer->String(GetTitleOverride());
writer->Key("thumbnail");
writer->String(thumbnail_);
writer->Key("live");
Expand Down Expand Up @@ -221,8 +239,9 @@ bool Stream::SaveNew() {
return true;
}

Streams::Streams(sqlite::database db)
: db_(db), observers_(std::make_shared<Observable<uint64_t>>()) {
Streams::Streams(sqlite::database db, std::shared_ptr<Users> users)
: db_(db), users_(users),
observers_(std::make_shared<Observable<uint64_t>>()) {
InitTable();

auto sql = R"sql(
Expand Down Expand Up @@ -254,7 +273,7 @@ Streams::Streams(sqlite::database db)
auto stream_channel = Channel::Create(channel, service, path);
auto stream = std::make_shared<Stream>(
db_, observers_, id, stream_channel, nsfw, hidden, afk, promoted, title,
thumbnail, live, viewer_count, service_nsfw, removed);
thumbnail, live, viewer_count, service_nsfw, removed, users_);

data_by_id_[stream->GetID()] = stream;
data_by_channel_[stream_channel] = stream;
Expand Down Expand Up @@ -353,7 +372,7 @@ void Streams::WriteStreamsJSON(
}

std::shared_ptr<Stream> Streams::Emplace(const Channel &channel) {
auto stream = std::make_shared<Stream>(db_, observers_, channel);
auto stream = std::make_shared<Stream>(db_, observers_, channel, users_);

{
boost::unique_lock<boost::shared_mutex> write_lock(lock_);
Expand Down
22 changes: 17 additions & 5 deletions api/src/Streams.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

namespace rustla2 {

class Users;

// Cap stream IDs to 48 bits for JS
const uint64_t kMaxStreamID = 0xFFFFFFFFFFF;

Expand All @@ -30,7 +32,8 @@ class Stream {
bool hidden = false, bool afk = false, bool promoted = false,
const std::string &title = "", const std::string &thumbnail = "",
const bool live = false, const uint64_t viewer_count = 0,
const bool service_nsfw = false, const bool removed = false)
const bool service_nsfw = false, const bool removed = false,
std::shared_ptr<Users> users = nullptr)
: db_(db),
observers_(observers),
viewer_ips_(std::make_shared<IPSet>()),
Expand All @@ -45,11 +48,14 @@ class Stream {
afk_(afk),
promoted_(promoted),
viewer_count_(viewer_count),
service_nsfw_(service_nsfw) {}
service_nsfw_(service_nsfw),
users_(users) {}

Stream(sqlite::database db, std::shared_ptr<Observable<uint64_t>> observers,
const Channel &channel)
: Stream(db, observers, ChannelHash{}(channel)&kMaxStreamID, channel) {}
const Channel &channel, std::shared_ptr<Users> users = nullptr)
: Stream(db, observers, ChannelHash{}(channel)&kMaxStreamID, channel) {
users_ = users;
}

inline uint64_t GetID() const {
boost::shared_lock<boost::shared_mutex> read_lock(lock_);
Expand Down Expand Up @@ -231,6 +237,10 @@ class Stream {
}

private:
// Called while holding the stream lock. Resolve the current profile so edits
// appear in the next directory broadcast without changing provider metadata.
std::string GetTitleOverride() const;

inline void ResetUpdatedTime() {
update_time_ = std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now().time_since_epoch())
Expand All @@ -257,6 +267,7 @@ class Stream {
uint64_t afk_count_{0};
uint64_t reset_time_{0};
uint64_t update_time_{0};
std::shared_ptr<Users> users_;

friend std::ostream &operator<<(std::ostream &os, const Stream &stream);
};
Expand Down Expand Up @@ -293,7 +304,7 @@ struct IsNotRemoved {

class Streams {
public:
explicit Streams(sqlite::database db);
explicit Streams(sqlite::database db, std::shared_ptr<Users> users = nullptr);

void InitTable();

Expand Down Expand Up @@ -351,6 +362,7 @@ class Streams {

private:
sqlite::database db_;
std::shared_ptr<Users> users_;
std::shared_ptr<Observable<uint64_t>> observers_;
boost::shared_mutex lock_;
std::unordered_map<uint64_t, std::shared_ptr<Stream>> data_by_id_;
Expand Down
57 changes: 52 additions & 5 deletions api/src/Users.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "Users.h"

#include <glog/logging.h>
#include <rapidjson/memorystream.h>
#include <boost/algorithm/string.hpp>
#include <boost/regex.hpp>

Expand Down Expand Up @@ -58,6 +59,8 @@ std::string User::GetProfileJSON() {
writer.Bool(show_dgg_chat_);
writer.Key("enable_public_state");
writer.Bool(enable_public_state_);
writer.Key("stream_title_override");
writer.String(stream_title_override_);
writer.EndObject();

return buf.GetString();
Expand Down Expand Up @@ -167,6 +170,28 @@ bool User::SetChannel(const Channel &channel) {
return true;
}

Status User::SetStreamTitleOverride(const std::string &title) {
const auto trimmed = boost::trim_copy(title);
rapidjson::MemoryStream input(trimmed.data(), trimmed.size());
size_t length = 0;
while (input.Tell() < trimmed.size()) {
unsigned codepoint;
if (!rapidjson::UTF8<>::Decode(input, &codepoint) || codepoint < 32 ||
(codepoint >= 127 && codepoint <= 159)) {
return Status(StatusCode::VALIDATION_ERROR,
"Stream title must be a single line of valid text.");
}
if (++length > 120) {
return Status(StatusCode::VALIDATION_ERROR,
"Stream title must be 120 characters or fewer.");
}
}

boost::unique_lock<boost::shared_mutex> write_lock(lock_);
stream_title_override_ = trimmed;
return Status::OK;
}

bool User::SetLeftChat(bool left_chat) {
boost::unique_lock<boost::shared_mutex> write_lock(lock_);
left_chat_ = left_chat;
Expand Down Expand Up @@ -219,13 +244,14 @@ bool User::Save() {
`show_hidden` = ?,
`show_dgg_chat` = ?,
`enable_public_state` = ?,
`stream_title_override` = ?,
`updated_at` = datetime()
WHERE `id` = ?
)sql";
db_ << sql << name_ << channel_->GetStreamPath() << channel_->GetService()
<< channel_->GetChannel() << last_ip_ << last_seen_ << left_chat_
<< is_admin_ << show_hidden_ << show_dgg_chat_ << enable_public_state_
<< GetIDString();
<< stream_title_override_ << GetIDString();
} catch (const sqlite::sqlite_exception &e) {
LOG(ERROR) << "error updating user " << this << ", "
<< "error: " << e.what() << ", "
Expand Down Expand Up @@ -255,6 +281,7 @@ bool User::SaveNew() {
`show_hidden`,
`show_dgg_chat`,
`enable_public_state`,
`stream_title_override`,
`ban_reason`,
`created_at`,
`updated_at`
Expand All @@ -274,6 +301,7 @@ bool User::SaveNew() {
?,
?,
?,
?,
'',
datetime(),
datetime()
Expand All @@ -282,7 +310,8 @@ bool User::SaveNew() {
db_ << sql << GetIDString() << twitch_id_ << channel_->GetChannel() << name_
<< channel_->GetStreamPath() << channel_->GetService()
<< channel_->GetChannel() << last_ip_ << last_seen_ << left_chat_
<< is_admin_ << show_hidden_ << show_dgg_chat_ << enable_public_state_;
<< is_admin_ << show_hidden_ << show_dgg_chat_ << enable_public_state_
<< stream_title_override_;
} catch (const sqlite::sqlite_exception &e) {
LOG(ERROR) << "error creating user " << this << ", "
<< "error: " << e.what() << ", "
Expand Down Expand Up @@ -327,7 +356,8 @@ Users::Users(sqlite::database db) : db_(db) {
`is_admin`,
`show_hidden`,
`show_dgg_chat`,
`enable_public_state`
`enable_public_state`,
`stream_title_override`
FROM `users`
)sql";
auto query = db_ << sql;
Expand All @@ -338,12 +368,14 @@ Users::Users(sqlite::database db) : db_(db) {
const std::string &last_ip, const time_t last_seen,
const bool left_chat, const bool is_admin,
const bool show_hidden, const bool show_dgg_chat,
const bool enable_public_state) {
const bool enable_public_state,
const std::string &stream_title_override) {
boost::uuids::string_generator to_uuid;
auto user_channel = Channel::Create(channel, service, stream_path);
auto user = std::make_shared<User>(
db_, to_uuid(id), twitch_id, name, user_channel, last_ip, last_seen,
left_chat, is_admin, show_hidden, show_dgg_chat, enable_public_state);
left_chat, is_admin, show_hidden, show_dgg_chat, enable_public_state,
stream_title_override);

data_by_id_[user->GetID()] = user;
data_by_twitch_id_[user->GetTwitchID()] = user;
Expand Down Expand Up @@ -377,11 +409,26 @@ void Users::InitTable() {
`show_hidden` TINYINT(1) DEFAULT 0,
`show_dgg_chat` TINYINT(1) DEFAULT 0,
`enable_public_state` TINYINT(1) DEFAULT 1,
`stream_title_override` TEXT NOT NULL DEFAULT '',
UNIQUE (`id`),
UNIQUE (`twitch_id`)
)
)sql";
db_ << sql;

// CREATE TABLE does not add columns to existing databases.
bool has_title_override = false;
db_ << "PRAGMA table_info(users)" >>
[&](int, const std::string &name, const std::string &, int,
std::unique_ptr<std::string>, int) {
if (name == "stream_title_override") {
has_title_override = true;
}
};
if (!has_title_override) {
db_ << "ALTER TABLE users ADD COLUMN stream_title_override "
"TEXT NOT NULL DEFAULT ''";
}
}

std::shared_ptr<User> Users::GetByID(const boost::uuids::uuid &id) {
Expand Down
17 changes: 14 additions & 3 deletions api/src/Users.h
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ class User {
const int64_t twitch_id, const std::string &name, const Channel &channel,
const std::string &last_ip, const time_t last_seen, const bool left_chat,
const bool is_admin, const bool show_hidden, const bool show_dgg_chat,
const bool enable_public_state)
const bool enable_public_state,
const std::string &stream_title_override = "")
: db_(db),
id_(id),
twitch_id_(twitch_id),
Expand All @@ -40,7 +41,8 @@ class User {
is_admin_(is_admin),
show_hidden_(show_hidden),
show_dgg_chat_(show_dgg_chat),
enable_public_state_(enable_public_state) {}
enable_public_state_(enable_public_state),
stream_title_override_(stream_title_override) {}

User(sqlite::database db, const uint64_t twitch_id, const Channel &channel,
const std::string &last_ip)
Expand Down Expand Up @@ -68,7 +70,8 @@ class User {
is_admin_(user.is_admin_),
show_hidden_(user.show_hidden_),
show_dgg_chat_(user.show_dgg_chat_),
enable_public_state_(user.enable_public_state_) {}
enable_public_state_(user.enable_public_state_),
stream_title_override_(user.stream_title_override_) {}

inline boost::uuids::uuid GetID() {
boost::shared_lock<boost::shared_mutex> read_lock(lock_);
Expand Down Expand Up @@ -130,6 +133,11 @@ class User {
return enable_public_state_;
}

inline std::string GetStreamTitleOverride() {
boost::shared_lock<boost::shared_mutex> read_lock(lock_);
return stream_title_override_;
}

std::string GetStreamJSON();

std::string GetUsernameJSON();
Expand All @@ -140,6 +148,8 @@ class User {

Status SetName(const std::string &name);

Status SetStreamTitleOverride(const std::string &title);

bool SetChannel(const Channel &channel);

bool SetLeftChat(bool left_chat);
Expand Down Expand Up @@ -172,6 +182,7 @@ class User {
bool show_hidden_;
bool show_dgg_chat_;
bool enable_public_state_;
std::string stream_title_override_;

friend class Users;
friend std::ostream &operator<<(std::ostream &os, const User &user);
Expand Down
Loading
Loading