Coverage for src/couchers/servicers/conversations.py: 90%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

240 statements  

1from datetime import timedelta 

2 

3import grpc 

4from google.protobuf import empty_pb2 

5from sqlalchemy.sql import func, or_ 

6 

7from couchers import errors 

8from couchers.constants import DATETIME_INFINITY, DATETIME_MINUS_INFINITY 

9from couchers.db import session_scope 

10from couchers.jobs.enqueue import queue_job 

11from couchers.models import ( 

12 BackgroundJobType, 

13 Conversation, 

14 GroupChat, 

15 GroupChatRole, 

16 GroupChatSubscription, 

17 Message, 

18 MessageType, 

19 User, 

20) 

21from couchers.sql import couchers_select as select 

22from couchers.utils import Timestamp_from_datetime, now 

23from proto import conversations_pb2, conversations_pb2_grpc 

24from proto.internal import jobs_pb2 

25 

26# TODO: Still needs custom pagination: GetUpdates 

27DEFAULT_PAGINATION_LENGTH = 20 

28MAX_PAGE_SIZE = 50 

29 

30 

31def _message_to_pb(message: Message): 

32 """ 

33 Turns the given message to a protocol buffer 

34 """ 

35 if message.is_normal_message: 

36 return conversations_pb2.Message( 

37 message_id=message.id, 

38 author_user_id=message.author_id, 

39 time=Timestamp_from_datetime(message.time), 

40 text=conversations_pb2.MessageContentText(text=message.text), 

41 ) 

42 else: 

43 return conversations_pb2.Message( 

44 message_id=message.id, 

45 author_user_id=message.author_id, 

46 time=Timestamp_from_datetime(message.time), 

47 chat_created=conversations_pb2.MessageContentChatCreated() 

48 if message.message_type == MessageType.chat_created 

49 else None, 

50 chat_edited=conversations_pb2.MessageContentChatEdited() 

51 if message.message_type == MessageType.chat_edited 

52 else None, 

53 user_invited=conversations_pb2.MessageContentUserInvited(target_user_id=message.target_id) 

54 if message.message_type == MessageType.user_invited 

55 else None, 

56 user_left=conversations_pb2.MessageContentUserLeft() 

57 if message.message_type == MessageType.user_left 

58 else None, 

59 user_made_admin=conversations_pb2.MessageContentUserMadeAdmin(target_user_id=message.target_id) 

60 if message.message_type == MessageType.user_made_admin 

61 else None, 

62 user_removed_admin=conversations_pb2.MessageContentUserRemovedAdmin(target_user_id=message.target_id) 

63 if message.message_type == MessageType.user_removed_admin 

64 else None, 

65 group_chat_user_removed=conversations_pb2.MessageContentUserRemoved(target_user_id=message.target_id) 

66 if message.message_type == MessageType.user_removed 

67 else None, 

68 ) 

69 

70 

71def _get_visible_members_for_subscription(subscription): 

72 """ 

73 If a user leaves a group chat, they shouldn't be able to see who's added 

74 after they left 

75 """ 

76 if not subscription.left: 

77 # still in the chat, we see everyone with a current subscription 

78 return [sub.user_id for sub in subscription.group_chat.subscriptions.where(GroupChatSubscription.left == None)] 

79 else: 

80 # not in chat anymore, see everyone who was in chat when we left 

81 return [ 

82 sub.user_id 

83 for sub in subscription.group_chat.subscriptions.where( 

84 GroupChatSubscription.joined <= subscription.left 

85 ).where(or_(GroupChatSubscription.left >= subscription.left, GroupChatSubscription.left == None)) 

86 ] 

87 

88 

89def _get_visible_admins_for_subscription(subscription): 

90 """ 

91 If a user leaves a group chat, they shouldn't be able to see who's added 

92 after they left 

93 """ 

94 if not subscription.left: 

95 # still in the chat, we see everyone with a current subscription 

