1:

# frozen_string_literal: true

2:


    
  

0

3:

require 'base64'

0

4:

require 'bcrypt'

0

5:

require 'digest'

0

6:

require 'mysql2'

0

7:

require 'mysql2-cs-bind'

0

8:

require 'open3'

0

9:

require 'securerandom'

0

10:

require 'sinatra/base'

0

11:

require 'sinatra/json'

12:


    
  

0

13:

module Isupipe

0

14:

  class App < Sinatra::Base

0

15:

    enable :logging

0

16:

    set :show_exceptions, :after_handler

0

17:

    set :sessions, domain: 't.isucon.pw', path: '/', expire_after: 1000*60

0

18:

    set :session_secret, ENV.fetch('ISUCON13_SESSION_SECRETKEY', 'isucon13_session_cookiestore_defaultsecret').unpack('H*')[0]

19:


    
  

0

20:

    POWERDNS_SUBDOMAIN_ADDRESS = ENV.fetch('ISUCON13_POWERDNS_SUBDOMAIN_ADDRESS')

21:


    
  

0

22:

    DEFAULT_SESSION_ID_KEY = 'SESSIONID'

0

23:

    DEFAULT_SESSION_EXPIRES_KEY = 'EXPIRES'

0

24:

    DEFAULT_USER_ID_KEY = 'USERID'

0

25:

    DEFAULT_USERNAME_KEY = 'USERNAME'

26:


    
  

0

27:

    class HttpError < StandardError

0

28:

      attr_reader :code

29:


    
  

0

30:

      def initialize(code, message = nil)

13

31:

        super(message || "HTTP error #{code}")

13

32:

        @code = code

33:

      end

34:

    end

35:


    
  

0

36:

    error HttpError do

13

37:

      e = env['sinatra.error']

13

38:

      status e.code

13

39:

      json(error: e.message)

40:

    end

41:


    
  

0

42:

    helpers do

0

43:

      def db_conn

11450

44:

        Thread.current[:db_conn] ||= connect_db

45:

      end

46:


    
  

0

47:

      def connect_db

21

48:

        Mysql2::Client.new(

49:

          host: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_ADDRESS', '127.0.0.1'),

50:

          port: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_PORT', '3306').to_i,

51:

          username: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_USER', 'isucon'),

52:

          password: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_PASSWORD', 'isucon'),

53:

          database: ENV.fetch('ISUCON13_MYSQL_DIALCONFIG_DATABASE', 'isupipe'),

54:

          symbolize_keys: true,

55:

          cast_booleans: true,

56:

          reconnect: true,

57:

        )

58:

      end

59:


    
  

0

60:

      def db_transaction(&block)

3818

61:

        db_conn.query('BEGIN')

3818

62:

        ok = false

63:

        begin

3818

64:

          retval = block.call(db_conn)

3802

65:

          db_conn.query('COMMIT')

3802

66:

          ok = true

3802

67:

          retval

68:

        ensure

3814

69:

          unless ok

12

70:

            db_conn.query('ROLLBACK')

71:

          end

72:

        end

73:

      end

74:


    
  

0

75:

      def decode_request_body(data_class)

1727

76:

        body = JSON.parse(request.body.tap(&:rewind).read, symbolize_names: true)

6308

77:

        data_class.new(**data_class.members.map { |key| [key, body[key]] }.to_h)

78:

      end

79:


    
  

0

80:

      def cast_as_integer(str)

755

81:

        Integer(str, 10)

82:

      rescue

0

83:

        raise HttpError.new(400)

84:

      end

85:


    
  

0

86:

      def verify_user_session!

1410

87:

        sess = session[DEFAULT_SESSION_ID_KEY]

1410

88:

        unless sess

0

89:

          raise HttpError.new(403)

90:

        end

91:


    
  

1410

92:

        session_expires = sess[DEFAULT_SESSION_EXPIRES_KEY]

1410

93:

        unless session_expires

0

94:

          raise HttpError.new(403)

95:

        end

96:


    
  

1410

97:

        now = Time.now

1410

98:

        if now.to_i > session_expires

0

99:

          raise HttpError.new(401, 'session has expired')

100:

        end

101:


    
  

102:

        nil

103:

      end

104:


    
  

0

105:

      def fill_livestream_response(tx, livestream_model)

4714

106:

        owner_model = tx.xquery('SELECT * FROM users WHERE id = ?', livestream_model.fetch(:user_id)).first

4714

107:

        owner = fill_user_response(tx, owner_model)

108:


    
  

4714

109:

        tags = tx.xquery('SELECT * FROM livestream_tags WHERE livestream_id = ?', livestream_model.fetch(:id)).map do |livestream_tag_model|

15757

110:

          tag_model = tx.xquery('SELECT * FROM tags WHERE id = ?', livestream_tag_model.fetch(:tag_id)).first

111:

          {

15757

112:

            id: tag_model.fetch(:id),

113:

            name: tag_model.fetch(:name),

114:

          }

115:

        end

116:


    
  

4714

117:

        livestream_model.slice(:id, :title, :description, :playlist_url, :thumbnail_url, :start_at, :end_at).merge(

118:

          owner:,

119:

          tags:,

120:

        )

121:

      end

122:


    
  

0

123:

      def fill_livecomment_response(tx, livecomment_model)

1096

124:

        comment_owner_model = tx.xquery('SELECT * FROM users WHERE id = ?', livecomment_model.fetch(:user_id)).first

1096

125:

        comment_owner = fill_user_response(tx, comment_owner_model)

