loading
Generated 2024-05-06T18:22:10+09:00

All Files ( 74.85% covered at 678.7 hits/line )

1 files in total.
497 relevant lines, 372 lines covered and 125 lines missed. ( 74.85% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
app.rb 74.85 % 1043 497 372 125 678.70

app.rb

74.85% lines covered

497 relevant lines. 372 lines covered and 125 lines missed.
    
  1. # frozen_string_literal: true
  2. require 'base64'
  3. require 'bcrypt'
  4. require 'digest'
  5. require 'mysql2'
  6. require 'mysql2-cs-bind'
  7. require 'open3'
  8. require 'securerandom'
  9. require 'sinatra/base'
  10. require 'sinatra/json'
  11. module Isupipe
  12. class App < Sinatra::Base
  13. enable :logging
  14. set :show_exceptions, :after_handler
  15. set :sessions, domain: 'u.isucon.dev', path: '/', expire_after: 1000*60
  16. set :session_secret, ENV.fetch('ISUCON13_SESSION_SECRETKEY', 'isucon13_session_cookiestore_defaultsecret').unpack('H*')[0]
  17. POWERDNS_SUBDOMAIN_ADDRESS = ENV.fetch('ISUCON13_POWERDNS_SUBDOMAIN_ADDRESS')
  18. DEFAULT_SESSION_ID_KEY = 'SESSIONID'
  19. DEFAULT_SESSION_EXPIRES_KEY = 'EXPIRES'
  20. DEFAULT_USER_ID_KEY = 'USERID'
  21. DEFAULT_USERNAME_KEY = 'USERNAME'
  22. class HttpError < StandardError
  23. attr_reader :code
  24. def initialize(code, message = nil)
  25. 13 super(message || "HTTP error #{code}")
  26. 13 @code = code
  27. end
  28. end
  29. error HttpError do
  30. 13 e = env['sinatra.error']
  31. 13 status e.code
  32. 13 json(error: e.message)
  33. end
  34. helpers do
  35. def db_conn
  36. 11450 Thread.current[:db_conn] ||= connect_db
  37. end
  38. def connect_db
  39. 21 Mysql2::Client.new(
  40. host: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_ADDRESS', '127.0.0.1'),
  41. port: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_PORT', '3306').to_i,
  42. username: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_USER', 'isucon'),
  43. password: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_PASSWORD', 'isucon'),
  44. database: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_DATABASE', 'isupipe'),
  45. symbolize_keys: true,
  46. cast_booleans: true,
  47. reconnect: true,
  48. )
  49. end
  50. def db_transaction(&block)
  51. 3818 db_conn.query('BEGIN')
  52. 3818 ok = false
  53. begin
  54. 3818 retval = block.call(db_conn)
  55. 3802 db_conn.query('COMMIT')
  56. 3802 ok = true
  57. 3802 retval
  58. ensure
  59. 3814 unless ok
  60. 12 db_conn.query('ROLLBACK')
  61. end
  62. end
  63. end
  64. def decode_request_body(data_class)
  65. 1727 body = JSON.parse(request.body.tap(&:rewind).read, symbolize_names: true)
  66. 6308 data_class.new(**data_class.members.map { |key| [key, body[key]] }.to_h)
  67. end
  68. def cast_as_integer(str)
  69. 755 Integer(str, 10)
  70. rescue
  71. raise HttpError.new(400)
  72. end
  73. def verify_user_session!
  74. 1410 sess = session[DEFAULT_SESSION_ID_KEY]
  75. 1410 unless sess
  76. raise HttpError.new(403)
  77. end
  78. 1410 session_expires = sess[DEFAULT_SESSION_EXPIRES_KEY]
  79. 1410 unless session_expires
  80. raise HttpError.new(403)
  81. end
  82. 1410 now = Time.now
  83. 1410 if now.to_i > session_expires
  84. raise HttpError.new(401, 'session has expired')
  85. end
  86. nil
  87. end
  88. def fill_livestream_response(tx, livestream_model)
  89. 4714 owner_model = tx.xquery('SELECT * FROM users WHERE id = ?', livestream_model.fetch(:user_id)).first
  90. 4714 owner = fill_user_response(tx, owner_model)
  91. 4714 tags = tx.xquery('SELECT * FROM livestream_tags WHERE livestream_id = ?', livestream_model.fetch(:id)).map do |livestream_tag_model|
  92. 15757 tag_model = tx.xquery('SELECT * FROM tags WHERE id = ?', livestream_tag_model.fetch(:tag_id)).first
  93. {
  94. 15757 id: tag_model.fetch(:id),
  95. name: tag_model.fetch(:name),
  96. }
  97. end
  98. 4714 livestream_model.slice(:id, :title, :description, :playlist_url, :thumbnail_url, :start_at, :end_at).merge(
  99. owner:,
  100. tags:,
  101. )
  102. end
  103. def fill_livecomment_response(tx, livecomment_model)
  104. 1096 comment_owner_model = tx.xquery('SELECT * FROM users WHERE id = ?', livecomment_model.fetch(:user_id)).first
  105. 1096 comment_owner = fill_user_response(tx, comment_owner_model)
  106. 1096 livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livecomment_model.fetch(:livestream_id)).first
  107. 1096 livestream = fill_livestream_response(tx, livestream_model)
  108. 1096 livecomment_model.slice(:id, :comment, :tip, :created_at).merge(
  109. user: comment_owner,
  110. livestream:,
  111. )
  112. end
  113. def fill_livecomment_report_response(tx, report_model)
  114. 62 reporter_model = tx.xquery('SELECT * FROM users WHERE id = ?', report_model.fetch(:user_id)).first
  115. 62 reporter = fill_user_response(tx, reporter_model)
  116. 62 livecomment_model = tx.xquery('SELECT * FROM livecomments WHERE id = ?', report_model.fetch(:livecomment_id)).first
  117. 62 livecomment = fill_livecomment_response(tx, livecomment_model)
  118. 62 report_model.slice(:id, :created_at).merge(
  119. reporter:,
  120. livecomment:,
  121. )
  122. end
  123. def fill_reaction_response(tx, reaction_model)
  124. 863 user_model = tx.xquery('SELECT * FROM users WHERE id = ?', reaction_model.fetch(:user_id)).first
  125. 863 user = fill_user_response(tx, user_model)
  126. 863 livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', reaction_model.fetch(:livestream_id)).first
  127. 863 livestream = fill_livestream_response(tx, livestream_model)
  128. 863 reaction_model.slice(:id, :emoji_name, :created_at).merge(
  129. user:,
  130. livestream:,
  131. )
  132. end
  133. def fill_user_response(tx, user_model)
  134. 7172 theme_model = tx.xquery('SELECT * FROM themes WHERE user_id = ?', user_model.fetch(:id)).first
  135. 7172 icon_model = tx.xquery('SELECT image FROM icons WHERE user_id = ?', user_model.fetch(:id)).first
  136. image =
  137. 7172 if icon_model
  138. 4712 icon_model.fetch(:image)
  139. else
  140. 2460 File.binread(FALLBACK_IMAGE)
  141. end
  142. 7172 icon_hash = Digest::SHA256.hexdigest(image)
  143. {
  144. 7172 id: user_model.fetch(:id),
  145. name: user_model.fetch(:name),
  146. display_name: user_model.fetch(:display_name),
  147. description: user_model.fetch(:description),
  148. theme: {
  149. id: theme_model.fetch(:id),
  150. dark_mode: theme_model.fetch(:dark_mode),
  151. },
  152. icon_hash:,
  153. }
  154. end
  155. end
  156. # 初期化
  157. post '/api/initialize' do
  158. 1 out, status = Open3.capture2e('../sql/init.sh')
  159. 1 unless status.success?
  160. logger.warn("init.sh failed with out=#{out}")
  161. halt 500
  162. end
  163. 1 json(
  164. language: 'ruby',
  165. )
  166. end
  167. # top
  168. get '/api/tag' do
  169. 62 tag_models = db_transaction do |tx|
  170. 62 tx.query('SELECT * FROM tags')
  171. end
  172. 62 json(
  173. tags: tag_models.map { |tag_model|
  174. {
  175. 6386 id: tag_model.fetch(:id),
  176. name: tag_model.fetch(:name),
  177. }
  178. },
  179. )
  180. end
  181. # 配信者のテーマ取得API
  182. get '/api/user/:username/theme' do
  183. 5 verify_user_session!
  184. 5 username = params[:username]
  185. 5 theme_model = db_transaction do |tx|
  186. 5 user_model = tx.xquery('SELECT id FROM users WHERE name = ?', username).first
  187. 5 unless user_model
  188. raise HttpError.new(404)
  189. end
  190. 5 tx.xquery('SELECT * FROM themes WHERE user_id = ?', user_model.fetch(:id)).first
  191. end
  192. 5 json(
  193. id: theme_model.fetch(:id),
  194. dark_mode: theme_model.fetch(:dark_mode),
  195. )
  196. end
  197. # livestream
  198. ReserveLivestreamRequest = Data.define(
  199. :tags,
  200. :title,
  201. :description,
  202. :playlist_url,
  203. :thumbnail_url,
  204. :start_at,
  205. :end_at,
  206. )
  207. # reserve livestream
  208. post '/api/livestream/reservation' do
  209. 84 verify_user_session!
  210. 84 sess = session[DEFAULT_SESSION_ID_KEY]
  211. 84 unless sess
  212. raise HttpError.new(401)
  213. end
  214. 84 user_id = sess[DEFAULT_USER_ID_KEY]
  215. 84 unless sess
  216. raise HttpError.new(401)
  217. end
  218. 84 req = decode_request_body(ReserveLivestreamRequest)
  219. 84 livestream = db_transaction do |tx|
  220. # 2023/11/25 10:00からの1年間の期間内であるかチェック
  221. 84 term_start_at = Time.utc(2023, 11, 25, 1)
  222. 84 term_end_at = Time.utc(2024, 11, 25, 1)
  223. 84 reserve_start_at = Time.at(req.start_at, in: 'UTC')
  224. 84 reserve_end_at = Time.at(req.end_at, in: 'UTC')
  225. 84 if reserve_start_at >= term_end_at || reserve_end_at <= term_start_at
  226. 2 raise HttpError.new(400, 'bad reservation time range')
  227. end
  228. # 予約枠をみて、予約が可能か調べる
  229. # NOTE: 並列な予約のoverbooking防止にFOR UPDATEが必要
  230. 82 tx.xquery('SELECT * FROM reservation_slots WHERE start_at >= ? AND end_at <= ? FOR UPDATE', req.start_at, req.end_at).each do |slot|
  231. 629 count = tx.xquery('SELECT slot FROM reservation_slots WHERE start_at = ? AND end_at = ?', slot.fetch(:start_at), slot.fetch(:end_at)).first.fetch(:slot)
  232. 629 logger.info("#{slot.fetch(:start_at)} ~ #{slot.fetch(:end_at)}予約枠の残数 = #{slot.fetch(:slot)}")
  233. 629 if count < 1
  234. 1 raise HttpError.new(400, "予約期間 #{term_start_at.to_i} ~ #{term_end_at.to_i}に対して、予約区間 #{req.start_at} ~ #{req.end_at}が予約できません")
  235. end
  236. end
  237. 81 tx.xquery('UPDATE reservation_slots SET slot = slot - 1 WHERE start_at >= ? AND end_at <= ?', req.start_at, req.end_at)
  238. 81 tx.xquery('INSERT INTO livestreams (user_id, title, description, playlist_url, thumbnail_url, start_at, end_at) VALUES(?, ?, ?, ?, ?, ?, ?)', user_id, req.title, req.description, req.playlist_url, req.thumbnail_url, req.start_at, req.end_at)
  239. 81 livestream_id = tx.last_id
  240. # タグ追加
  241. 81 req.tags.each do |tag_id|
  242. 327 tx.xquery('INSERT INTO livestream_tags (livestream_id, tag_id) VALUES (?, ?)', livestream_id, tag_id)
  243. end
  244. 81 fill_livestream_response(tx, {
  245. id: livestream_id,
  246. user_id:,
  247. title: req.title,
  248. description: req.description,
  249. playlist_url: req.playlist_url,
  250. thumbnail_url: req.thumbnail_url,
  251. start_at: req.start_at,
  252. end_at: req.end_at,
  253. })
  254. end
  255. 81 status 201
  256. 81 json(livestream)
  257. end
  258. # list livestream
  259. get '/api/livestream/search' do
  260. 35 key_tag_name = params[:tag] || ''
  261. 35 livestreams = db_transaction do |tx|
  262. livestream_models =
  263. 35 if key_tag_name != ''
  264. # タグによる取得
  265. 13 tag_id_list = tx.xquery('SELECT id FROM tags WHERE name = ?', key_tag_name, as: :array).map(&:first)
  266. 13 tx.xquery('SELECT * FROM livestream_tags WHERE tag_id IN (?) ORDER BY livestream_id DESC', tag_id_list).map do |key_tagged_livestream|
  267. 1479 tx.xquery('SELECT * FROM livestreams WHERE id = ?', key_tagged_livestream.fetch(:livestream_id)).first
  268. end
  269. else
  270. # 検索条件なし
  271. 22 query = 'SELECT * FROM livestreams ORDER BY id DESC'
  272. 22 limit_str = params[:limit] || ''
  273. 22 if limit_str != ''
  274. 22 limit = cast_as_integer(limit_str)
  275. 22 query = "#{query} LIMIT #{limit}"
  276. end
  277. 22 tx.xquery(query).to_a
  278. end
  279. 35 livestream_models.map do |livestream_model|
  280. 2579 fill_livestream_response(tx, livestream_model)
  281. end
  282. end
  283. 35 json(livestreams)
  284. end
  285. get '/api/livestream' do
  286. 76 verify_user_session!
  287. 76 sess = session[DEFAULT_SESSION_ID_KEY]
  288. 76 unless sess
  289. raise HttpError.new(401)
  290. end
  291. 76 user_id = sess[DEFAULT_USER_ID_KEY]
  292. 76 unless sess
  293. raise HttpError.new(401)
  294. end
  295. 76 livestreams = db_transaction do |tx|
  296. 76 tx.xquery('SELECT * FROM livestreams WHERE user_id = ?', user_id).map do |livestream_model|
  297. 94 fill_livestream_response(tx, livestream_model)
  298. end
  299. end
  300. 76 json(livestreams)
  301. end
  302. get '/api/user/:username/livestream' do
  303. verify_user_session!
  304. username = params[:username]
  305. livestreams = db_transaction do |tx|
  306. user = tx.xquery('SELECT * FROM users WHERE name = ?', username).first
  307. unless user
  308. raise HttpError.new(404, 'user not found')
  309. end
  310. tx.xquery('SELECT * FROM livestreams WHERE user_id = ?', user.fetch(:id)).map do |livestream_model|
  311. fill_livestream_response(tx, livestream_model)
  312. end
  313. end
  314. json(livestreams)
  315. end
  316. # ユーザ視聴開始 (viewer)
  317. post '/api/livestream/:livestream_id/enter' do
  318. 19 verify_user_session!
  319. 19 sess = session[DEFAULT_SESSION_ID_KEY]
  320. 19 unless sess
  321. raise HttpError.new(401)
  322. end
  323. 19 user_id = sess[DEFAULT_USER_ID_KEY]
  324. 19 unless sess
  325. raise HttpError.new(401)
  326. end
  327. 19 livestream_id = cast_as_integer(params[:livestream_id])
  328. 19 db_transaction do |tx|
  329. 19 created_at = Time.now.to_i
  330. 19 tx.xquery('INSERT INTO livestream_viewers_history (user_id, livestream_id, created_at) VALUES(?, ?, ?)', user_id, livestream_id, created_at)
  331. end
  332. 19 ''
  333. end
  334. # ユーザ視聴終了 (viewer)
  335. delete '/api/livestream/:livestream_id/exit' do
  336. 9 verify_user_session!
  337. 9 sess = session[DEFAULT_SESSION_ID_KEY]
  338. 9 unless sess
  339. raise HttpError.new(401)
  340. end
  341. 9 user_id = sess[DEFAULT_USER_ID_KEY]
  342. 9 unless sess
  343. raise HttpError.new(401)
  344. end
  345. 9 livestream_id = cast_as_integer(params[:livestream_id])
  346. 9 db_transaction do |tx|
  347. 9 tx.xquery('DELETE FROM livestream_viewers_history WHERE user_id = ? AND livestream_id = ?', user_id, livestream_id)
  348. end
  349. 9 ''
  350. end
  351. # get livestream
  352. get '/api/livestream/:livestream_id' do
  353. 1 verify_user_session!
  354. 1 livestream_id = cast_as_integer(params[:livestream_id])
  355. 1 livestream = db_transaction do |tx|
  356. 1 livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first
  357. 1 unless livestream_model
  358. raise HttpError.new(404)
  359. end
  360. 1 fill_livestream_response(tx, livestream_model)
  361. end
  362. 1 json(livestream)
  363. end
  364. # (配信者向け)ライブコメントの報告一覧取得API
  365. get '/api/livestream/:livestream_id/report' do
  366. 21 verify_user_session!
  367. 21 sess = session[DEFAULT_SESSION_ID_KEY]
  368. 21 unless sess
  369. raise HttpError.new(401)
  370. end
  371. 21 user_id = sess[DEFAULT_USER_ID_KEY]
  372. 21 unless sess
  373. raise HttpError.new(401)
  374. end
  375. 21 livestream_id = cast_as_integer(params[:livestream_id])
  376. 21 reports = db_transaction do |tx|
  377. 21 livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first
  378. 21 if livestream_model.fetch(:user_id) != user_id
  379. raise HttpError.new(403, "can't get other streamer's livecomment reports")
  380. end
  381. 21 tx.xquery('SELECT * FROM livecomment_reports WHERE livestream_id = ?', livestream_id).map do |report_model|
  382. 20 fill_livecomment_report_response(tx, report_model)
  383. end
  384. end
  385. 21 json(reports)
  386. end
  387. # get polling livecomment timeline
  388. get '/api/livestream/:livestream_id/livecomment' do
  389. 160 verify_user_session!
  390. 160 livestream_id = cast_as_integer(params[:livestream_id])
  391. 160 livecomments = db_transaction do |tx|
  392. 160 query = 'SELECT * FROM livecomments WHERE livestream_id = ? ORDER BY created_at DESC'
  393. 160 limit_str = params[:limit] || ''
  394. 160 if limit_str != ''
  395. 18 limit = cast_as_integer(limit_str)
  396. 18 query = "#{query} LIMIT #{limit}"
  397. end
  398. 160 tx.xquery(query, livestream_id).map do |livecomment_model|
  399. 864 fill_livecomment_response(tx, livecomment_model)
  400. end
  401. end
  402. 160 json(livecomments)
  403. end
  404. get '/api/livestream/:livestream_id/ngwords' do
  405. 15 verify_user_session!
  406. 15 sess = session[DEFAULT_SESSION_ID_KEY]
  407. 15 unless sess
  408. raise HttpError.new(401)
  409. end
  410. 15 user_id = sess[DEFAULT_USER_ID_KEY]
  411. 15 unless sess
  412. raise HttpError.new(401)
  413. end
  414. 15 livestream_id = cast_as_integer(params[:livestream_id])
  415. 15 ng_words = db_transaction do |tx|
  416. 15 tx.xquery('SELECT * FROM ng_words WHERE user_id = ? AND livestream_id = ? ORDER BY created_at DESC', user_id, livestream_id).to_a
  417. end
  418. 15 json(ng_words)
  419. end
  420. PostLivecommentRequest = Data.define(
  421. :comment,
  422. :tip,
  423. )
  424. # ライブコメント投稿
  425. post '/api/livestream/:livestream_id/livecomment' do
  426. 171 verify_user_session!
  427. 171 sess = session[DEFAULT_SESSION_ID_KEY]
  428. 171 unless sess
  429. raise HttpError.new(401)
  430. end
  431. 171 user_id = sess[DEFAULT_USER_ID_KEY]
  432. 171 unless sess
  433. raise HttpError.new(401)
  434. end
  435. 171 livestream_id = cast_as_integer(params[:livestream_id])
  436. 171 req = decode_request_body(PostLivecommentRequest)
  437. 171 livecomment = db_transaction do |tx|
  438. 171 livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first
  439. 171 unless livestream_model
  440. raise HttpError.new(404, 'livestream not found')
  441. end
  442. # スパム判定
  443. 171 tx.xquery('SELECT id, user_id, livestream_id, word FROM ng_words WHERE user_id = ? AND livestream_id = ?', livestream_model.fetch(:user_id), livestream_model.fetch(:id)).each do |ng_word|
  444. 33 query = <<~SQL
  445. SELECT COUNT(*)
  446. FROM
  447. (SELECT ? AS text) AS texts
  448. INNER JOIN
  449. (SELECT CONCAT('%', ?, '%') AS pattern) AS patterns
  450. ON texts.text LIKE patterns.pattern
  451. SQL
  452. 33 hit_spam = tx.xquery(query, req.comment, ng_word.fetch(:word), as: :array).first[0]
  453. 33 logger.info("[hit_spam=#{hit_spam}] comment = #{req.comment}")
  454. 33 if hit_spam >= 1
  455. 1 raise HttpError.new(400, 'このコメントがスパム判定されました')
  456. end
  457. end
  458. 170 now = Time.now.to_i
  459. 170 tx.xquery('INSERT INTO livecomments (user_id, livestream_id, comment, tip, created_at) VALUES (?, ?, ?, ?, ?)', user_id, livestream_id, req.comment, req.tip, now)
  460. 170 livecomment_id = tx.last_id
  461. 170 fill_livecomment_response(tx, {
  462. id: livecomment_id,
  463. user_id:,
  464. livestream_id:,
  465. comment: req.comment,
  466. tip: req.tip,
  467. created_at: now,
  468. })
  469. end
  470. 170 status 201
  471. 170 json(livecomment)
  472. end
  473. # ライブコメント報告
  474. post '/api/livestream/:livestream_id/livecomment/:livecomment_id/report' do
  475. 48 verify_user_session!
  476. 48 sess = session[DEFAULT_SESSION_ID_KEY]
  477. 48 unless sess
  478. raise HttpError.new(401)
  479. end
  480. 48 user_id = sess[DEFAULT_USER_ID_KEY]
  481. 48 unless user_id
  482. raise HttpError.new(401)
  483. end
  484. 48 livestream_id = cast_as_integer(params[:livestream_id])
  485. 48 livecomment_id = cast_as_integer(params[:livecomment_id])
  486. 48 report = db_transaction do |tx|
  487. 48 livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first
  488. 48 unless livestream_model
  489. raise HttpError.new(404, 'livestream not found')
  490. end
  491. 48 livecomment_model = tx.xquery('SELECT * FROM livecomments WHERE id = ?', livecomment_id).first
  492. 48 unless livecomment_model
  493. 6 raise HttpError.new(404, 'livecomment not found')
  494. end
  495. 42 now = Time.now.to_i
  496. 42 tx.xquery('INSERT INTO livecomment_reports(user_id, livestream_id, livecomment_id, created_at) VALUES (?, ?, ?, ?)', user_id, livestream_id, livecomment_id, now)
  497. 42 report_id = tx.last_id
  498. 42 fill_livecomment_report_response(tx, {
  499. id: report_id,
  500. user_id:,
  501. livestream_id:,
  502. livecomment_id:,
  503. created_at: now,
  504. })
  505. end
  506. 42 status 201
  507. 42 json(report)
  508. end
  509. ModerateRequest = Data.define(:ng_word)
  510. # 配信者によるモデレーション (NGワード登録)
  511. post '/api/livestream/:livestream_id/moderate' do
  512. 13 verify_user_session!
  513. 13 sess = session[DEFAULT_SESSION_ID_KEY]
  514. 13 unless sess
  515. raise HttpError.new(401)
  516. end
  517. 13 user_id = sess[DEFAULT_USER_ID_KEY]
  518. 13 unless user_id
  519. raise HttpError.new(401)
  520. end
  521. 13 livestream_id = cast_as_integer(params[:livestream_id])
  522. 13 req = decode_request_body(ModerateRequest)
  523. 13 word_id = db_transaction do |tx|
  524. # 配信者自身の配信に対するmoderateなのかを検証
  525. 13 owned_livestreams = tx.xquery('SELECT * FROM livestreams WHERE id = ? AND user_id = ?', livestream_id, user_id).to_a
  526. 13 if owned_livestreams.empty?
  527. raise HttpError.new(400, "A streamer can't moderate livestreams that other streamers own")
  528. end
  529. 13 tx.xquery('INSERT INTO ng_words(user_id, livestream_id, word, created_at) VALUES (?, ?, ?, ?)', user_id, livestream_id, req.ng_word, Time.now.to_i)
  530. 13 word_id = tx.last_id
  531. # NGワードにヒットする過去の投稿も全削除する
  532. 13 tx.xquery('SELECT * FROM ng_words WHERE livestream_id = ?', livestream_id).each do |ng_word|
  533. # ライブコメント一覧取得
  534. 14 tx.xquery('SELECT * FROM livecomments').each do |livecomment|
  535. 15411 query = <<~SQL
  536. DELETE FROM livecomments
  537. WHERE
  538. id = ? AND
  539. livestream_id = ? AND
  540. (SELECT COUNT(*)
  541. FROM
  542. (SELECT ? AS text) AS texts
  543. INNER JOIN
  544. (SELECT CONCAT('%', ?, '%') AS pattern) AS patterns
  545. ON texts.text LIKE patterns.pattern) >= 1
  546. SQL
  547. 15411 tx.xquery(query, livecomment.fetch(:id), livestream_id, livecomment.fetch(:comment), ng_word.fetch(:word))
  548. end
  549. end
  550. 13 word_id
  551. end
  552. 13 status 201
  553. 13 json(word_id:)
  554. end
  555. get '/api/livestream/:livestream_id/reaction' do
  556. 191 verify_user_session!
  557. 191 livestream_id = cast_as_integer(params[:livestream_id])
  558. 191 reactions = db_transaction do |tx|
  559. 191 query = 'SELECT * FROM reactions WHERE livestream_id = ? ORDER BY created_at DESC'
  560. 191 limit_str = params[:limit] || ''
  561. 191 if limit_str != ''
  562. 17 limit = cast_as_integer(limit_str)
  563. 17 query = "#{query} LIMIT #{limit}"
  564. end
  565. 191 tx.xquery(query, livestream_id).map do |reaction_model|
  566. 709 fill_reaction_response(tx, reaction_model)
  567. end
  568. end
  569. 191 json(reactions)
  570. end
  571. PostReactionRequest = Data.define(:emoji_name)
  572. post '/api/livestream/:livestream_id/reaction' do
  573. 154 verify_user_session!
  574. 154 sess = session[DEFAULT_SESSION_ID_KEY]
  575. 154 unless sess
  576. raise HttpError.new(401)
  577. end
  578. 154 user_id = sess[DEFAULT_USER_ID_KEY]
  579. 154 unless user_id
  580. raise HttpError.new(401)
  581. end
  582. 154 livestream_id = Integer(params[:livestream_id], 10)
  583. 154 req = decode_request_body(PostReactionRequest)
  584. 154 reaction = db_transaction do |tx|
  585. 154 created_at = Time.now.to_i
  586. 154 tx.xquery('INSERT INTO reactions (user_id, livestream_id, emoji_name, created_at) VALUES (?, ?, ?, ?)', user_id, livestream_id, req.emoji_name, created_at)
  587. 154 reaction_id = tx.last_id
  588. 154 fill_reaction_response(tx, {
  589. id: reaction_id,
  590. user_id:,
  591. livestream_id:,
  592. emoji_name: req.emoji_name,
  593. created_at:,
  594. })
  595. end
  596. 154 status 201
  597. 154 json(reaction)
  598. end
  599. BCRYPT_DEFAULT_COST = 4
  600. FALLBACK_IMAGE = '../img/NoImage.jpg'
  601. get '/api/user/:username/icon' do
  602. 1437 username = params[:username]
  603. 1437 image = db_transaction do |tx|
  604. 1437 user = tx.xquery('SELECT * FROM users WHERE name = ?', username).first
  605. 1437 unless user
  606. raise HttpError.new(404, 'not found user that has the given username')
  607. end
  608. 1437 tx.xquery('SELECT image FROM icons WHERE user_id = ?', user.fetch(:id)).first
  609. end
  610. 1437 content_type 'image/jpeg'
  611. 1437 if image
  612. 920 image[:image]
  613. else
  614. 517 send_file FALLBACK_IMAGE
  615. end
  616. end
  617. PostIconRequest = Data.define(:image)
  618. post '/api/icon' do
  619. 431 verify_user_session!
  620. 431 sess = session[DEFAULT_SESSION_ID_KEY]
  621. 431 unless sess
  622. raise HttpError.new(401)
  623. end
  624. 431 user_id = sess[DEFAULT_USER_ID_KEY]
  625. 431 unless user_id
  626. raise HttpError.new(401)
  627. end
  628. 431 req = decode_request_body(PostIconRequest)
  629. 431 image = Base64.decode64(req.image)
  630. 431 icon_id = db_transaction do |tx|
  631. 431 tx.xquery('DELETE FROM icons WHERE user_id = ?', user_id)
  632. 431 tx.xquery('INSERT INTO icons (user_id, image) VALUES (?, ?)', user_id, image)
  633. 431 tx.last_id
  634. end
  635. 431 status 201
  636. 431 json(
  637. id: icon_id,
  638. )
  639. end
  640. get '/api/user/me' do
  641. 3 verify_user_session!
  642. 3 sess = session[DEFAULT_SESSION_ID_KEY]
  643. 3 unless sess
  644. raise HttpError.new(401)
  645. end
  646. 3 user_id = sess[DEFAULT_USER_ID_KEY]
  647. 3 unless user_id
  648. raise HttpError.new(401)
  649. end
  650. 3 user = db_transaction do |tx|
  651. 3 user_model = tx.xquery('SELECT * FROM users WHERE id = ?', user_id).first
  652. 3 unless user_model
  653. raise HttpError.new(404)
  654. end
  655. 3 fill_user_response(tx, user_model)
  656. end
  657. 3 json(user)
  658. end
  659. PostUserRequest = Data.define(
  660. :name,
  661. :display_name,
  662. :description,
  663. # password is non-hashed password.
  664. :password,
  665. :theme,
  666. )
  667. # ユーザ登録API
  668. post '/api/register' do
  669. 435 req = decode_request_body(PostUserRequest)
  670. 435 if req.name == 'pipe'
  671. 1 raise HttpError.new(400, "the username 'pipe' is reserved")
  672. end
  673. 434 hashed_password = BCrypt::Password.create(req.password, cost: BCRYPT_DEFAULT_COST)
  674. 434 user = db_transaction do |tx|
  675. 434 tx.xquery('INSERT INTO users (name, display_name, description, password) VALUES(?, ?, ?, ?)', req.name, req.display_name, req.description, hashed_password)
  676. 433 user_id = tx.last_id
  677. 433 tx.xquery('INSERT INTO themes (user_id, dark_mode) VALUES(?, ?)', user_id, req.theme.fetch(:dark_mode))
  678. 433 out, status = Open3.capture2e('pdnsutil', 'add-record', 'u.isucon.dev', req.name, 'A', '0', POWERDNS_SUBDOMAIN_ADDRESS)
  679. 433 unless status.success?
  680. raise HttpError.new(500, "pdnsutil failed with out=#{out}")
  681. end
  682. 433 fill_user_response(tx, {
  683. id: user_id,
  684. name: req.name,
  685. display_name: req.display_name,
  686. description: req.description,
  687. })
  688. end
  689. 433 status 201
  690. 433 json(user)
  691. end
  692. LoginRequest = Data.define(
  693. :username,
  694. # password is non-hashed password.
  695. :password,
  696. )
  697. # ユーザログインAPI
  698. post '/api/login' do
  699. 439 req = decode_request_body(LoginRequest)
  700. 439 user_model = db_transaction do |tx|
  701. # usernameはUNIQUEなので、whereで一意に特定できる
  702. 439 tx.xquery('SELECT * FROM users WHERE name = ?', req.username).first.tap do |user_model|
  703. 439 unless user_model
  704. 1 raise HttpError.new(401, 'invalid username or password')
  705. end
  706. end
  707. end
  708. 438 unless BCrypt::Password.new(user_model.fetch(:password)).is_password?(req.password)
  709. 1 raise HttpError.new(401, 'invalid username or password')
  710. end
  711. 437 session_end_at = Time.now + 10*60*60
  712. 437 session_id = SecureRandom.uuid
  713. 437 session[DEFAULT_SESSION_ID_KEY] = {
  714. DEFAULT_SESSION_ID_KEY => session_id,
  715. DEFAULT_USER_ID_KEY => user_model.fetch(:id),
  716. DEFAULT_USERNAME_KEY => user_model.fetch(:name),
  717. DEFAULT_SESSION_EXPIRES_KEY => session_end_at.to_i,
  718. }
  719. 437 ''
  720. end
  721. # ユーザ詳細API
  722. get '/api/user/:username' do
  723. 1 verify_user_session!
  724. 1 username = params[:username]
  725. 1 user = db_transaction do |tx|
  726. 1 user_model = tx.xquery('SELECT * FROM users WHERE name = ?', username).first
  727. 1 unless user_model
  728. raise HttpError.new(404)
  729. end
  730. 1 fill_user_response(tx, user_model)
  731. end
  732. 1 json(user)
  733. end
  734. UserRankingEntry = Data.define(:username, :score)
  735. get '/api/user/:username/statistics' do
  736. 6 verify_user_session!
  737. 6 username = params[:username]
  738. # ユーザごとに、紐づく配信について、累計リアクション数、累計ライブコメント数、累計売上金額を算出
  739. # また、現在の合計視聴者数もだす
  740. 6 stats = db_transaction do |tx|
  741. 6 user = tx.xquery('SELECT * FROM users WHERE name = ?', username).first
  742. 6 unless user
  743. raise HttpError.new(400)
  744. end
  745. # ランク算出
  746. 6 users = tx.xquery('SELECT * FROM users').to_a
  747. 6 ranking = users.map do |user|
  748. 3871 reactions = tx.xquery(<<~SQL, user.fetch(:id), as: :array).first[0]
  749. SELECT COUNT(*) FROM users u
  750. INNER JOIN livestreams l ON l.user_id = u.id
  751. INNER JOIN reactions r ON r.livestream_id = l.id
  752. WHERE u.id = ?
  753. SQL
  754. 3871 tips = tx.xquery(<<~SQL, user.fetch(:id), as: :array).first[0]
  755. SELECT IFNULL(SUM(l2.tip), 0) FROM users u
  756. INNER JOIN livestreams l ON l.user_id = u.id
  757. INNER JOIN livecomments l2 ON l2.livestream_id = l.id
  758. WHERE u.id = ?
  759. SQL
  760. 3867 score = reactions + tips
  761. 3867 UserRankingEntry.new(username: user.fetch(:name), score:)
  762. end
  763. 2002 ranking.sort_by! { |entry| [entry.score, entry.username] }
  764. 754 ridx = ranking.rindex { |entry| entry.username == username }
  765. 2 rank = ranking.size - ridx
  766. # リアクション数
  767. 2 total_reactions = tx.xquery(<<~SQL, username, as: :array).first[0]
  768. SELECT COUNT(*) FROM users u
  769. INNER JOIN livestreams l ON l.user_id = u.id
  770. INNER JOIN reactions r ON r.livestream_id = l.id
  771. WHERE u.name = ?
  772. SQL
  773. # ライブコメント数、チップ合計
  774. 2 total_livecomments = 0
  775. 2 total_tip = 0
  776. 2 livestreams = tx.xquery('SELECT * FROM livestreams WHERE user_id = ?', user.fetch(:id))
  777. 2 livestreams.each do |livestream|
  778. 18 tx.xquery('SELECT * FROM livecomments WHERE livestream_id = ?', livestream.fetch(:id)).each do |livecomment|
  779. 4 total_tip += livecomment.fetch(:tip)
  780. 4 total_livecomments += 1
  781. end
  782. end
  783. # 合計視聴者数
  784. 2 viewers_count = 0
  785. 2 livestreams.each do |livestream|
  786. 18 cnt = tx.xquery('SELECT COUNT(*) FROM livestream_viewers_history WHERE livestream_id = ?', livestream.fetch(:id), as: :array).first[0]
  787. 18 viewers_count += cnt
  788. end
  789. # お気に入り絵文字
  790. 2 favorite_emoji = tx.xquery(<<~SQL, username).first&.fetch(:emoji_name)
  791. SELECT r.emoji_name
  792. FROM users u
  793. INNER JOIN livestreams l ON l.user_id = u.id
  794. INNER JOIN reactions r ON r.livestream_id = l.id
  795. WHERE u.name = ?
  796. GROUP BY emoji_name
  797. ORDER BY COUNT(*) DESC, emoji_name DESC
  798. LIMIT 1
  799. SQL
  800. {
  801. 2 rank:,
  802. viewers_count:,
  803. total_reactions:,
  804. total_livecomments:,
  805. total_tip:,
  806. favorite_emoji:,
  807. }
  808. end
  809. 2 json(stats)
  810. end
  811. LivestreamRankingEntry = Data.define(:livestream_id, :score)
  812. # ライブ配信統計情報
  813. get '/api/livestream/:livestream_id/statistics' do
  814. 2 verify_user_session!
  815. 2 livestream_id = cast_as_integer(params[:livestream_id])
  816. 2 stats = db_transaction do |tx|
  817. 2 unless tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first
  818. raise HttpError.new(400)
  819. end
  820. # ランク算出
  821. 2 ranking = tx.xquery('SELECT * FROM livestreams').map do |livestream|
  822. 14992 reactions = tx.xquery('SELECT COUNT(*) FROM livestreams l INNER JOIN reactions r ON l.id = r.livestream_id WHERE l.id = ?', livestream.fetch(:id), as: :array).first[0]
  823. 14992 total_tips = tx.xquery('SELECT IFNULL(SUM(l2.tip), 0) FROM livestreams l INNER JOIN livecomments l2 ON l.id = l2.livestream_id WHERE l.id = ?', livestream.fetch(:id), as: :array).first[0]
  824. 14992 score = reactions + total_tips
  825. 14992 LivestreamRankingEntry.new(livestream_id: livestream.fetch(:id), score:)
  826. end
  827. 14994 ranking.sort_by! { |entry| [entry.score, entry.livestream_id] }
  828. 944 ridx = ranking.rindex { |entry| entry.livestream_id == livestream_id }
  829. 2 rank = ranking.size - ridx
  830. # 視聴者数算出
  831. 2 viewers_count = tx.xquery('SELECT COUNT(*) FROM livestreams l INNER JOIN livestream_viewers_history h ON h.livestream_id = l.id WHERE l.id = ?', livestream_id, as: :array).first[0]
  832. # 最大チップ額
  833. 2 max_tip = tx.xquery('SELECT IFNULL(MAX(tip), 0) FROM livestreams l INNER JOIN livecomments l2 ON l2.livestream_id = l.id WHERE l.id = ?', livestream_id, as: :array).first[0]
  834. # リアクション数
  835. 2 total_reactions = tx.xquery('SELECT COUNT(*) FROM livestreams l INNER JOIN reactions r ON r.livestream_id = l.id WHERE l.id = ?', livestream_id, as: :array).first[0]
  836. # スパム報告数
  837. 2 total_reports = tx.xquery('SELECT COUNT(*) FROM livestreams l INNER JOIN livecomment_reports r ON r.livestream_id = l.id WHERE l.id = ?', livestream_id, as: :array).first[0]
  838. {
  839. 2 rank:,
  840. viewers_count:,
  841. max_tip:,
  842. total_reactions:,
  843. total_reports:,
  844. }
  845. end
  846. 2 json(stats)
  847. end
  848. get '/api/payment' do
  849. 1 total_tip = db_transaction do |tx|
  850. 1 tx.xquery('SELECT IFNULL(SUM(tip), 0) FROM livecomments', as: :array).first[0]
  851. end
  852. 1 json(total_tip:)
  853. end
  854. end
  855. end