96 return [ 

97 sub.user_id 

98 for sub in subscription.group_chat.subscriptions.where(GroupChatSubscription.left == None).where( 

99 GroupChatSubscription.role == GroupChatRole.admin 

100 ) 

101 ] 

102 else: 

103 # not in chat anymore, see everyone who was in chat when we left 

104 return [ 

105 sub.user_id 

106 for sub in subscription.group_chat.subscriptions.where(GroupChatSubscription.role == GroupChatRole.admin) 

107 .where(GroupChatSubscription.joined <= subscription.left) 

108 .where(or_(GroupChatSubscription.left >= subscription.left, GroupChatSubscription.left == None)) 

109 ] 

110 

111 

112def _add_message_to_subscription(session, subscription, **kwargs): 

113 """ 

114 Creates a new message for a subscription, from the user whose subscription that is. Updates last seen message id 

115 

116 Specify the keyword args for Message 

117 """ 

118 message = Message(conversation=subscription.group_chat.conversation, author_id=subscription.user_id, **kwargs) 

119 

120 session.add(message) 

121 session.flush() 

122 

123 subscription.last_seen_message_id = message.id 

124 

125 # generate notifications in the background 

126 queue_job( 

127 job_type=BackgroundJobType.generate_message_notifications, 

128 payload=jobs_pb2.GenerateMessageNotificationsPayload( 

129 message_id=message.id, 

130 ), 

131 ) 

132 

133 return message 

134 

135 

136def _unseen_message_count(session, subscription_id): 

137 return session.execute( 

138 select(func.count()) 

139 .select_from(Message) 

140 .join(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id) 

141 .where(GroupChatSubscription.id == subscription_id) 

142 .where(Message.id > GroupChatSubscription.last_seen_message_id) 

143 ).scalar_one() 

144 

145 

146def _mute_info(subscription): 

147 (muted, muted_until) = subscription.muted_display() 

148 return conversations_pb2.MuteInfo( 

149 muted=muted, 

150 muted_until=Timestamp_from_datetime(muted_until) if muted_until else None, 

151 ) 

152 

153 

154class Conversations(conversations_pb2_grpc.ConversationsServicer): 

155 def ListGroupChats(self, request, context): 

156 with session_scope() as session: 

157 page_size = request.number if request.number != 0 else DEFAULT_PAGINATION_LENGTH 

158 page_size = min(page_size, MAX_PAGE_SIZE) 

159 

160 # select group chats where you have a subscription, and for each of 

161 # these, the latest message from them 

162 

163 t = ( 

164 select( 

165 GroupChatSubscription.group_chat_id.label("group_chat_id"), 

166 func.max(GroupChatSubscription.id).label("group_chat_subscriptions_id"), 

167 func.max(Message.id).label("message_id"), 

168 ) 

169 .join(Message, Message.conversation_id == GroupChatSubscription.group_chat_id) 

170 .where(GroupChatSubscription.user_id == context.user_id) 

171 .where(Message.time >= GroupChatSubscription.joined) 

172 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None)) 

173 .group_by(GroupChatSubscription.group_chat_id) 

174 .order_by(func.max(Message.id).desc()) 

175 .subquery() 

176 ) 

177 

178 results = session.execute( 

179 select(t, GroupChat, GroupChatSubscription, Message) 

180 .join(Message, Message.id == t.c.message_id) 

181 .join(GroupChatSubscription, GroupChatSubscription.id == t.c.group_chat_subscriptions_id) 

182 .join(GroupChat, GroupChat.conversation_id == t.c.group_chat_id) 

183 .where(or_(t.c.message_id < request.last_message_id, request.last_message_id == 0)) 

184 .order_by(t.c.message_id.desc()) 

185 .limit(page_size + 1) 

186 ).all() 

187 