126:


    
  

1096

127:

        livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livecomment_model.fetch(:livestream_id)).first

1096

128:

        livestream = fill_livestream_response(tx, livestream_model)

129:


    
  

1096

130:

        livecomment_model.slice(:id, :comment, :tip, :created_at).merge(

131:

          user: comment_owner,

132:

          livestream:,

133:

        )

134:

      end

135:


    
  

0

136:

      def fill_livecomment_report_response(tx, report_model)

62

137:

        reporter_model = tx.xquery('SELECT * FROM users WHERE id = ?', report_model.fetch(:user_id)).first

62

138:

        reporter = fill_user_response(tx, reporter_model)

139:


    
  

62

140:

        livecomment_model = tx.xquery('SELECT * FROM livecomments WHERE id = ?', report_model.fetch(:livecomment_id)).first

62

141:

        livecomment = fill_livecomment_response(tx, livecomment_model)

142:


    
  

62

143:

        report_model.slice(:id, :created_at).merge(

144:

          reporter:,

145:

          livecomment:,

146:

        )

147:

      end

148:


    
  

0

149:

      def fill_reaction_response(tx, reaction_model)

863

150:

        user_model = tx.xquery('SELECT * FROM users WHERE id = ?', reaction_model.fetch(:user_id)).first

863

151:

        user = fill_user_response(tx, user_model)

152:


    
  

863

153:

        livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', reaction_model.fetch(:livestream_id)).first

863

154:

        livestream = fill_livestream_response(tx, livestream_model)

155:


    
  

863

156:

        reaction_model.slice(:id, :emoji_name, :created_at).merge(

157:

          user:,

158:

          livestream:,

159:

        )

160:

      end

161:


    
  

0

162:

      def fill_user_response(tx, user_model)

7172

163:

        theme_model = tx.xquery('SELECT * FROM themes WHERE user_id = ?', user_model.fetch(:id)).first

164:


    
  

7172

165:

        icon_model = tx.xquery('SELECT image FROM icons WHERE user_id = ?', user_model.fetch(:id)).first

166:

        image =

7172

167:

          if icon_model

4712

168:

            icon_model.fetch(:image)

169:

          else

2460

170:

            File.binread(FALLBACK_IMAGE)

171:

          end

7172

172:

        icon_hash = Digest::SHA256.hexdigest(image)

173:


    
  

174:

        {

7172

175:

          id: user_model.fetch(:id),

176:

          name: user_model.fetch(:name),

177:

          display_name: user_model.fetch(:display_name),

178:

          description: user_model.fetch(:description),

179:

          theme: {

180:

            id: theme_model.fetch(:id),

181:

            dark_mode: theme_model.fetch(:dark_mode),

182:

          },

183:

          icon_hash:,

184:

        }

185:

      end

186:

    end

187:


    
  

188:

    # 初期化

0

189:

    post '/api/initialize' do

1

190:

      out, status = Open3.capture2e('../sql/init.sh')

1

191:

      unless status.success?

0

192:

        logger.warn("init.sh failed with out=#{out}")

0

193:

        halt 500

194:

      end

195:


    
  

1

196:

      json(

197:

        language: 'ruby',

198:

      )

199:

    end

200:


    
  

201:

    # top

0

202:

    get '/api/tag' do

62

203:

      tag_models = db_transaction do |tx|

62

204:

        tx.query('SELECT * FROM tags')

205:

      end

206:


    
  

62

207:

      json(

208:

        tags: tag_models.map { |tag_model|

209:

          {

6386

210:

            id: tag_model.fetch(:id),

211:

            name: tag_model.fetch(:name),

212:

          }

213:

        },

214:

      )

215:

    end

216:


    
  

217:

    # 配信者のテーマ取得API

0

218:

    get '/api/user/:username/theme' do

5

219:

      verify_user_session!

220:


    
  

5

221:

      username = params[:username]

222:


    
  

5

223:

      theme_model = db_transaction do |tx|

5

224:

        user_model = tx.xquery('SELECT id FROM users WHERE name = ?', username).first

5

225:

        unless user_model

0

226:

          raise HttpError.new(404)

227:

        end

5

228:

        tx.xquery('SELECT * FROM themes WHERE user_id = ?', user_model.fetch(:id)).first

229:

      end

230:


    
  

5

231:

      json(

232:

        id: theme_model.fetch(:id),

233:

        dark_mode: theme_model.fetch(:dark_mode),

234:

      )

235:

    end

236:


    
  

237:

    # livestream

238:


    
  

0

239:

    ReserveLivestreamRequest = Data.define(

240:

      :tags,

241:

      :title,

242:

      :description,

243:

      :playlist_url,

244:

      :thumbnail_url,

245:

      :start_at,

246:

      :end_at,

247:

    )

248:


    
  

249:

    # reserve livestream

0

250:

    post '/api/livestream/reservation' do

84

251:

      verify_user_session!

84

252:

      sess = session[DEFAULT_SESSION_ID_KEY]

84

253:

      unless sess

0

254:

        raise HttpError.new(401)

255:

      end

84

256:

      user_id = sess[DEFAULT_USER_ID_KEY]

84

257:

      unless sess

0

258:

        raise HttpError.new(401)

259:

      end

260:


    
  

84

261:

      req = decode_request_body(ReserveLivestreamRequest)

262:


    
  

84

263:

      livestream = db_transaction do |tx|

264:

        # 2023/11/25 10:00からの1年間の期間内であるかチェック

84

265:

        term_start_at = Time.utc(2023, 11, 25, 1)

