app.rb 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. # flickrlicense -- A thingy to update Flickr photo licenses
  2. # Copyright (C) 2017 Douglas Thrift
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published
  6. # by the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. require 'flickr/login'
  17. require 'flickraw'
  18. require 'pry'
  19. require 'sequel_enum'
  20. require 'sinatra'
  21. require 'sinatra/config_file'
  22. require 'sinatra/json'
  23. require 'sinatra/reloader'
  24. require 'sinatra/sequel'
  25. config_file 'config.yml'
  26. enable :sessions
  27. set :agpl_src_url, 'https://git.douglasthrift.net/douglas/flickrlicense'
  28. set :database, 'sqlite://db.sqlite3'
  29. FlickRaw.api_key = settings.flickr_api_key
  30. FlickRaw.shared_secret = settings.flickr_shared_secret
  31. migration 'create users, licenses, and photos tables' do
  32. database.create_table :users do
  33. column :nsid, String, primary_key: true
  34. column :username, String
  35. column :fullname, String
  36. # column :json, 'text'
  37. # foreign_key :show_license_id, :licenses, null: true, on_delete: :set_null, on_update: :restrict
  38. # column :show_privacy, Integer, default: 0, null: false
  39. # column :show_ignored, 'boolean', default: true, null: false
  40. end
  41. database.create_table :licenses do
  42. column :id, Integer, primary_key: true
  43. column :name, String
  44. column :url, String
  45. end
  46. database.create_table :photos do
  47. column :id, Integer, primary_key: true
  48. # foreign_key :user_id, :users, on_delete: :cascade, on_update: :restrict
  49. foreign_key :owner, :users
  50. # foreign_key :license_id, :licenses, on_delete: :cascade, on_update: :restrict
  51. foreign_key :license, :licenses
  52. column :json, 'text'
  53. column :ignore, 'boolean'
  54. # column :public, 'boolean'
  55. # column :friend, 'boolean'
  56. # column :family, 'boolean'
  57. end
  58. end
  59. migration 'rename photo owner and license columns to user_id and license_id' do
  60. database.alter_table :photos do
  61. drop_foreign_key [:owner]
  62. rename_column :owner, :user_id
  63. add_foreign_key [:user_id], :users
  64. drop_foreign_key [:license]
  65. rename_column :license, :license_id
  66. add_foreign_key [:license_id], :licenses
  67. end
  68. end
  69. migration 'add on delete and update constraints to photo user_id and license_id' do
  70. database.alter_table :photos do
  71. drop_foreign_key [:user_id]
  72. add_foreign_key [:user_id], :users, on_delete: :cascade, on_update: :restrict
  73. drop_foreign_key [:license_id]
  74. add_foreign_key [:license_id], :licenses, on_delete: :cascade, on_update: :restrict
  75. end
  76. end
  77. migration 'add not null constraint to photo user_id and license_id' do
  78. database.alter_table :photos do
  79. set_column_not_null :user_id
  80. set_column_not_null :license_id
  81. end
  82. end
  83. migration 'add json field to user' do
  84. database.alter_table :users do
  85. add_column :json, 'text'
  86. end
  87. database.select(:nsid).from(:users).select_map(:nsid).each do |nsid|
  88. info = OpenStruct.new(flickr.people.getInfo(user_id: nsid).to_hash)
  89. info.timezone = info.timezone.to_hash
  90. info.photos = info.photos.to_hash
  91. database.from(:users).where(nsid: nsid).update(json: info.to_h.to_json)
  92. end
  93. end
  94. migration 'add show license/privacy/ignored fields to user' do
  95. database.alter_table :users do
  96. add_foreign_key :show_license_id, :licenses, null: true, on_delete: :set_null, on_update: :restrict
  97. add_column :show_privacy, Integer, default: 0, null: false
  98. add_column :show_ignored, 'boolean', default: true, null: false
  99. end
  100. end
  101. migration 'add public/friend/family fields to photos' do
  102. database.alter_table :photos do
  103. add_column :public, 'boolean'
  104. add_column :friend, 'boolean'
  105. add_column :family, 'boolean'
  106. end
  107. database.select(:id, :json).from(:photos).select_map([:id, :json]).each do |id, json|
  108. photo = OpenStruct.new(JSON.parse(json))
  109. database.from(:photos).where(id: id).update(public: photo.ispublic != 0, friend: photo.isfriend != 0, family: photo.isfamily != 0)
  110. end
  111. end
  112. class User < Sequel::Model
  113. plugin :enum
  114. one_to_many :photos
  115. many_to_one :show_license, class: :License
  116. enum :show_privacy, [:all, :public, :friends_family, :friends, :family, :private]
  117. unrestrict_primary_key
  118. def flickraw
  119. @flickraw ||= OpenStruct.new(JSON.parse(json))
  120. end
  121. def buddyicon
  122. if flickraw.iconserver.to_i > 0
  123. "https://farm#{flickraw.iconfarm}.staticflickr.com/#{flickraw.iconserver}/buddyicons/#{nsid}.jpg"
  124. else
  125. "https://www.flickr.com/images/buddyicon.gif"
  126. end
  127. end
  128. def photosurl
  129. flickraw.photosurl
  130. end
  131. end
  132. class License < Sequel::Model
  133. one_to_many :photos
  134. unrestrict_primary_key
  135. def icon
  136. case url
  137. when %r{/by-nc-sa/}
  138. '<i class="cc cc-cc"></i><i class="cc cc-by"></i><i class="cc cc-nc"></i><i class="cc cc-sa"></i>'
  139. when %r{/by-nc/}
  140. '<i class="cc cc-cc"></i><i class="cc cc-by"></i><i class="cc cc-nc"></i>'
  141. when %r{/by-nc-nd/}
  142. '<i class="cc cc-cc"></i><i class="cc cc-by"></i><i class="cc cc-nc"></i><i class="cc cc-nd"></i>'
  143. when %r{/by/}
  144. '<i class="cc cc-cc"></i><i class="cc cc-by"></i>'
  145. when %r{/by-sa/}
  146. '<i class="cc cc-cc"></i><i class="cc cc-by"></i><i class="cc cc-sa"></i>'
  147. when %r{/by-nd/}
  148. '<i class="cc cc-cc"></i><i class="cc cc-by"></i><i class="cc cc-nd"></i>'
  149. when %r{/commons/}, %r{/mark/}
  150. '<i class="cc cc-pd-alt"></i>'
  151. when %r{/zero/}
  152. '<i class="cc cc-zero"></i>'
  153. else
  154. '©'
  155. end
  156. end
  157. def icon_name
  158. icon + ' ' + name
  159. end
  160. def as_json(*)
  161. {
  162. id: id,
  163. name: name,
  164. url: url,
  165. icon: icon,
  166. iconname: icon_name,
  167. }
  168. end
  169. def to_json(*args)
  170. as_json.to_json(*args)
  171. end
  172. end
  173. class Photo < Sequel::Model
  174. many_to_one :user
  175. many_to_one :license
  176. unrestrict_primary_key
  177. def flickraw
  178. @flickraw ||= OpenStruct.new(JSON.parse(json))
  179. end
  180. def as_json(*)
  181. {
  182. id: id,
  183. license: license_id,
  184. ignore: ignore,
  185. img: FlickRaw.url_q(flickraw),
  186. url: FlickRaw.url_photopage(flickraw),
  187. title: flickraw.title,
  188. public: public,
  189. friend: friend,
  190. family: family,
  191. path: "/photos/#{id}",
  192. }
  193. end
  194. def to_json(*args)
  195. as_json.to_json(*args)
  196. end
  197. end
  198. helpers Flickr::Login::Helpers
  199. helpers do
  200. def flickr
  201. unless @flickr
  202. @flickr = FlickRaw::Flickr.new(api_key: settings.flickr_api_key, shared_secret: settings.flickr_shared_secret)
  203. @flickr.access_token, @flickr.access_secret = flickr_access_token
  204. end
  205. @flickr
  206. end
  207. end
  208. before do
  209. redirect to('/login?perms=write') unless flickr_user
  210. @user = User.find_or_create(nsid: flickr_user[:user_nsid]) do |user|
  211. user.username = flickr_user[:username]
  212. user.fullname = flickr_user[:fullname]
  213. info = OpenStruct.new(flickr.people.getInfo(user_id: user.nsid).to_hash)
  214. info.timezone = info.timezone.to_hash
  215. info.photos = info.photos.to_hash
  216. user.json = info.to_h.to_json
  217. user.show_license = nil
  218. user.show_privacy = :all
  219. user.show_ignored = true
  220. end
  221. end
  222. get '/' do
  223. @licenses = License.all
  224. if @licenses.count == 0
  225. flickr.photos.licenses.getInfo.each do |flickr_license|
  226. License.create do |license|
  227. license.id = flickr_license.id
  228. license.name = flickr_license.name
  229. license.url = flickr_license.url
  230. end
  231. end
  232. @licenses = License.all
  233. end
  234. @show_privacies = {
  235. all: 'show public and private photos',
  236. public: 'show only public photos',
  237. friends_family: 'show only photos visible to friends and family',
  238. friends: 'show only photos visible to only friends',
  239. family: 'show only photos visible to only family',
  240. private: 'show only completely private photos',
  241. }
  242. @show_ignoreds = {
  243. true => 'show ignored photos',
  244. false => 'hide ignored photos',
  245. }
  246. erb :index
  247. end
  248. get '/logout' do
  249. flickr_clear
  250. redirect to('/')
  251. end
  252. get %r{/photos/([1-8])} do |page|
  253. @user.photos_dataset.delete if params['reload'] == 'true'
  254. page, per_page = page.to_i, 500
  255. photos = @user.photos_dataset.reverse(:id).limit(per_page, (page - 1) * per_page).all
  256. begin
  257. photos = flickr.photos.search(user_id: :me, extras: 'license', per_page: per_page, page: page).map do |flickr_photo|
  258. Photo.create do |photo|
  259. photo.id = flickr_photo.id
  260. photo.user = @user
  261. photo.license_id = flickr_photo.license
  262. photo.json = flickr_photo.to_hash.to_json
  263. photo.ignore = false
  264. photo.public = flickr_photo.ispublic != 0
  265. photo.friend = flickr_photo.isfriend != 0
  266. photo.family = flickr_photo.isfamily != 0
  267. end
  268. end if photos.count == 0
  269. rescue FlickRaw::Error => e
  270. halt 422, json(error: e.message)
  271. rescue Sequel::UniqueConstraintViolation
  272. # sometimes the Flickr API will just keep repeating the same results for subsequent pages
  273. end
  274. halt json path: "/photos/#{page + 1}", photos: photos if photos.count == per_page && page < 8
  275. json photos: photos
  276. end
  277. post '/user' do
  278. halt 422, json(error: 'Missing required parameter(s)') unless %w(show_license show_privacy show_ignored).any? {|param| params[param]}
  279. show_license_id = params['show_license']
  280. if show_license_id
  281. if show_license_id.empty?
  282. show_license = nil
  283. else
  284. show_license_id = show_license_id.to_i
  285. show_license = License[show_license_id]
  286. halt 422, json(error: "Could not find license with ID: #{show_license_id.inspect}") unless show_license
  287. end
  288. @user.show_license = show_license
  289. @user.save
  290. end
  291. show_privacy = params['show_privacy']
  292. if show_privacy
  293. @user.show_privacy = show_privacy
  294. begin
  295. @user.save
  296. rescue Sequel::NotNullConstraintViolation
  297. halt 422, json(error: "Invalid privacy value: #{show_privacy.inspect}")
  298. end
  299. end
  300. show_ignored = params['show_ignored']
  301. if show_ignored
  302. @user.show_ignored = show_ignored == 'true'
  303. @user.save
  304. end
  305. status 204
  306. end
  307. post '/photos' do
  308. ignore = params['ignore'] == 'true'
  309. ids = params['photos'] && params['photos'].is_a?(Array) ? params['photos'] : [params['photo']]
  310. photos = @user.photos_dataset.where(id: ids)
  311. halt 404, json(error: "Could not find photo(s) with ID(s): #{ids.map(&:to_i) - photos.map(:id)}") unless photos.count == ids.size
  312. photos.update(ignore: ignore)
  313. status 204
  314. end
  315. post '/photos/*' do |id|
  316. photo = @user.photos_dataset.where(id: id).first
  317. halt 404, json(error: "Could not find photo with ID: #{id}") unless photo
  318. license_id = params['license'] && params['license'].to_i
  319. license = License[license_id]
  320. halt 422, json(error: "Could not find license with ID: #{license_id.inspect}") unless license
  321. case @user.show_privacy
  322. when :all
  323. # no need to check privacy
  324. when :public
  325. halt 422, json(error: 'Could not change license of non public photo') unless photo.public
  326. when :friends_family
  327. halt 422, json(error: 'Could not change license of non friends and family photo') unless photo.friend && photo.family
  328. when :friends
  329. halt 422, json(error: 'Could not change license of non only friends photo') unless photo.friend && !photo.family
  330. when :family
  331. halt 422, json(error: 'Could not change license of non only family photo') unless !photo.friend && photo.family
  332. when :private
  333. halt 422, json(error: 'Could not change license of non private photo') unless !photo.public && !photo.friend && !photo.family
  334. end
  335. halt 422, json(error: 'Could not change license of photo with unselected license') if @user.show_license && @user.show_license != photo.license
  336. halt 422, json(error: 'Could not change license of ignored photo') if photo.ignore
  337. begin
  338. flickr.photos.licenses.setLicense(photo_id: photo.id, license_id: license.id)
  339. rescue FlickRaw::Error => e
  340. halt 422, json(error: e.message)
  341. end
  342. photo.update(license: license)
  343. status 204
  344. end