188 return conversations_pb2.ListGroupChatsRes( 

189 group_chats=[ 

190 conversations_pb2.GroupChat( 

191 group_chat_id=result.GroupChat.conversation_id, 

192 title=result.GroupChat.title, # TODO: proper title for DMs, etc 

193 member_user_ids=_get_visible_members_for_subscription(result.GroupChatSubscription), 

194 admin_user_ids=_get_visible_admins_for_subscription(result.GroupChatSubscription), 

195 only_admins_invite=result.GroupChat.only_admins_invite, 

196 is_dm=result.GroupChat.is_dm, 

197 created=Timestamp_from_datetime(result.GroupChat.conversation.created), 

198 unseen_message_count=_unseen_message_count(session, result.GroupChatSubscription.id), 

199 last_seen_message_id=result.GroupChatSubscription.last_seen_message_id, 

200 latest_message=_message_to_pb(result.Message) if result.Message else None, 

201 mute_info=_mute_info(result.GroupChatSubscription), 

202 ) 

203 for result in results[:page_size] 

204 ], 

205 last_message_id=min(map(lambda g: g.Message.id if g.Message else 1, results[:page_size])) 

206 if len(results) > 0 

207 else 0, # TODO 

208 no_more=len(results) <= page_size, 

209 ) 

210 

211 def GetGroupChat(self, request, context): 

212 with session_scope() as session: 

213 result = session.execute( 

214 select(GroupChat, GroupChatSubscription, Message) 

215 .join(Message, Message.conversation_id == GroupChatSubscription.group_chat_id) 

216 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id) 

217 .where(GroupChatSubscription.user_id == context.user_id) 

218 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

219 .where(Message.time >= GroupChatSubscription.joined) 

220 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None)) 

221 .order_by(Message.id.desc()) 

222 ).first() 

223 

224 if not result: 

225 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

226 

227 return conversations_pb2.GroupChat( 

228 group_chat_id=result.GroupChat.conversation_id, 

229 title=result.GroupChat.title, 

230 member_user_ids=_get_visible_members_for_subscription(result.GroupChatSubscription), 

231 admin_user_ids=_get_visible_admins_for_subscription(result.GroupChatSubscription), 

232 only_admins_invite=result.GroupChat.only_admins_invite, 

233 is_dm=result.GroupChat.is_dm, 

234 created=Timestamp_from_datetime(result.GroupChat.conversation.created), 

235 unseen_message_count=_unseen_message_count(session, result.GroupChatSubscription.id), 

236 last_seen_message_id=result.GroupChatSubscription.last_seen_message_id, 

237 latest_message=_message_to_pb(result.Message) if result.Message else None, 

238 mute_info=_mute_info(result.GroupChatSubscription), 

239 ) 

240 

241 def GetDirectMessage(self, request, context): 

242 with session_scope() as session: 

243 count = func.count(GroupChatSubscription.id).label("count") 

244 subquery = ( 

245 select(GroupChatSubscription.group_chat_id) 

246 .where( 

247 or_( 

248 GroupChatSubscription.user_id == context.user_id, 

249 GroupChatSubscription.user_id == request.user_id, 

250 ) 

251 ) 

252 .where(GroupChatSubscription.left == None) 

253 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id) 

254 .where(GroupChat.is_dm == True) 

255 .group_by(GroupChatSubscription.group_chat_id) 

256 .having(count == 2) 

257 .subquery() 

258 ) 

259 

260 result = session.execute( 

261 select(subquery, GroupChat, GroupChatSubscription, Message) 

262 .join(subquery, subquery.c.group_chat_id == GroupChat.conversation_id) 

263 .join(Message, Message.conversation_id == GroupChat.conversation_id) 

264 .where(GroupChatSubscription.user_id == context.user_id) 

265 .where(GroupChatSubscription.group_chat_id == GroupChat.conversation_id) 

266 .where(Message.time >= GroupChatSubscription.joined) 

267 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None)) 

268 .order_by(Message.id.desc()) 

269 ).first() 

270 

271 if not result: 

272 context.abort(grpc.StatusCode.NOT_FOUND, "Couldn't find that chat.") 

273 