84

266:

        term_end_at = Time.utc(2024, 11, 25, 1)

84

267:

        reserve_start_at = Time.at(req.start_at, in: 'UTC')

84

268:

        reserve_end_at = Time.at(req.end_at, in: 'UTC')

84

269:

        if reserve_start_at >= term_end_at || reserve_end_at <= term_start_at

2

270:

          raise HttpError.new(400, 'bad reservation time range')

271:

        end

272:


    
  

273:

        # 予約枠をみて、予約が可能か調べる

274:

        # NOTE: 並列な予約のoverbooking防止にFOR UPDATEが必要

82

275:

        tx.xquery('SELECT * FROM reservation_slots WHERE start_at >= ? AND end_at <= ? FOR UPDATE', req.start_at, req.end_at).each do |slot|

629

276:

          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)

629

277:

          logger.info("#{slot.fetch(:start_at)} ~ #{slot.fetch(:end_at)}予約枠の残数 = #{slot.fetch(:slot)}")

629

278:

          if count < 1

1

279:

            raise HttpError.new(400, "予約期間 #{term_start_at.to_i} ~ #{term_end_at.to_i}に対して、予約区間 #{req.start_at} ~ #{req.end_at}が予約できません")

280:

          end

281:

        end

282:


    
  

81

283:

        tx.xquery('UPDATE reservation_slots SET slot = slot - 1 WHERE start_at >= ? AND end_at <= ?', req.start_at, req.end_at)

81

284:

        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)

81

285:

        livestream_id = tx.last_id

286:


    
  

287:

	# タグ追加

81

288:

        req.tags.each do |tag_id|

327

289:

          tx.xquery('INSERT INTO livestream_tags (livestream_id, tag_id) VALUES (?, ?)', livestream_id, tag_id)

290:

        end

291:


    
  

81

292:

        fill_livestream_response(tx, {

293:

          id: livestream_id,

294:

          user_id:,

295:

          title: req.title,

296:

          description: req.description,

297:

          playlist_url: req.playlist_url,

298:

          thumbnail_url: req.thumbnail_url,

299:

          start_at: req.start_at,

300:

          end_at: req.end_at,

301:

        })

302:

      end

303:


    
  

81

304:

      status 201

81

305:

      json(livestream)

306:

    end

307:


    
  

308:

    # list livestream

0

309:

    get '/api/livestream/search' do

35

310:

      key_tag_name = params[:tag] || ''

311:


    
  

35

312:

      livestreams = db_transaction do |tx|

313:

        livestream_models =

35

314:

          if key_tag_name != ''

315:

            # タグによる取得

13

316:

            tag_id_list = tx.xquery('SELECT id FROM tags WHERE name = ?', key_tag_name, as: :array).map(&:first)

13

317:

            tx.xquery('SELECT * FROM livestream_tags WHERE tag_id IN (?) ORDER BY livestream_id DESC', tag_id_list).map do |key_tagged_livestream|

1479

318:

              tx.xquery('SELECT * FROM livestreams WHERE id = ?', key_tagged_livestream.fetch(:livestream_id)).first

319:

            end

320:

          else

321:

            # 検索条件なし

22

322:

            query = 'SELECT * FROM livestreams ORDER BY id DESC'

22

323:

            limit_str = params[:limit] || ''

22

324:

            if limit_str != ''

22

325:

              limit = cast_as_integer(limit_str)

22

326:

              query = "#{query} LIMIT #{limit}"

327:

            end

328:


    
  

22

329:

            tx.xquery(query).to_a

330:

          end

331:


    
  

35

332:

        livestream_models.map do |livestream_model|

2579

333:

          fill_livestream_response(tx, livestream_model)

334:

        end

335:

      end

336:


    
  

35

337:

      json(livestreams)

338:

    end

339:


    
  

0

340:

    get '/api/livestream' do

76

341:

      verify_user_session!

76

342:

      sess = session[DEFAULT_SESSION_ID_KEY]

76

343:

      unless sess

0

344:

        raise HttpError.new(401)

345:

      end

76

346:

      user_id = sess[DEFAULT_USER_ID_KEY]

76

347:

      unless sess

0

348:

        raise HttpError.new(401)

349:

      end

350:


    
  

76

351:

      livestreams = db_transaction do |tx|

76

352:

        tx.xquery('SELECT * FROM livestreams WHERE user_id = ?', user_id).map do |livestream_model|

94

353:

          fill_livestream_response(tx, livestream_model)

354:

        end

355:

      end

356:


    
  

76

357:

      json(livestreams)

358:

    end

359:


    
  

0

360:

    get '/api/user/:username/livestream' do

0

361:

      verify_user_session!

0

362:

      username = params[:username]

363:


    
  

0

364:

      livestreams = db_transaction do |tx|

0

365:

        user = tx.xquery('SELECT * FROM users WHERE name = ?', username).first

0

366:

        unless user

0

367:

          raise HttpError.new(404, 'user not found')

368:

        end

369:


    
  

0

370:

        tx.xquery('SELECT * FROM livestreams WHERE user_id = ?', user.fetch(:id)).map do |livestream_model|

0

371:

          fill_livestream_response(tx, livestream_model)

372:

        end

373:

      end

374:


    
  

0

375:

      json(livestreams)

376:

    end

377:


    
  

378:

    # ユーザ視聴開始 (viewer)

0

379:

    post '/api/livestream/:livestream_id/enter' do

19

380:

      verify_user_session!

19

381:

      sess = session[DEFAULT_SESSION_ID_KEY]

