about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt11
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock19
-rw-r--r--database.cpp122
-rw-r--r--database.h53
-rw-r--r--lunatic.cpp50
-rw-r--r--moons/README.md1
-rw-r--r--moons/blue.pngbin0 -> 37618 bytes
-rw-r--r--moons/brown.pngbin0 -> 38150 bytes
-rw-r--r--moons/cyan.pngbin0 -> 33139 bytes
-rw-r--r--moons/green.pngbin0 -> 36760 bytes
-rw-r--r--moons/orange.pngbin0 -> 36465 bytes
-rw-r--r--moons/pink.pngbin0 -> 30838 bytes
-rw-r--r--moons/purple.pngbin0 -> 32331 bytes
-rw-r--r--moons/red.pngbin0 -> 37165 bytes
-rw-r--r--moons/star.pngbin0 -> 27829 bytes
-rw-r--r--moons/white.pngbin0 -> 29878 bytes
-rw-r--r--moons/yellow.pngbin0 -> 32939 bytes
-rw-r--r--schema.sql32
-rw-r--r--scrape.rb181
20 files changed, 442 insertions, 32 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index 55a671b..00c0ad9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt
@@ -5,12 +5,17 @@ add_subdirectory(vendor/libtwittercpp)
5 5
6find_package(PkgConfig) 6find_package(PkgConfig)
7pkg_check_modules(yaml-cpp yaml-cpp REQUIRED) 7pkg_check_modules(yaml-cpp yaml-cpp REQUIRED)
8pkg_check_modules(GraphicsMagick GraphicsMagick++ REQUIRED)
9pkg_check_modules(sqlite3 sqlite3>=3.8.3 REQUIRED)
8 10
9include_directories( 11include_directories(
10 vendor/libtwittercpp/src 12 vendor/libtwittercpp/src
11 ${yaml-cpp_INCLUDE_DIRS}) 13 ${yaml-cpp_INCLUDE_DIRS}
14 ${GraphicsMagick_INCLUDE_DIRS})
12 15
13add_executable(lunatic lunatic.cpp) 16link_directories(${GraphicsMagick_LIBRARY_DIRS})
17
18add_executable(lunatic lunatic.cpp database.cpp)
14set_property(TARGET lunatic PROPERTY CXX_STANDARD 11) 19set_property(TARGET lunatic PROPERTY CXX_STANDARD 11)
15set_property(TARGET lunatic PROPERTY CXX_STANDARD_REQUIRED ON) 20set_property(TARGET lunatic PROPERTY CXX_STANDARD_REQUIRED ON)
16target_link_libraries(lunatic ${yaml-cpp_LIBRARIES} twitter++) \ No newline at end of file 21target_link_libraries(lunatic ${yaml-cpp_LIBRARIES} ${GraphicsMagick_LIBRARIES} ${sqlite3_LIBRARIES} twitter++) \ No newline at end of file
diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..57a02a2 --- /dev/null +++ b/Gemfile
@@ -0,0 +1,5 @@
1source 'https://rubygems.org'
2
3gem 'sqlite3'
4gem 'sequel'
5gem 'nokogiri'
diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..58ec121 --- /dev/null +++ b/Gemfile.lock
@@ -0,0 +1,19 @@
1GEM
2 remote: https://rubygems.org/
3 specs:
4 mini_portile2 (2.3.0)
5 nokogiri (1.8.2)
6 mini_portile2 (~> 2.3.0)
7 sequel (5.5.0)
8 sqlite3 (1.3.13)
9
10PLATFORMS
11 ruby
12
13DEPENDENCIES
14 nokogiri
15 sequel
16 sqlite3
17
18BUNDLED WITH
19 1.16.1
diff --git a/database.cpp b/database.cpp new file mode 100644 index 0000000..f8b5016 --- /dev/null +++ b/database.cpp
@@ -0,0 +1,122 @@
1#include "database.h"
2#include <sqlite3.h>
3#include <stdexcept>
4
5database::database(std::string path)
6{
7 if (sqlite3_open_v2(
8 path.c_str(),
9 &ppdb_,
10 SQLITE_OPEN_READONLY,
11 NULL) != SQLITE_OK)
12 {
13 // We still have to free the resources allocated. In the event that
14 // allocation failed, ppdb will be null and sqlite3_close_v2 will just
15 // ignore it.
16 std::string errmsg(sqlite3_errmsg(ppdb_));
17 sqlite3_close_v2(ppdb_);
18
19 throw std::logic_error(errmsg);
20 }
21}
22
23database::database(database&& other) : database()
24{
25 swap(*this, other);
26}
27
28database& database::operator=(database&& other)
29{
30 swap(*this, other);
31
32 return *this;
33}
34
35void swap(database& first, database& second)
36{
37 std::swap(first.ppdb_, second.ppdb_);
38}
39
40database::~database()
41{
42 sqlite3_close_v2(ppdb_);
43}
44
45achievement database::getRandomAchievement() const
46{
47 std::string queryString = "SELECT achievements.achievement_id, achievements.game_id, achievements.title, games.moon_image FROM achievements INNER JOIN games ON games.game_id = achievements.game_id ORDER BY RANDOM() LIMIT 1";
48
49 sqlite3_stmt* ppstmt;
50 if (sqlite3_prepare_v2(
51 ppdb_,
52 queryString.c_str(),
53 queryString.length(),
54 &ppstmt,
55 NULL) != SQLITE_OK)
56 {
57 std::string errorMsg = sqlite3_errmsg(ppdb_);
58 sqlite3_finalize(ppstmt);
59
60 throw std::logic_error(errorMsg);
61 }
62
63 if (sqlite3_step(ppstmt) != SQLITE_ROW)
64 {
65 std::string errorMsg = sqlite3_errmsg(ppdb_);
66 sqlite3_finalize(ppstmt);
67
68 throw std::logic_error(errorMsg);
69 }
70
71 achievement result;
72
73 result.achievementId = sqlite3_column_int(ppstmt, 0);
74 result.gameId = sqlite3_column_int(ppstmt, 1);
75 result.title = reinterpret_cast<const char*>(sqlite3_column_text(ppstmt, 2));
76 result.moonImage = reinterpret_cast<const char*>(sqlite3_column_text(ppstmt, 3));
77
78 sqlite3_finalize(ppstmt);
79
80 return result;
81}
82
83std::string database::getRandomImageForGame(int gameId) const
84{
85 std::string queryString = "SELECT filename FROM images WHERE game_id = ? ORDER BY RANDOM() LIMIT 1";
86
87 sqlite3_stmt* ppstmt;
88 if (sqlite3_prepare_v2(
89 ppdb_,
90 queryString.c_str(),
91 queryString.length(),
92 &ppstmt,
93 NULL) != SQLITE_OK)
94 {
95 std::string errorMsg = sqlite3_errmsg(ppdb_);
96 sqlite3_finalize(ppstmt);
97
98 throw std::logic_error(errorMsg);
99 }
100
101 if (sqlite3_bind_int(ppstmt, 1, gameId) != SQLITE_OK)
102 {
103 std::string errorMsg = sqlite3_errmsg(ppdb_);
104 sqlite3_finalize(ppstmt);
105
106 throw std::logic_error(errorMsg);
107 }
108
109 if (sqlite3_step(ppstmt) != SQLITE_ROW)
110 {
111 std::string errorMsg = sqlite3_errmsg(ppdb_);
112 sqlite3_finalize(ppstmt);
113
114 throw std::logic_error(errorMsg);
115 }
116
117 std::string result = reinterpret_cast<const char*>(sqlite3_column_text(ppstmt, 0));
118
119 sqlite3_finalize(ppstmt);
120
121 return result;
122}
diff --git a/database.h b/database.h new file mode 100644 index 0000000..560eeda --- /dev/null +++ b/database.h
@@ -0,0 +1,53 @@
1#ifndef DATABASE_H_75C3CE0F
2#define DATABASE_H_75C3CE0F
3
4#include <string>
5
6struct sqlite3;
7
8struct achievement {
9 int achievementId;
10 int gameId;
11 std::string title;
12 std::string moonImage;
13};
14
15class database {
16public:
17
18 // Constructor
19
20 explicit database(std::string path);
21
22 // Disable copying
23
24 database(const database& other) = delete;
25 database& operator=(const database& other) = delete;
26
27 // Move constructor and move assignment
28
29 database(database&& other);
30 database& operator=(database&& other);
31
32 // Swap
33
34 friend void swap(database& first, database& second);
35
36 // Destructor
37
38 ~database();
39
40 // Accessors
41
42 achievement getRandomAchievement() const;
43
44 std::string getRandomImageForGame(int gameId) const;
45
46private:
47
48 database() = default;
49
50 sqlite3* ppdb_ = nullptr;
51};
52
53#endif /* end of include guard: DATABASE_H_75C3CE0F */
diff --git a/lunatic.cpp b/lunatic.cpp index 09dcc41..ca3140a 100644 --- a/lunatic.cpp +++ b/lunatic.cpp
@@ -5,6 +5,8 @@
5#include <twitter.h> 5#include <twitter.h>
6#include <chrono> 6#include <chrono>
7#include <thread> 7#include <thread>
8#include <Magick++.h>
9#include "database.h"
8 10
9int main(int argc, char** argv) 11int main(int argc, char** argv)
10{ 12{
@@ -14,6 +16,8 @@ int main(int argc, char** argv)
14 return -1; 16 return -1;
15 } 17 }
16 18
19 Magick::InitializeMagick(nullptr);
20
17 std::string configfile(argv[1]); 21 std::string configfile(argv[1]);
18 YAML::Node config = YAML::LoadFile(configfile); 22 YAML::Node config = YAML::LoadFile(configfile);
19 23
@@ -23,7 +27,9 @@ int main(int argc, char** argv)
23 auth.setAccessKey(config["access_key"].as<std::string>()); 27 auth.setAccessKey(config["access_key"].as<std::string>());
24 auth.setAccessSecret(config["access_secret"].as<std::string>()); 28 auth.setAccessSecret(config["access_secret"].as<std::string>());
25 29
26 twitter::client client(auth); 30 //twitter::client client(auth);
31
32 database db(config["database"].as<std::string>());
27 33
28 std::random_device randomDevice; 34 std::random_device randomDevice;
29 std::mt19937 rng(randomDevice()); 35 std::mt19937 rng(randomDevice());
@@ -32,6 +38,40 @@ int main(int argc, char** argv)
32 { 38 {
33 std::cout << "Generating tweet" << std::endl; 39 std::cout << "Generating tweet" << std::endl;
34 40
41 achievement ach = db.getRandomAchievement();
42 std::string imageName = db.getRandomImageForGame(ach.gameId);
43 std::string imagePath = config["images"].as<std::string>()
44 + "/" + imageName;
45
46
47 try
48 {
49 Magick::Image image;
50 image.read(imagePath);
51 image.transform("1600x900");
52 image.scale("160x90");
53 image.scale("1600x900");
54 image.magick("png");
55 image.write("output.png");
56 } catch (const Magick::WarningCoder& ex)
57 {
58 // Ok
59 }
60
61
62
63
64
65
66
67
68
69
70
71
72
73 /*
74
35 // Reload achievements list every time in case it has been updated 75 // Reload achievements list every time in case it has been updated
36 std::vector<std::string> achievements; 76 std::vector<std::string> achievements;
37 std::ifstream datafile(config["achievements"].as<std::string>()); 77 std::ifstream datafile(config["achievements"].as<std::string>());
@@ -53,7 +93,7 @@ int main(int argc, char** argv)
53 } 93 }
54 94
55 std::uniform_int_distribution<int> dist(0, achievements.size() - 1); 95 std::uniform_int_distribution<int> dist(0, achievements.size() - 1);
56 std::string achievement = achievements[dist(rng)]; 96 std::string achievement = achievements[dist(rng)];*/
57 97
58 std::string header; 98 std::string header;
59 if (std::bernoulli_distribution(1.0 / 50.0)(rng)) 99 if (std::bernoulli_distribution(1.0 / 50.0)(rng))
@@ -63,16 +103,16 @@ int main(int argc, char** argv)
63 header = "YOU GOT A MOON!"; 103 header = "YOU GOT A MOON!";
64 } 104 }
65 105
66 std::string action = header + "\n" + achievement; 106 std::string action = header + "\n" + ach.title;
67 action.resize(140); 107 action.resize(140);
68 108
69 try 109 /*try
70 { 110 {
71 client.updateStatus(action); 111 client.updateStatus(action);
72 } catch (const twitter::twitter_error& e) 112 } catch (const twitter::twitter_error& e)
73 { 113 {
74 std::cout << "Twitter error: " << e.what() << std::endl; 114 std::cout << "Twitter error: " << e.what() << std::endl;
75 } 115 }*/
76 116
77 std::cout << action << std::endl; 117 std::cout << action << std::endl;
78 std::cout << "Waiting" << std::endl; 118 std::cout << "Waiting" << std::endl;
diff --git a/moons/README.md b/moons/README.md new file mode 100644 index 0000000..876f0db --- /dev/null +++ b/moons/README.md
@@ -0,0 +1 @@
The images in this directory are originally from the game Super Mario Odyssey, which is copyrighted by Nintendo. \ No newline at end of file
diff --git a/moons/blue.png b/moons/blue.png new file mode 100644 index 0000000..5e4ca02 --- /dev/null +++ b/moons/blue.png
Binary files differ
diff --git a/moons/brown.png b/moons/brown.png new file mode 100644 index 0000000..6a31b16 --- /dev/null +++ b/moons/brown.png
Binary files differ
diff --git a/moons/cyan.png b/moons/cyan.png new file mode 100644 index 0000000..8984126 --- /dev/null +++ b/moons/cyan.png
Binary files differ
diff --git a/moons/green.png b/moons/green.png new file mode 100644 index 0000000..5e7d311 --- /dev/null +++ b/moons/green.png
Binary files differ
diff --git a/moons/orange.png b/moons/orange.png new file mode 100644 index 0000000..06ca595 --- /dev/null +++ b/moons/orange.png
Binary files differ
diff --git a/moons/pink.png b/moons/pink.png new file mode 100644 index 0000000..3b1521b --- /dev/null +++ b/moons/pink.png
Binary files differ
diff --git a/moons/purple.png b/moons/purple.png new file mode 100644 index 0000000..9eeacf9 --- /dev/null +++ b/moons/purple.png
Binary files differ
diff --git a/moons/red.png b/moons/red.png new file mode 100644 index 0000000..2c0ef77 --- /dev/null +++ b/moons/red.png
Binary files differ
diff --git a/moons/star.png b/moons/star.png new file mode 100644 index 0000000..29263d5 --- /dev/null +++ b/moons/star.png
Binary files differ
diff --git a/moons/white.png b/moons/white.png new file mode 100644 index 0000000..fae666b --- /dev/null +++ b/moons/white.png
Binary files differ
diff --git a/moons/yellow.png b/moons/yellow.png new file mode 100644 index 0000000..8c012df --- /dev/null +++ b/moons/yellow.png
Binary files differ
diff --git a/schema.sql b/schema.sql new file mode 100644 index 0000000..61fdc45 --- /dev/null +++ b/schema.sql
@@ -0,0 +1,32 @@
1CREATE TABLE `profiles` (
2 `profile_id` INTEGER PRIMARY KEY,
3 `profile_path` VARCHAR(255) NOT NULL
4);
5
6CREATE UNIQUE INDEX `profile_by_path` ON `profiles`(`profile_path`);
7
8CREATE TABLE `games` (
9 `game_id` INTEGER PRIMARY KEY,
10 `steam_appid` INTEGER NOT NULL,
11 `moon_image` VARCHAR(255) NOT NULL
12);
13
14CREATE UNIQUE INDEX `game_by_appid` ON `games`(`steam_appid`);
15
16CREATE TABLE `achievements` (
17 `achievement_id` INTEGER PRIMARY KEY,
18 `game_id` INTEGER NOT NULL,
19 `title` VARCHAR(255) NOT NULL
20);
21
22CREATE TABLE `dids` (
23 `profile_id` INTEGER NOT NULL,
24 `achievement_id` INTEGER NOT NULL,
25 `achieved_at` DATETIME NOT NULL
26);
27
28CREATE TABLE `images` (
29 `image_id` INTEGER PRIMARY KEY,
30 `game_id` INTEGER NOT NULL,
31 `filename` VARCHAR(255) NOT NULL
32);
diff --git a/scrape.rb b/scrape.rb index a28f4c5..6f3a8e4 100644 --- a/scrape.rb +++ b/scrape.rb
@@ -1,42 +1,175 @@
1require 'json' 1require 'json'
2require 'nokogiri'
3require 'open-uri' 2require 'open-uri'
4require 'yaml' 3require 'yaml'
5 4
6config = YAML.load(open(ARGV[0])) 5require 'rubygems'
7usernames = config["usernames"] 6require 'bundler/setup'
7Bundler.require :default
8 8
9achieves = usernames.map do |username| 9@config = YAML.load(open(ARGV[0]))
10 page = Nokogiri::HTML(open("https://steamcommunity.com/#{username}/games/?tab=all")) 10db_existed = File.exists?(@config["database"])
11db = Sequel.connect("sqlite://" + @config["database"])
12
13if ARGV[1] == "init"
14 if db_existed
15 raise "Datafile already exists"
16 end
17
18 schema = File.read("schema.sql")
19
20 db.run schema
21
22 puts "Initialized datafile"
23
24 exit
25end
26
27class Profile < Sequel::Model
28 many_to_many :achievements, join_table: :dids
29end
30
31class Game < Sequel::Model
32 one_to_many :achievements
33 one_to_many :images
34end
35
36class Achievement < Sequel::Model
37 many_to_one :game
38 many_to_many :profiles, join_table: :dids
39end
40
41class Image < Sequel::Model
42 many_to_one :game
43end
44
45class Did < Sequel::Model
46 many_to_one :profile
47 many_to_one :achievement
48end
49
50@moonimgs = Dir.entries(@config["moon_images"]).select do |img|
51 img.end_with? ".png"
52end
53
54def scrape_profile(profile, full)
55 if full
56 url = "https://steamcommunity.com/#{profile.profile_path}/games/?tab=all"
57 else
58 url = "https://steamcommunity.com/#{profile.profile_path}/games/"
59 end
60
61 page = Nokogiri::HTML(open(url))
11 script = page.css(".responsive_page_template_content script").text[18..-1] 62 script = page.css(".responsive_page_template_content script").text[18..-1]
12 data = JSON.parse(script[0..script.index(";\r\n\t\t")-1]) 63 data = JSON.parse(script[0..script.index(";\r\n\t\t")-1])
13 ids = data.map { |d| d["appid"] } 64 ids = data.map { |d| d["appid"] }
14 65
15 index = 0 66 index = 0
16 ids.map do |id| 67 ids.each do |id|
17 index += 1 68 index += 1
18 puts "#{username} - #{index}/#{ids.count}" 69 puts "#{profile.profile_path} - #{index}/#{ids.count}"
19 70
20 achsp = Nokogiri::HTML(open("https://steamcommunity.com/#{username}/stats/#{id}/")) 71 achsp = Nokogiri::HTML(
21 achsp.css(".achieveTxt .achieveUnlockTime + h3").map { |d| d.text } 72 open("https://steamcommunity.com/#{profile.profile_path}/stats/#{id}/"))
22 end
23end.flatten
24 73
25if File.exists?(config["achievements"]) 74 achsp.css(".achieveTxt").each do |node|
26 already = File.read(config["achievements"]).split("\n") 75 unless node.css(".achieveUnlockTime").empty?
27 all_achieves = achieves + already 76 if Game.where(steam_appid: id).count > 0
28else 77 game = Game.where(steam_appid: id).first
29 all_achieves = achieves 78 else
30end 79 moon_index = Random.rand(@moonimgs.size)
80
81 game = Game.new(steam_appid: id, moon_image: @moonimgs[moon_index])
82 game.save
83
84 storepage = Nokogiri::HTML(
85 open("http://store.steampowered.com/app/#{id}"))
86
87 img_id = 0
88 storepage.css(".highlight_screenshot_link").each do |node|
89 begin
90 imagepage = open(node["href"]).read
91
92 img_id += 1
93 img_filename = "#{id}-#{img_id}.jpg"
94 img_filepath = File.join(@config["images"], img_filename)
95
96 img_file = File.open(img_filepath, "w")
97 img_file.write(imagepage)
98 img_file.close
99
100 image = Image.new(game: game, filename: img_filename)
101 image.save
102 rescue OpenURI::HTTPError
103 puts "Error downloading an image"
104 end
105
106 sleep 2
107 end
108 end
109
110 title = node.at_css("h3").text
31 111
32all_achieves.sort! 112 if game.achievements_dataset.where(title: title).count > 0
33all_achieves.uniq! 113 achievement = game.achievements_dataset.where(title: title).first
114 else
115 achievement = Achievement.new(game: game, title: title)
116 achievement.save
117 end
34 118
35if config.key? "blacklist" 119 unless Did.where(profile: profile, achievement: achievement).count > 0
36 blacklist = File.read(config["blacklist"]).split("\n") 120 begin
37 all_achieves.reject! { |l| blacklist.include? l } 121 unlock = DateTime.strptime(
122 node.css(".achieveUnlockTime").text.lstrip[9..-1],
123 "%b %d, %Y @ %l:%M%P")
124 rescue ArgumentError
125 unlock = DateTime.strptime(
126 node.css(".achieveUnlockTime").text.lstrip[9..-1],
127 "%b %d @ %l:%M%P")
128 end
129
130 join = Did.new(
131 profile: profile,
132 achievement: achievement,
133 achieved_at: unlock)
134 join.save
135 end
136 end
137 end
138 end
38end 139end
39 140
40File.open(config["achievements"], "w") do |f| 141if ARGV[1] == "add"
41 f << all_achieves.join("\n") 142 userpath = ARGV[2]
143
144 if Profile.where(profile_path: userpath).count > 0
145 raise "Profile " + userpath + " already exists"
146 end
147
148 profile = Profile.new(profile_path: userpath)
149 profile.save
150
151 scrape_profile profile, true
152elsif ARGV[1] == "update"
153 if ARGV.size == 3
154 scrape_profile Profile.where(profile_path: ARGV[2]).first, false
155 else
156 Profile.all.each do |profile|
157 scrape_profile profile, false
158 end
159 end
160elsif ARGV[1] == "full"
161 if ARGV.size == 3
162 scrape_profile Profile.where(profile_path: ARGV[2]).first, true
163 else
164 Profile.all.each do |profile|
165 scrape_profile profile, true
166 end
167 end
168elsif ARGV[1] == "recolor"
169 Game.all.each do |game|
170 moon_index = Random.rand(@moonimgs.size)
171
172 game.moon_image = @moonimgs[moon_index]
173 game.save
174 end
42end 175end