274 return conversations_pb2.GroupChat( 

275 group_chat_id=result.GroupChat.conversation_id, 

276 title=result.GroupChat.title, 

277 member_user_ids=_get_visible_members_for_subscription(result.GroupChatSubscription), 

278 admin_user_ids=_get_visible_admins_for_subscription(result.GroupChatSubscription), 

279 only_admins_invite=result.GroupChat.only_admins_invite, 

280 is_dm=result.GroupChat.is_dm, 

281 created=Timestamp_from_datetime(result.GroupChat.conversation.created), 

282 unseen_message_count=_unseen_message_count(session, result.GroupChatSubscription.id), 

283 last_seen_message_id=result.GroupChatSubscription.last_seen_message_id, 

284 latest_message=_message_to_pb(result.Message) if result.Message else None, 

285 mute_info=_mute_info(result.GroupChatSubscription), 

286 ) 

287 

288 def GetUpdates(self, request, context): 

289 with session_scope() as session: 

290 results = ( 

291 session.execute( 

292 select(Message) 

293 .join(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id) 

294 .where(GroupChatSubscription.user_id == context.user_id) 

295 .where(Message.time >= GroupChatSubscription.joined) 

296 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None)) 

297 .where(Message.id > request.newest_message_id) 

298 .order_by(Message.id.asc()) 

299 .limit(DEFAULT_PAGINATION_LENGTH + 1) 

300 ) 

301 .scalars() 

302 .all() 

303 ) 

304 

305 return conversations_pb2.GetUpdatesRes( 

306 updates=[ 

307 conversations_pb2.Update( 

308 group_chat_id=message.conversation_id, 

309 message=_message_to_pb(message), 

310 ) 

311 for message in sorted(results, key=lambda message: message.id)[:DEFAULT_PAGINATION_LENGTH] 

312 ], 

313 no_more=len(results) <= DEFAULT_PAGINATION_LENGTH, 

314 ) 

315 

316 def GetGroupChatMessages(self, request, context): 

317 with session_scope() as session: 

318 page_size = request.number if request.number != 0 else DEFAULT_PAGINATION_LENGTH 

319 page_size = min(page_size, MAX_PAGE_SIZE) 

320 

321 results = ( 

322 session.execute( 

323 select(Message) 

324 .join(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id) 

325 .where(GroupChatSubscription.user_id == context.user_id) 

326 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

327 .where(Message.time >= GroupChatSubscription.joined) 

328 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None)) 

329 .where(or_(Message.id < request.last_message_id, request.last_message_id == 0)) 

330 .where(or_(Message.id > GroupChatSubscription.last_seen_message_id, request.only_unseen == 0)) 

331 .order_by(Message.id.desc()) 

332 .limit(page_size + 1) 

333 ) 

334 .scalars() 

335 .all() 

336 ) 

337 

338 return conversations_pb2.GetGroupChatMessagesRes( 

339 messages=[_message_to_pb(message) for message in results[:page_size]], 

340 last_message_id=results[-2].id if len(results) > 1 else 0, # TODO 

341 no_more=len(results) <= page_size, 

342 ) 

343 

344 def MarkLastSeenGroupChat(self, request, context): 

345 with session_scope() as session: 

346 subscription = session.execute( 

347 select(GroupChatSubscription) 

348 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

349 .where(GroupChatSubscription.user_id == context.user_id) 

350 .where(GroupChatSubscription.left == None) 

351 ).scalar_one_or_none() 

352 

353 if not subscription: 

354 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

355 

356 if not subscription.last_seen_message_id <= request.last_seen_message_id: 

357 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_UNSEE_MESSAGES) 

358 

359 subscription.last_seen_message_id = request.last_seen_message_id 

360 

361 # TODO: notify 

362 

363 return empty_pb2.Empty() 

364 

365 def MuteGroupChat(self, request, context): 

366 with session_scope() as session: 

367 subscription = session.execute( 

368 select(GroupChatSubscription) 

369 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

370 .where(GroupChatSubscription.user_id == context.user_id) 

371 .where(GroupChatSubscription.left == None) 

372 ).scalar_one_or_none() 