19

382:

      unless sess

0

383:

        raise HttpError.new(401)

384:

      end

19

385:

      user_id = sess[DEFAULT_USER_ID_KEY]

19

386:

      unless sess

0

387:

        raise HttpError.new(401)

388:

      end

389:


    
  

19

390:

      livestream_id = cast_as_integer(params[:livestream_id])

391:


    
  

19

392:

      db_transaction do |tx|

19

393:

        created_at = Time.now.to_i

19

394:

        tx.xquery('INSERT INTO livestream_viewers_history (user_id, livestream_id, created_at) VALUES(?, ?, ?)', user_id, livestream_id, created_at)

395:

      end

396:


    
  

19

397:

      ''

398:

    end

399:


    
  

400:

    # ユーザ視聴終了 (viewer)

0

401:

    delete '/api/livestream/:livestream_id/exit' do

9

402:

      verify_user_session!

9

403:

      sess = session[DEFAULT_SESSION_ID_KEY]

9

404:

      unless sess

0

405:

        raise HttpError.new(401)

406:

      end

9

407:

      user_id = sess[DEFAULT_USER_ID_KEY]

9

408:

      unless sess

0

409:

        raise HttpError.new(401)

410:

      end

411:


    
  

9

412:

      livestream_id = cast_as_integer(params[:livestream_id])

413:


    
  

9

414:

      db_transaction do |tx|

9

415:

        tx.xquery('DELETE FROM livestream_viewers_history WHERE user_id = ? AND livestream_id = ?', user_id, livestream_id)

416:

      end

417:


    
  

9

418:

      ''

419:

    end

420:


    
  

421:

    # get livestream

0

422:

    get '/api/livestream/:livestream_id' do

1

423:

      verify_user_session!

424:


    
  

1

425:

      livestream_id = cast_as_integer(params[:livestream_id])

426:


    
  

1

427:

      livestream = db_transaction do |tx|

1

428:

        livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first

1

429:

        unless livestream_model

0

430:

          raise HttpError.new(404)

431:

        end

432:


    
  

1

433:

        fill_livestream_response(tx, livestream_model)

434:

      end

435:


    
  

1

436:

      json(livestream)

437:

    end

438:


    
  

439:

    # (配信者向け)ライブコメントの報告一覧取得API

0

440:

    get '/api/livestream/:livestream_id/report' do

21

441:

      verify_user_session!

442:


    
  

21

443:

      sess = session[DEFAULT_SESSION_ID_KEY]

21

444:

      unless sess

0

445:

        raise HttpError.new(401)

446:

      end

21

447:

      user_id = sess[DEFAULT_USER_ID_KEY]

21

448:

      unless sess

0

449:

        raise HttpError.new(401)

450:

      end

451:


    
  

21

452:

      livestream_id = cast_as_integer(params[:livestream_id])

453:


    
  

21

454:

      reports = db_transaction do |tx|

21

455:

        livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first

21

456:

        if livestream_model.fetch(:user_id) != user_id

0

457:

          raise HttpError.new(403, "can't get other streamer's livecomment reports")

458:

        end

459:


    
  

21

460:

        tx.xquery('SELECT * FROM livecomment_reports WHERE livestream_id = ?', livestream_id).map do |report_model|

20

461:

          fill_livecomment_report_response(tx, report_model)

462:

        end

463:

      end

464:


    
  

21

465:

      json(reports)

466:

    end

467:


    
  

468:

    # get polling livecomment timeline

0

469:

    get '/api/livestream/:livestream_id/livecomment' do

160

470:

      verify_user_session!

160

471:

      livestream_id = cast_as_integer(params[:livestream_id])

472:


    
  

160

473:

      livecomments = db_transaction do |tx|

160

474:

        query = 'SELECT * FROM livecomments WHERE livestream_id = ? ORDER BY created_at DESC'

160

475:

        limit_str = params[:limit] || ''

160

476:

        if limit_str != ''

18

477:

          limit = cast_as_integer(limit_str)

18

478:

          query = "#{query} LIMIT #{limit}"

479:

        end

480:


    
  

160

481:

        tx.xquery(query, livestream_id).map do |livecomment_model|

864

482:

          fill_livecomment_response(tx, livecomment_model)

483:

        end

484:

      end

485:


    
  

160

486:

      json(livecomments)

487:

    end

488:


    
  

0

489:

    get '/api/livestream/:livestream_id/ngwords' do

15

490:

      verify_user_session!

15

491:

      sess = session[DEFAULT_SESSION_ID_KEY]

15

492:

      unless sess

0

493:

        raise HttpError.new(401)

494:

      end

15

495:

      user_id = sess[DEFAULT_USER_ID_KEY]

15

496:

      unless sess

0

497:

        raise HttpError.new(401)

498:

      end

499:


    
  

15

500:

      livestream_id = cast_as_integer(params[:livestream_id])

501:


    
  

15

502:

      ng_words = db_transaction do |tx|

15

503:

        tx.xquery('SELECT * FROM ng_words WHERE user_id = ? AND livestream_id = ? ORDER BY created_at DESC', user_id, livestream_id).to_a

504:

      end

505:


    
  

15

506:

      json(ng_words)

507:

    end

508:


    
  

0

509:

    PostLivecommentRequest = Data.define(

510:

      :comment,

511:

      :tip,

512:

    )

513:


    
  

514:

    # ライブコメント投稿

0

515:

    post '/api/livestream/:livestream_id/livecomment' do

171

516:

      verify_user_session!

171