373 

374 if not subscription: 

375 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

376 

377 if request.unmute: 

378 subscription.muted_until = DATETIME_MINUS_INFINITY 

379 elif request.forever: 

380 subscription.muted_until = DATETIME_INFINITY 

381 elif request.for_duration: 

382 duration = request.for_duration.ToTimedelta() 

383 if duration < timedelta(seconds=0): 

384 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_MUTE_PAST) 

385 subscription.muted_until = now() + duration 

386 

387 return empty_pb2.Empty() 

388 

389 def SearchMessages(self, request, context): 

390 with session_scope() as session: 

391 page_size = request.number if request.number != 0 else DEFAULT_PAGINATION_LENGTH 

392 page_size = min(page_size, MAX_PAGE_SIZE) 

393 

394 results = ( 

395 session.execute( 

396 select(Message) 

397 .join(GroupChatSubscription, GroupChatSubscription.group_chat_id == Message.conversation_id) 

398 .where(GroupChatSubscription.user_id == context.user_id) 

399 .where(Message.time >= GroupChatSubscription.joined) 

400 .where(or_(Message.time <= GroupChatSubscription.left, GroupChatSubscription.left == None)) 

401 .where(or_(Message.id < request.last_message_id, request.last_message_id == 0)) 

402 .where(Message.text.ilike(f"%{request.query}%")) 

403 .order_by(Message.id.desc()) 

404 .limit(page_size + 1) 

405 ) 

406 .scalars() 

407 .all() 

408 ) 

409 

410 return conversations_pb2.SearchMessagesRes( 

411 results=[ 

412 conversations_pb2.MessageSearchResult( 

413 group_chat_id=message.conversation_id, 

414 message=_message_to_pb(message), 

415 ) 

416 for message in results[:page_size] 

417 ], 

418 last_message_id=results[-2].id if len(results) > 1 else 0, 

419 no_more=len(results) <= page_size, 

420 ) 

421 

422 def CreateGroupChat(self, request, context): 

423 with session_scope() as session: 

424 recipient_user_ids = [ 

425 user_id 

426 for user_id in ( 

427 session.execute( 

428 select(User.id).where_users_visible(context).where(User.id.in_(request.recipient_user_ids)) 

429 ) 

430 .scalars() 

431 .all() 

432 ) 

433 ] 

434 

435 # make sure all requested users are visible 

436 if len(recipient_user_ids) != len(request.recipient_user_ids): 

437 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.USER_NOT_FOUND) 

438 

439 if not recipient_user_ids: 

440 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.NO_RECIPIENTS) 

441 

442 if len(recipient_user_ids) != len(set(recipient_user_ids)): 

443 # make sure there's no duplicate users 

444 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_RECIPIENTS) 

445 

446 if context.user_id in recipient_user_ids: 

447 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.CANT_ADD_SELF) 

448 

449 if len(recipient_user_ids) == 1: 

450 # can only have one DM at a time between any two users 

451 other_user_id = recipient_user_ids[0] 

452 

453 # the following sql statement selects subscriptions that are DMs and have the same group_chat_id, and have 

454 # user_id either this user or the recipient user. If you find two subscriptions to the same DM group 

455 # chat, you know they already have a shared group chat 

456 count = func.count(GroupChatSubscription.id).label("count") 

457 if session.execute( 

458 select(count) 

459 .where( 

460 or_( 

461 GroupChatSubscription.user_id == context.user_id, 

462 GroupChatSubscription.user_id == other_user_id, 

463 ) 

464 ) 

465 .where(GroupChatSubscription.left == None) 

466 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id) 

467 .where(GroupChat.is_dm == True) 

468 .group_by(GroupChatSubscription.group_chat_id) 

469 .having(count == 2) 

470 ).scalar_one_or_none(): 

471 context.abort( 

472 grpc.StatusCode.FAILED_PRECONDITION, "You already have a direct message chat with this user." 

473 ) 

474 

475 conversation = Conversation() 

476 session.add(conversation) 

477 

478 group_chat = GroupChat( 

479 conversation=conversation, 

480 title=request.title.value, 

481 creator_id=context.user_id, 

482 is_dm=True if len(recipient_user_ids) == 1 else False, # TODO 

483 ) 

484 session.add(group_chat) 

485 

486 your_subscription = GroupChatSubscription( 

487 user_id=context.user_id, 

488 group_chat=group_chat, 

489 role=GroupChatRole.admin, 

490 ) 

491 session.add(your_subscription) 

492 

493 for recipient_id in request.recipient_user_ids: 

494 subscription = GroupChatSubscription( 

495 user_id=recipient_id, 

496 group_chat=group_chat, 

497 role=GroupChatRole.participant, 

498 ) 

499 session.add(subscription) 

500 

501 _add_message_to_subscription(session, your_subscription, message_type=MessageType.chat_created) 

502 

503 session.flush() 

504 

505 return conversations_pb2.GroupChat( 

506 group_chat_id=group_chat.conversation_id, 

507 title=group_chat.title, 

508 member_user_ids=_get_visible_members_for_subscription(your_subscription), 

509 admin_user_ids=_get_visible_admins_for_subscription(your_subscription), 

510 only_admins_invite=group_chat.only_admins_invite, 

511 is_dm=group_chat.is_dm, 

512 created=Timestamp_from_datetime(group_chat.conversation.created), 

513 mute_info=_mute_info(your_subscription), 

514 ) 

515 

516 def SendMessage(self, request, context): 

517 if request.text == "": 

518 context.abort(grpc.StatusCode.INVALID_ARGUMENT, errors.INVALID_MESSAGE) 

519 

520 with session_scope() as session: 

521 subscription = session.execute( 

522 select(GroupChatSubscription) 

523 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

524 .where(GroupChatSubscription.user_id == context.user_id) 

525 .where(GroupChatSubscription.left == None) 

526 ).scalar_one_or_none() 

527 if not subscription: 

528 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

529 

530 _add_message_to_subscription(session, subscription, message_type=MessageType.text, text=request.text) 

531 

532 return empty_pb2.Empty() 

533 

534 def EditGroupChat(self, request, context): 

535 with session_scope() as session: 

536 subscription = session.execute( 

537 select(GroupChatSubscription) 

538 .where(GroupChatSubscription.user_id == context.user_id) 

539 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

540 .where(GroupChatSubscription.left == None) 

541 ).scalar_one_or_none() 

542 

543 if not subscription: 

544 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

545 

546 if subscription.role != GroupChatRole.admin: 

547 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.ONLY_ADMIN_CAN_EDIT) 

548 

549 if request.HasField("title"): 

550 subscription.group_chat.title = request.title.value 

551 

552 if request.HasField("only_admins_invite"): 

553 subscription.group_chat.only_admins_invite = request.only_admins_invite.value 

554 

555 _add_message_to_subscription(session, subscription, message_type=MessageType.chat_edited) 

556 

557 return empty_pb2.Empty() 

558 

559 def MakeGroupChatAdmin(self, request, context): 

560 with session_scope() as session: 

561 if not session.execute( 

562 select(User).where_users_visible(context).where(User.id == request.user_id) 

563 ).scalar_one_or_none(): 

564 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

565 

566 your_subscription = session.execute( 

567 select(GroupChatSubscription) 

568 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

569 .where(GroupChatSubscription.user_id == context.user_id) 

570 .where(GroupChatSubscription.left == None) 

571 ).scalar_one_or_none() 

572 

573 if not your_subscription: 

574 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

575 

576 if your_subscription.role != GroupChatRole.admin: 

577 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.ONLY_ADMIN_CAN_MAKE_ADMIN) 

578 

579 if request.user_id == context.user_id: 

580 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_MAKE_SELF_ADMIN) 

581 