517:

      sess = session[DEFAULT_SESSION_ID_KEY]

171

518:

      unless sess

0

519:

        raise HttpError.new(401)

520:

      end

171

521:

      user_id = sess[DEFAULT_USER_ID_KEY]

171

522:

      unless sess

0

523:

        raise HttpError.new(401)

524:

      end

525:


    
  

171

526:

      livestream_id = cast_as_integer(params[:livestream_id])

527:


    
  

171

528:

      req = decode_request_body(PostLivecommentRequest)

529:


    
  

171

530:

      livecomment = db_transaction do |tx|

171

531:

        livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first

171

532:

        unless livestream_model

0

533:

          raise HttpError.new(404, 'livestream not found')

534:

        end

535:


    
  

536:

        # スパム判定

171

537:

        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|

33

538:

          query = <<~SQL

539:

            SELECT COUNT(*)

540:

            FROM

541:

            (SELECT ? AS text) AS texts

542:

            INNER JOIN

543:

            (SELECT CONCAT('%', ?, '%')	AS pattern) AS patterns

544:

            ON texts.text LIKE patterns.pattern

545:

          SQL

33

546:

          hit_spam = tx.xquery(query, req.comment, ng_word.fetch(:word), as: :array).first[0]

33

547:

          logger.info("[hit_spam=#{hit_spam}] comment = #{req.comment}")

33

548:

          if hit_spam >= 1

1

549:

            raise HttpError.new(400, 'このコメントがスパム判定されました')

550:

          end

551:

        end

552:


    
  

170

553:

        now = Time.now.to_i

170

554:

        tx.xquery('INSERT INTO livecomments (user_id, livestream_id, comment, tip, created_at) VALUES (?, ?, ?, ?, ?)', user_id, livestream_id, req.comment, req.tip, now)

170

555:

        livecomment_id = tx.last_id

556:


    
  

170

557:

        fill_livecomment_response(tx, {

558:

          id: livecomment_id,

559:

          user_id:,

560:

          livestream_id:,

561:

          comment: req.comment,

562:

          tip: req.tip,

563:

          created_at: now,

564:

        })

565:

      end

566:


    
  

170

567:

      status 201

170

568:

      json(livecomment)

569:

    end

570:


    
  

571:

    # ライブコメント報告

0

572:

    post '/api/livestream/:livestream_id/livecomment/:livecomment_id/report' do

48

573:

      verify_user_session!

48

574:

      sess = session[DEFAULT_SESSION_ID_KEY]

48

575:

      unless sess

0

576:

        raise HttpError.new(401)

577:

      end

48

578:

      user_id = sess[DEFAULT_USER_ID_KEY]

48

579:

      unless user_id

0

580:

        raise HttpError.new(401)

581:

      end

582:


    
  

48

583:

      livestream_id = cast_as_integer(params[:livestream_id])

48

584:

      livecomment_id = cast_as_integer(params[:livecomment_id])

585:


    
  

48

586:

      report = db_transaction do |tx|

48

587:

        livestream_model = tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first

48

588:

        unless livestream_model

0

589:

          raise HttpError.new(404, 'livestream not found')

590:

        end

591:


    
  

48

592:

        livecomment_model = tx.xquery('SELECT * FROM livecomments WHERE id = ?', livecomment_id).first

48

593:

        unless livecomment_model

6

594:

          raise HttpError.new(404, 'livecomment not found')

595:

        end

596:


    
  

42

597:

        now = Time.now.to_i

42

598:

        tx.xquery('INSERT INTO livecomment_reports(user_id, livestream_id, livecomment_id, created_at) VALUES (?, ?, ?, ?)', user_id, livestream_id, livecomment_id, now)

42

599:

        report_id = tx.last_id

600:


    
  

42

601:

        fill_livecomment_report_response(tx, {

602:

          id: report_id,

603:

          user_id:,

604:

          livestream_id:,

605:

          livecomment_id:,

606:

          created_at: now,

607:

        })

608:

      end

609:


    
  

42

610:

      status 201

42

611:

      json(report)

612:

    end

613:


    
  

0

614:

    ModerateRequest = Data.define(:ng_word)

615:


    
  

616:

    # 配信者によるモデレーション (NGワード登録)

0

617:

    post '/api/livestream/:livestream_id/moderate' do

13

618:

      verify_user_session!

13

619:

      sess = session[DEFAULT_SESSION_ID_KEY]

13

620:

      unless sess

0

621:

        raise HttpError.new(401)

622:

      end

13

623:

      user_id = sess[DEFAULT_USER_ID_KEY]

13

624:

      unless user_id

0

625:

        raise HttpError.new(401)

626:

      end

627:


    
  

13

628:

      livestream_id = cast_as_integer(params[:livestream_id])

629:


    
  

13

630:

      req = decode_request_body(ModerateRequest)

631:


    
  

13

632:

      word_id = db_transaction do |tx|

633:

        # 配信者自身の配信に対するmoderateなのかを検証

13

634:

        owned_livestreams = tx.xquery('SELECT * FROM livestreams WHERE id = ? AND user_id = ?', livestream_id, user_id).to_a

13

635:

        if owned_livestreams.empty?

0

636:

          raise HttpError.new(400, "A streamer can't moderate livestreams that other streamers own")

637:

        end

638:


    
  

13

639:

        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)

13

640:

        word_id = tx.last_id

641:


    
  

642:

        # NGワードにヒットする過去の投稿も全削除する

13

643:

        tx.xquery('SELECT * FROM ng_words WHERE livestream_id = ?', livestream_id).each do |ng_word|

644:

          # ライブコメント一覧取得

14

645:

          tx.xquery('SELECT * FROM livecomments').each do |livecomment|

15411

646:

            query = <<~SQL

647:

              DELETE FROM livecomments

648:

              WHERE

649:

              id = ? AND

650:

              livestream_id = ? AND

651:

              (SELECT COUNT(*)

652:

              FROM

653:

              (SELECT ? AS text) AS texts

654:

              INNER JOIN

655:

              (SELECT CONCAT('%', ?, '%')	AS pattern) AS patterns

656:

              ON texts.text LIKE patterns.pattern) >= 1

657:

            SQL

15411

658:

            tx.xquery(query, livecomment.fetch(:id), livestream_id, livecomment.fetch(:comment), ng_word.fetch(:word))

659:

          end

660:

        end

661:


    
  

13

662:

        word_id

663:

      end

664:


    
  

13

665:

      status 201

13

666:

      json(word_id:)

667:

    end

668:


    
  

0

669:

    get '/api/livestream/:livestream_id/reaction' do

191

670:

      verify_user_session!

671:


    
  

191

672:

      livestream_id = cast_as_integer(params[:livestream_id])

673:


    
  

191

674:

      reactions = db_transaction do |tx|

191

675:

        query = 'SELECT * FROM reactions WHERE livestream_id = ? ORDER BY created_at DESC'

191

676:

        limit_str = params[:limit] || ''

191

677:

        if limit_str != ''

17

678:

          limit = cast_as_integer(limit_str)

17

679:

          query = "#{query} LIMIT #{limit}"

680:

        end

681:


    
  

191

682:

        tx.xquery(query, livestream_id).map do |reaction_model|

709

683:

          fill_reaction_response(tx, reaction_model)

684:

        end

685:

      end

686:


    
  

191

687:

      json(reactions)

688:

    end

689:


    
  

0

690:

    PostReactionRequest = Data.define(:emoji_name)

691:


    
  

0

692:

    post '/api/livestream/:livestream_id/reaction' do

154

693:

      verify_user_session!

154

694:

      sess = session[DEFAULT_SESSION_ID_KEY]

154

695:

      unless sess

0

696:

        raise HttpError.new(401)

697:

      end

154

698:

      user_id = sess[DEFAULT_USER_ID_KEY]

154

699:

      unless user_id

0

700:

        raise HttpError.new(401)

701:

      end

702:


    
  

154

703:

      livestream_id = Integer(params[:livestream_id], 10)

704:


    
  

154

705:

      req = decode_request_body(PostReactionRequest)

706:


    
  

154

707:

      reaction = db_transaction do |tx|

154

708:

        created_at = Time.now.to_i

154

709:

        tx.xquery('INSERT INTO reactions (user_id, livestream_id, emoji_name, created_at) VALUES (?, ?, ?, ?)', user_id, livestream_id, req.emoji_name, created_at)

154

710:

        reaction_id = tx.last_id

711:


    
  

154

712:

        fill_reaction_response(tx, {

713:

          id: reaction_id,

714:

          user_id:,

715:

          livestream_id:,

716:

          emoji_name: req.emoji_name,

717:

          created_at:,

718:

        })

719:

      end

720:


    
  

154

721:

      status 201

154

722:

      json(reaction)

723:

    end

724:


    
  

0

725:

    BCRYPT_DEFAULT_COST = 4

0

726:

    FALLBACK_IMAGE = '../img/NoImage.jpg'

727:


    
  

0

728:

    get '/api/user/:username/icon' do

1437

729:

      username = params[:username]

730:


    
  

1437

731:

      image = db_transaction do |tx|

1437

732:

        user = tx.xquery('SELECT * FROM users WHERE name = ?', username).first

1437

733:

        unless user

0

734:

          raise HttpError.new(404, 'not found user that has the given username')

735:

        end

1437

736:

        tx.xquery('SELECT image FROM icons WHERE user_id = ?', user.fetch(:id)).first

737:

      end

738:


    
  

1437

739:

      content_type 'image/jpeg'

1437

740:

      if image

920

741:

        image[:image]

742:

      else

517

743:

        send_file FALLBACK_IMAGE

744:

      end

745:

    end

746:


    
  

0

747:

    PostIconRequest = Data.define(:image)

748:


    
  

0

749:

    post '/api/icon' do

431

750:

      verify_user_session!

751:


    
  

431

752:

      sess = session[DEFAULT_SESSION_ID_KEY]

431

753:

      unless sess

0

754:

        raise HttpError.new(401)

755:

      end

431

756:

      user_id = sess[DEFAULT_USER_ID_KEY]

431

757:

      unless user_id

0

758:

        raise HttpError.new(401)

759:

      end

760:


    
  

431

761:

      req = decode_request_body(PostIconRequest)

431

762:

      image = Base64.decode64(req.image)

763:


    
  

431

764:

      icon_id = db_transaction do |tx|

431

765:

        tx.xquery('DELETE FROM icons WHERE user_id = ?', user_id)

431

766:

        tx.xquery('INSERT INTO icons (user_id, image) VALUES (?, ?)', user_id, image)

431

767:

        tx.last_id

768:

      end

769:


    
  

431

770:

      status 201

431

771:

      json(

772:

        id: icon_id,

773:

      )

774:

    end

775:


    
  

0

776:

    get '/api/user/me' do

3

777:

      verify_user_session!

778:


    
  

3

779:

      sess = session[DEFAULT_SESSION_ID_KEY]