582 their_subscription = session.execute( 

583 select(GroupChatSubscription) 

584 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

585 .where(GroupChatSubscription.user_id == request.user_id) 

586 .where(GroupChatSubscription.left == None) 

587 ).scalar_one_or_none() 

588 

589 if not their_subscription: 

590 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.USER_NOT_IN_CHAT) 

591 

592 if their_subscription.role != GroupChatRole.participant: 

593 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.ALREADY_ADMIN) 

594 

595 their_subscription.role = GroupChatRole.admin 

596 

597 _add_message_to_subscription( 

598 session, your_subscription, message_type=MessageType.user_made_admin, target_id=request.user_id 

599 ) 

600 

601 return empty_pb2.Empty() 

602 

603 def RemoveGroupChatAdmin(self, request, context): 

604 with session_scope() as session: 

605 if not session.execute( 

606 select(User).where_users_visible(context).where(User.id == request.user_id) 

607 ).scalar_one_or_none(): 

608 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

609 

610 your_subscription = session.execute( 

611 select(GroupChatSubscription) 

612 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

613 .where(GroupChatSubscription.user_id == context.user_id) 

614 .where(GroupChatSubscription.left == None) 

615 ).scalar_one_or_none() 

616 

617 if not your_subscription: 

618 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

619 

620 if request.user_id == context.user_id: 

621 # Race condition! 

622 other_admins_count = session.execute( 

623 select(func.count()) 

624 .select_from(GroupChatSubscription) 

625 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

626 .where(GroupChatSubscription.user_id != context.user_id) 

627 .where(GroupChatSubscription.role == GroupChatRole.admin) 

628 .where(GroupChatSubscription.left == None) 

629 ).scalar_one() 

630 if not other_admins_count > 0: 

631 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_REMOVE_LAST_ADMIN) 

632 

633 if your_subscription.role != GroupChatRole.admin: 

634 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.ONLY_ADMIN_CAN_REMOVE_ADMIN) 

635 

636 their_subscription = session.execute( 

637 select(GroupChatSubscription) 

638 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

639 .where(GroupChatSubscription.user_id == request.user_id) 

640 .where(GroupChatSubscription.left == None) 

641 .where(GroupChatSubscription.role == GroupChatRole.admin) 

642 ).scalar_one_or_none() 

643 

644 if not their_subscription: 

645 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.USER_NOT_ADMIN) 

646 

647 their_subscription.role = GroupChatRole.participant 

648 

649 _add_message_to_subscription( 

650 session, your_subscription, message_type=MessageType.user_removed_admin, target_id=request.user_id 

651 ) 

652 

653 return empty_pb2.Empty() 

654 

655 def InviteToGroupChat(self, request, context): 

656 with session_scope() as session: 

657 if not session.execute( 

658 select(User).where_users_visible(context).where(User.id == request.user_id) 

659 ).scalar_one_or_none(): 

660 context.abort(grpc.StatusCode.NOT_FOUND, errors.USER_NOT_FOUND) 

661 

662 result = session.execute( 

663 select(GroupChatSubscription, GroupChat) 

664 .join(GroupChat, GroupChat.conversation_id == GroupChatSubscription.group_chat_id) 

665 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

666 .where(GroupChatSubscription.user_id == context.user_id) 

667 .where(GroupChatSubscription.left == None) 

668 ).one_or_none() 

669 

670 if not result: 

671 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

672 

673 your_subscription, group_chat = result 

674 

675 if not your_subscription or not group_chat: 

676 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

677 

678 if request.user_id == context.user_id: 

679 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_INVITE_SELF) 

680 

681 if your_subscription.role != GroupChatRole.admin and your_subscription.group_chat.only_admins_invite: 

682 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.INVITE_PERMISSION_DENIED) 

683 

684 if group_chat.is_dm: 

685 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_INVITE_TO_DM) 

686 

687 their_subscription = session.execute( 

688 select(GroupChatSubscription) 

689 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

690 .where(GroupChatSubscription.user_id == request.user_id) 

691 .where(GroupChatSubscription.left == None) 

692 ).scalar_one_or_none() 

693 

694 if their_subscription: 

695 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.ALREADY_IN_CHAT) 

696 

697 # TODO: race condition! 

698 

699 subscription = GroupChatSubscription( 

700 user_id=request.user_id, 

701 group_chat=your_subscription.group_chat, 

702 role=GroupChatRole.participant, 

703 ) 

704 session.add(subscription) 

705 

706 _add_message_to_subscription( 

707 session, your_subscription, message_type=MessageType.user_invited, target_id=request.user_id 

708 ) 

709 

710 return empty_pb2.Empty() 

711 

712 def RemoveGroupChatUser(self, request, context): 

713 """ 

714 1. Get admin info and check it's correct 

715 2. Get user data, check it's correct and remove user 

716 """ 

717 with session_scope() as session: 

718 # Admin info 

719 your_subscription = session.execute( 

720 select(GroupChatSubscription) 

721 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

722 .where(GroupChatSubscription.user_id == context.user_id) 

723 .where(GroupChatSubscription.left == None) 

724 ).scalar_one_or_none() 

725 

726 # if user info is missing 

727 if not your_subscription: 

728 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

729 

730 # if user not admin 

731 if your_subscription.role != GroupChatRole.admin: 

732 context.abort(grpc.StatusCode.PERMISSION_DENIED, errors.ONLY_ADMIN_CAN_REMOVE_USER) 

733 

734 # if user wants to remove themselves 

735 if request.user_id == context.user_id: 

736 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.CANT_REMOVE_SELF) 

737 

738 # get user info 

739 their_subscription = session.execute( 

740 select(GroupChatSubscription) 

741 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

742 .where(GroupChatSubscription.user_id == request.user_id) 

743 .where(GroupChatSubscription.left == None) 

744 ).scalar_one_or_none() 

745 

746 # user not found 

747 if not their_subscription: 

748 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.USER_NOT_IN_CHAT) 

749 

750 _add_message_to_subscription( 

751 session, your_subscription, message_type=MessageType.user_removed, target_id=request.user_id 

752 ) 

753 

754 their_subscription.left = func.now() 

755 

756 return empty_pb2.Empty() 

757 

758 def LeaveGroupChat(self, request, context): 

759 with session_scope() as session: 

760 subscription = session.execute( 

761 select(GroupChatSubscription) 

762 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

763 .where(GroupChatSubscription.user_id == context.user_id) 

764 .where(GroupChatSubscription.left == None) 

765 ).scalar_one_or_none() 

766 

767 if not subscription: 

768 context.abort(grpc.StatusCode.NOT_FOUND, errors.CHAT_NOT_FOUND) 

769 

770 if subscription.role == GroupChatRole.admin: 

771 other_admins_count = session.execute( 

772 select(func.count()) 

773 .select_from(GroupChatSubscription) 

774 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

775 .where(GroupChatSubscription.user_id != context.user_id) 

776 .where(GroupChatSubscription.role == GroupChatRole.admin) 

777 .where(GroupChatSubscription.left == None) 

778 ).scalar_one() 

779 participants_count = session.execute( 

780 select(func.count()) 

781 .select_from(GroupChatSubscription) 

782 .where(GroupChatSubscription.group_chat_id == request.group_chat_id) 

783 .where(GroupChatSubscription.user_id != context.user_id) 

784 .where(GroupChatSubscription.role == GroupChatRole.participant) 

785 .where(GroupChatSubscription.left == None) 

786 ).scalar_one() 

787 if not (other_admins_count > 0 or participants_count == 0): 

788 context.abort(grpc.StatusCode.FAILED_PRECONDITION, errors.LAST_ADMIN_CANT_LEAVE) 

789 

790 _add_message_to_subscription(session, subscription, message_type=MessageType.user_left) 

791 

792 subscription.left = func.now() 

793 

794 return empty_pb2.Empty()