3

780:

      unless sess

0

781:

        raise HttpError.new(401)

782:

      end

3

783:

      user_id = sess[DEFAULT_USER_ID_KEY]

3

784:

      unless user_id

0

785:

        raise HttpError.new(401)

786:

      end

787:


    
  

3

788:

      user = db_transaction do |tx|

3

789:

        user_model = tx.xquery('SELECT * FROM users WHERE id = ?', user_id).first

3

790:

        unless user_model

0

791:

          raise HttpError.new(404)

792:

        end

3

793:

        fill_user_response(tx, user_model)

794:

      end

795:


    
  

3

796:

      json(user)

797:

    end

798:


    
  

0

799:

    PostUserRequest = Data.define(

800:

      :name,

801:

      :display_name,

802:

      :description,

803:

      # password is non-hashed password.

804:

      :password,

805:

      :theme,

806:

    )

807:


    
  

808:

    # ユーザ登録API

0

809:

    post '/api/register' do

435

810:

      req = decode_request_body(PostUserRequest)

435

811:

      if req.name == 'pipe'

1

812:

        raise HttpError.new(400, "the username 'pipe' is reserved")

813:

      end

814:


    
  

434

815:

      hashed_password = BCrypt::Password.create(req.password, cost: BCRYPT_DEFAULT_COST)

816:


    
  

434

817:

      user = db_transaction do |tx|

434

818:

        tx.xquery('INSERT INTO users (name, display_name, description, password) VALUES(?, ?, ?, ?)', req.name, req.display_name, req.description, hashed_password)

433

819:

        user_id = tx.last_id

820:


    
  

433

821:

        tx.xquery('INSERT INTO themes (user_id, dark_mode) VALUES(?, ?)', user_id, req.theme.fetch(:dark_mode))

822:


    
  

433

823:

        out, status = Open3.capture2e('pdnsutil', 'add-record', 't.isucon.pw', req.name, 'A', '0', POWERDNS_SUBDOMAIN_ADDRESS)

433

824:

        unless status.success?

0

825:

          raise HttpError.new(500, "pdnsutil failed with out=#{out}")

826:

        end

827:


    
  

433

828:

        fill_user_response(tx, {

829:

          id: user_id,

830:

          name: req.name,

831:

          display_name: req.display_name,

832:

          description: req.description,

833:

        })

834:

      end

835:


    
  

433

836:

      status 201

433

837:

      json(user)

838:

    end

839:


    
  

0

840:

    LoginRequest = Data.define(

841:

      :username,

842:

      # password is non-hashed password.

843:

      :password,

844:

    )

845:


    
  

846:

    # ユーザログインAPI

0

847:

    post '/api/login' do

439

848:

      req = decode_request_body(LoginRequest)

849:


    
  

439

850:

      user_model = db_transaction do |tx|

851:

        # usernameはUNIQUEなので、whereで一意に特定できる

439

852:

        tx.xquery('SELECT * FROM users WHERE name = ?', req.username).first.tap do |user_model|

439

853:

          unless user_model

1

854:

            raise HttpError.new(401, 'invalid username or password')

855:

          end

856:

        end

857:

      end

858:


    
  

438

859:

      unless BCrypt::Password.new(user_model.fetch(:password)).is_password?(req.password)

1

860:

        raise HttpError.new(401, 'invalid username or password')

861:

      end

862:


    
  

437

863:

      session_end_at = Time.now + 10*60*60

437

864:

      session_id = SecureRandom.uuid

437

865:

      session[DEFAULT_SESSION_ID_KEY] = {

866:

        DEFAULT_SESSION_ID_KEY => session_id,

867:

        DEFAULT_USER_ID_KEY => user_model.fetch(:id),

868:

        DEFAULT_USERNAME_KEY => user_model.fetch(:name),

869:

        DEFAULT_SESSION_EXPIRES_KEY => session_end_at.to_i,

870:

      }

871:


    
  

437

872:

      ''

873:

    end

874:


    
  

875:

    # ユーザ詳細API

0

876:

    get '/api/user/:username' do

1

877:

      verify_user_session!

878:


    
  

1

879:

      username = params[:username]

880:


    
  

1

881:

      user = db_transaction do |tx|

1

882:

        user_model = tx.xquery('SELECT * FROM users WHERE name = ?', username).first

1

883:

        unless user_model

0

884:

          raise HttpError.new(404)

885:

        end

886:


    
  

1

887:

        fill_user_response(tx, user_model)

888:

      end

889:


    
  

1

890:

      json(user)

891:

    end

892:


    
  

0

893:

    UserRankingEntry = Data.define(:username, :score)

894:


    
  

0

895:

    get '/api/user/:username/statistics' do

6

896:

      verify_user_session!

897:


    
  

6

898:

      username = params[:username]

899:


    
  

900:

      # ユーザごとに、紐づく配信について、累計リアクション数、累計ライブコメント数、累計売上金額を算出

901:

      # また、現在の合計視聴者数もだす

902:


    
  

6

903:

      stats = db_transaction do |tx|

6

904:

        user = tx.xquery('SELECT * FROM users WHERE name = ?', username).first

6

905:

        unless user

0

906:

          raise HttpError.new(400)

907:

        end

908:


    
  

909:

        # ランク算出

6

910:

        users = tx.xquery('SELECT * FROM users').to_a

911:


    
  

6

912:

        ranking = users.map do |user|

3871

913:

          reactions = tx.xquery(<<~SQL, user.fetch(:id), as: :array).first[0]

914:

            SELECT COUNT(*) FROM users u

915:

            INNER JOIN livestreams l ON l.user_id = u.id

916:

            INNER JOIN reactions r ON r.livestream_id = l.id

917:

            WHERE u.id = ?

918:

          SQL

919:


    
  

3871

920:

          tips = tx.xquery(<<~SQL, user.fetch(:id), as: :array).first[0]

921:

            SELECT IFNULL(SUM(l2.tip), 0) FROM users u

922:

            INNER JOIN livestreams l ON l.user_id = u.id

923:

            INNER JOIN livecomments l2 ON l2.livestream_id = l.id

924:

            WHERE u.id = ?

925:

          SQL

926:


    
  

3867

927:

          score = reactions + tips

3867

928:

          UserRankingEntry.new(username: user.fetch(:name), score:)

929:

        end

930:


    
  

2002

931:

        ranking.sort_by! { |entry| [entry.score, entry.username] }

754

932:

        ridx = ranking.rindex { |entry| entry.username == username }

2

933:

        rank = ranking.size - ridx

934:


    
  

935:

        # リアクション数

2

936:

        total_reactions = tx.xquery(<<~SQL, username, as: :array).first[0]

937:

          SELECT COUNT(*) FROM users u

938:

          INNER JOIN livestreams l ON l.user_id = u.id

939:

          INNER JOIN reactions r ON r.livestream_id = l.id

940:

          WHERE u.name = ?

941:

        SQL

942:


    
  

943:

        # ライブコメント数、チップ合計

2

944:

        total_livecomments = 0

2

945:

        total_tip = 0

2

946:

        livestreams = tx.xquery('SELECT * FROM livestreams WHERE user_id = ?', user.fetch(:id))

2

947:

        livestreams.each do |livestream|

18

948:

          tx.xquery('SELECT * FROM livecomments WHERE livestream_id = ?', livestream.fetch(:id)).each do |livecomment|

4

949:

            total_tip += livecomment.fetch(:tip)

4

950:

            total_livecomments += 1

951:

          end

952:

        end

953:


    
  

954:

        # 合計視聴者数

2

955:

        viewers_count = 0

2

956:

        livestreams.each do |livestream|

18

957:

          cnt = tx.xquery('SELECT COUNT(*) FROM livestream_viewers_history WHERE livestream_id = ?', livestream.fetch(:id), as: :array).first[0]

18

958:

          viewers_count += cnt

959:

        end

960:


    
  

961:

        # お気に入り絵文字

2

962:

        favorite_emoji = tx.xquery(<<~SQL, username).first&.fetch(:emoji_name)

963:

          SELECT r.emoji_name

964:

          FROM users u

965:

          INNER JOIN livestreams l ON l.user_id = u.id

966:

          INNER JOIN reactions r ON r.livestream_id = l.id

967:

          WHERE u.name = ?

968:

          GROUP BY emoji_name

969:

          ORDER BY COUNT(*) DESC, emoji_name DESC

970:

          LIMIT 1

971:

        SQL

972:


    
  

973:

        {

2

974:

          rank:,

975:

          viewers_count:,

976:

          total_reactions:,

977:

          total_livecomments:,

978:

          total_tip:,

979:

          favorite_emoji:,

980:

        }

981:

      end

982:


    
  

2

983:

      json(stats)

984:

    end

985:


    
  

0

986:

    LivestreamRankingEntry = Data.define(:livestream_id, :score)

987:


    
  

988:

    # ライブ配信統計情報

0

989:

    get '/api/livestream/:livestream_id/statistics' do

2

990:

      verify_user_session!

2

991:

      livestream_id = cast_as_integer(params[:livestream_id])

992:


    
  

2

993:

      stats = db_transaction do |tx|

2

994:

        unless tx.xquery('SELECT * FROM livestreams WHERE id = ?', livestream_id).first

0

995:

          raise HttpError.new(400)

996:

        end

997:


    
  

998:

        # ランク算出

2

999:

        ranking = tx.xquery('SELECT * FROM livestreams').map do |livestream|

14992

1000:

          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]

1001:


    
  

14992

1002:

          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]

1003:


    
  

14992

1004:

          score = reactions + total_tips

14992

1005:

          LivestreamRankingEntry.new(livestream_id: livestream.fetch(:id), score:)

1006:

        end

14994

1007:

        ranking.sort_by! { |entry| [entry.score, entry.livestream_id] }

944

1008:

        ridx = ranking.rindex { |entry| entry.livestream_id == livestream_id }

2

1009:

        rank = ranking.size - ridx

1010:


    
  

1011:

	# 視聴者数算出

2

1012:

        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]

1013:


    
  

1014:

	# 最大チップ額

2

1015:

        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]

1016:


    
  

1017:

	# リアクション数

2

1018:

        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]

1019:


    
  

1020:

	# スパム報告数

2

1021:

        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]

1022:


    
  

1023:

        {

2

1024:

          rank:,

1025:

          viewers_count:,

1026:

          max_tip:,

1027:

          total_reactions:,

1028:

          total_reports:,

1029:

        }

1030:

      end

1031:


    
  

2

1032:

      json(stats)

1033:

    end

1034:


    
  

0

1035:

    get '/api/payment' do

1

1036:

      total_tip = db_transaction do |tx|

1

1037:

        tx.xquery('SELECT IFNULL(SUM(tip), 0) FROM livecomments', as: :array).first[0]

1038:

      end

1039:


    
  

1

1040:

      json(total_tip:)

1041:

    end

1042:

  end

1043:

end