this repo has no description
at main 28 kB view raw
1const std = @import("std"); 2const uuid = @import("uuid"); 3const xev = @import("xev"); 4const zeit = @import("zeit"); 5 6const db = @import("db.zig"); 7const http = @import("http.zig"); 8const log = @import("log.zig"); 9const sanitize = @import("sanitize.zig"); 10 11const Allocator = std.mem.Allocator; 12const Connection = Server.Connection; 13const HeapArena = @import("HeapArena.zig"); 14const Queue = @import("queue.zig").Queue; 15const Server = @import("Server.zig"); 16const Http = @import("http.zig"); 17 18const assert = std.debug.assert; 19 20// Global user modes 21const UserMode = packed struct { 22 operator: bool = false, // +o, global mod 23 24 const none: UserMode = .{}; 25}; 26 27// Channel modes. We can add "private" channels for example 28const ChannelMode = packed struct {}; 29 30pub const ChannelPrivileges = packed struct { 31 operator: bool = false, // +o, channel mod 32 33 const none: ChannelPrivileges = .{}; 34}; 35 36pub const SaslMechanism = enum { 37 plain, 38}; 39 40const Sasl = union(SaslMechanism) { 41 plain: struct { 42 username: []const u8, 43 password: []const u8, 44 }, 45}; 46 47pub const Capability = enum { 48 @"away-notify", 49 batch, 50 @"draft/chathistory", 51 @"draft/no-implicit-names", 52 @"draft/read-marker", 53 @"echo-message", 54 @"message-tags", 55 sasl, 56 @"server-time", 57}; 58 59pub const ChatHistory = struct { 60 pub const TargetsRequest = struct { 61 from: Timestamp, 62 to: Timestamp, 63 limit: u16, 64 }; 65 66 pub const AfterRequest = struct { 67 target: []const u8, 68 after_ms: Timestamp, 69 limit: u16, 70 }; 71 72 pub const BeforeRequest = struct { 73 conn: *Connection, 74 target: []const u8, 75 before_ms: Timestamp, 76 limit: u16, 77 }; 78 79 pub const LatestRequest = struct { 80 conn: *Connection, 81 target: []const u8, 82 limit: u16, 83 }; 84 85 pub const HistoryMessage = struct { 86 uuid: []const u8, 87 timestamp: Timestamp, 88 sender: []const u8, 89 message: []const u8, 90 91 pub fn lessThan(_: void, lhs: HistoryMessage, rhs: HistoryMessage) bool { 92 return lhs.timestamp.milliseconds < rhs.timestamp.milliseconds; 93 } 94 }; 95 96 pub const HistoryBatch = struct { 97 arena: HeapArena, 98 fd: xev.TCP, 99 items: []HistoryMessage, 100 target: []const u8, 101 }; 102 103 pub const Target = struct { 104 nick_or_channel: []const u8, 105 latest_timestamp: Timestamp, 106 }; 107 108 pub const TargetBatch = struct { 109 arena: HeapArena, 110 fd: xev.TCP, 111 items: []Target, 112 }; 113}; 114 115pub const Message = struct { 116 bytes: []const u8, 117 timestamp: Timestamp, 118 uuid: uuid.Uuid, 119 120 pub fn init(bytes: []const u8) Message { 121 return .{ 122 .bytes = bytes, 123 .timestamp = .init(), 124 .uuid = uuid.v4.new(), 125 }; 126 } 127 128 pub fn copy(self: Message, alloc: Allocator) Allocator.Error!Message { 129 return .{ 130 .bytes = try alloc.dupe(u8, self.bytes), 131 .timestamp = self.timestamp, 132 .uuid = self.uuid, 133 }; 134 } 135 136 pub const ParamIterator = struct { 137 params: ?[]const u8, 138 index: usize = 0, 139 140 pub fn next(self: *ParamIterator) ?[]const u8 { 141 const params = self.params orelse return null; 142 if (self.index >= params.len) return null; 143 144 // consume leading whitespace 145 while (self.index < params.len) { 146 if (params[self.index] != ' ') break; 147 self.index += 1; 148 } 149 150 const start = self.index; 151 if (start >= params.len) return null; 152 153 // If our first byte is a ':', we return the rest of the string as a 154 // single param (or the empty string) 155 if (params[start] == ':') { 156 self.index = params.len; 157 if (start == params.len - 1) { 158 return ""; 159 } 160 return params[start + 1 ..]; 161 } 162 163 // Find the first index of space. If we don't have any, the reset of 164 // the line is the last param 165 self.index = std.mem.indexOfScalarPos(u8, params, self.index, ' ') orelse { 166 defer self.index = params.len; 167 return params[start..]; 168 }; 169 170 return params[start..self.index]; 171 } 172 }; 173 174 pub const Tag = struct { 175 key: []const u8, 176 value: []const u8, 177 }; 178 179 pub const TagIterator = struct { 180 tags: []const u8, 181 index: usize = 0, 182 183 // tags are a list of key=value pairs delimited by semicolons. 184 // key[=value] [; key[=value]] 185 pub fn next(self: *TagIterator) ?Tag { 186 if (self.index >= self.tags.len) return null; 187 188 // find next delimiter 189 const end = std.mem.indexOfScalarPos(u8, self.tags, self.index, ';') orelse self.tags.len; 190 var kv_delim = std.mem.indexOfScalarPos(u8, self.tags, self.index, '=') orelse end; 191 // it's possible to have tags like this: 192 // @bot;account=botaccount;+typing=active 193 // where the first tag doesn't have a value. Guard against the 194 // kv_delim being past the end position 195 if (kv_delim > end) kv_delim = end; 196 197 defer self.index = end + 1; 198 199 return .{ 200 .key = self.tags[self.index..kv_delim], 201 .value = if (end == kv_delim) "" else self.tags[kv_delim + 1 .. end], 202 }; 203 } 204 }; 205 206 pub fn tagIterator(msg: Message) TagIterator { 207 const src = msg.bytes; 208 if (src[0] != '@') return .{ .tags = "" }; 209 210 assert(src.len > 1); 211 const n = std.mem.indexOfScalarPos(u8, src, 1, ' ') orelse src.len; 212 return .{ .tags = src[1..n] }; 213 } 214 215 pub fn source(msg: Message) ?[]const u8 { 216 const src = msg.bytes; 217 var i: usize = 0; 218 219 // get past tags 220 if (src[0] == '@') { 221 assert(src.len > 1); 222 i = std.mem.indexOfScalarPos(u8, src, 1, ' ') orelse return null; 223 } 224 225 // consume whitespace 226 while (i < src.len) : (i += 1) { 227 if (src[i] != ' ') break; 228 } 229 230 // Start of source 231 if (src[i] == ':') { 232 assert(src.len > i); 233 i += 1; 234 const end = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse src.len; 235 return src[i..end]; 236 } 237 238 return null; 239 } 240 241 pub fn command(msg: Message) []const u8 { 242 if (msg.bytes.len == 0) return ""; 243 const src = msg.bytes; 244 var i: usize = 0; 245 246 // get past tags 247 if (src[0] == '@') { 248 assert(src.len > 1); 249 i = std.mem.indexOfScalarPos(u8, src, 1, ' ') orelse return ""; 250 } 251 // consume whitespace 252 while (i < src.len) : (i += 1) { 253 if (src[i] != ' ') break; 254 } 255 256 // get past source 257 if (src[i] == ':') { 258 assert(src.len > i); 259 i += 1; 260 i = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse return ""; 261 } 262 // consume whitespace 263 while (i < src.len) : (i += 1) { 264 if (src[i] != ' ') break; 265 } 266 267 assert(src.len > i); 268 // Find next space 269 const end = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse src.len; 270 271 return src[i..end]; 272 } 273 274 pub fn paramIterator(msg: Message) ParamIterator { 275 return .{ .params = msg.rawParameters() }; 276 } 277 278 pub fn rawParameters(msg: Message) []const u8 { 279 const src = msg.bytes; 280 var i: usize = 0; 281 282 // get past tags 283 if (src[0] == '@') { 284 i = std.mem.indexOfScalarPos(u8, src, 0, ' ') orelse return ""; 285 } 286 // consume whitespace 287 while (i < src.len) : (i += 1) { 288 if (src[i] != ' ') break; 289 } 290 291 // get past source 292 if (src[i] == ':') { 293 assert(src.len > i); 294 i += 1; 295 i = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse return ""; 296 } 297 // consume whitespace 298 while (i < src.len) : (i += 1) { 299 if (src[i] != ' ') break; 300 } 301 302 // get past command 303 i = std.mem.indexOfScalarPos(u8, src, i, ' ') orelse return ""; 304 305 assert(src.len > i); 306 return src[i + 1 ..]; 307 } 308 309 /// Returns the value of the tag 'key', if present 310 pub fn getTag(self: Message, key: []const u8) ?[]const u8 { 311 var tag_iter = self.tagIterator(); 312 while (tag_iter.next()) |tag| { 313 if (!std.mem.eql(u8, tag.key, key)) continue; 314 return tag.value; 315 } 316 return null; 317 } 318 319 pub fn compareTime(_: void, lhs: Message, rhs: Message) bool { 320 return lhs.timestamp_ms < rhs.timestamp_ms; 321 } 322}; 323 324pub const MessageIterator = struct { 325 bytes: []const u8, 326 index: usize = 0, 327 328 /// Returns the next message. Trailing \r\n is is removed 329 pub fn next(self: *MessageIterator) ?[]const u8 { 330 if (self.index >= self.bytes.len) return null; 331 const n = std.mem.indexOfScalarPos(u8, self.bytes, self.index, '\n') orelse return null; 332 defer self.index = n + 1; 333 return std.mem.trimRight(u8, self.bytes[self.index..n], "\r\n"); 334 } 335 336 pub fn bytesRead(self: MessageIterator) usize { 337 return self.index; 338 } 339}; 340 341pub const ClientMessage = enum { 342 // Connection Messages 343 CAP, 344 AUTHENTICATE, 345 PASS, 346 NICK, 347 USER, 348 PING, 349 PONG, 350 OPER, 351 QUIT, 352 ERROR, 353 354 // Channel Ops 355 JOIN, 356 PART, 357 TOPIC, 358 NAMES, 359 LIST, 360 INVITE, 361 KICK, 362 363 // Server queries and commands 364 MOTD, 365 VERSION, 366 ADMIN, 367 CONNECT, 368 LUSERS, 369 TIME, 370 STATS, 371 HELP, 372 INFO, 373 MODE, 374 375 // Sending messages 376 PRIVMSG, 377 NOTICE, 378 TAGMSG, 379 380 // User-based queries 381 WHO, 382 WHOIS, 383 WHOWAS, 384 385 // Operator messages 386 KILL, 387 REHASH, 388 RESTART, 389 SQUIT, 390 391 // Optional messages 392 AWAY, 393 LINKS, 394 USERHOST, 395 WALLOPS, 396 397 // Extensions 398 CHATHISTORY, 399 MARKREAD, 400 401 pub fn fromString(str: []const u8) ?ClientMessage { 402 inline for (@typeInfo(ClientMessage).@"enum".fields) |enumField| { 403 if (std.ascii.eqlIgnoreCase(str, enumField.name)) { 404 return @field(ClientMessage, enumField.name); 405 } 406 } 407 return null; 408 } 409}; 410 411pub const User = struct { 412 nick: []const u8, 413 username: []const u8, 414 real: []const u8, 415 avatar_url: []const u8, 416 modes: UserMode, 417 418 away: bool, 419 420 connections: std.ArrayListUnmanaged(*Connection), 421 channels: std.ArrayListUnmanaged(*Channel), 422 423 pub fn init() User { 424 return .{ 425 .nick = "", 426 .username = "", 427 .real = "", 428 .avatar_url = "", 429 .connections = .empty, 430 .channels = .empty, 431 .away = false, 432 .modes = .none, 433 }; 434 } 435 436 pub fn deinit(self: *User, gpa: Allocator) void { 437 gpa.free(self.nick); 438 gpa.free(self.username); 439 gpa.free(self.real); 440 gpa.free(self.avatar_url); 441 self.connections.deinit(gpa); 442 self.channels.deinit(gpa); 443 } 444 445 pub fn isAway(self: *User) bool { 446 return self.away or self.connections.items.len == 0; 447 } 448}; 449 450pub const Channel = struct { 451 name: []const u8, 452 topic: []const u8, 453 members: std.ArrayListUnmanaged(Member), 454 streams: std.ArrayListUnmanaged(*http.EventStream), 455 456 state: State = .{}, 457 458 // We store some state so we can format messages to event streams better. The event stream 459 // clients are stateless, we just send them messages and they render it 460 const State = struct { 461 last_sender: ?*User = null, 462 last_timestamp: Timestamp = .{ .milliseconds = 0 }, 463 }; 464 465 const Member = struct { 466 user: *User, 467 privileges: ChannelPrivileges, 468 }; 469 470 pub fn init(name: []const u8, topic: []const u8) Channel { 471 return .{ 472 .name = name, 473 .topic = topic, 474 .members = .empty, 475 .streams = .empty, 476 }; 477 } 478 479 pub fn deinit(self: *Channel, gpa: Allocator) void { 480 gpa.free(self.name); 481 gpa.free(self.topic); 482 self.members.deinit(gpa); 483 self.web_event_queues.deinit(gpa); 484 self.streams.deinit(gpa); 485 } 486 487 pub fn addUser(self: *Channel, server: *Server, user: *User, new_conn: *Connection) Allocator.Error!void { 488 log.debug("user={s} joining {s}", .{ user.nick, self.name }); 489 // First, we see if the User is already in this channel 490 for (self.members.items) |u| { 491 if (u.user == user) { 492 // The user is already here. We just need to send the new connection a JOIN and NAMES 493 try new_conn.print(server.gpa, ":{s} JOIN {s}\r\n", .{ user.nick, self.name }); 494 495 if (self.topic.len > 0) { 496 // Send the topic 497 try new_conn.print( 498 server.gpa, 499 ":{s} 332 {s} {s} :{s}\r\n", 500 .{ server.hostname, user.nick, self.name, self.topic }, 501 ); 502 } 503 504 // Next we see if this user needs to have an implicit names sent 505 if (new_conn.caps.@"draft/no-implicit-names") return; 506 507 // Send implicit NAMES 508 return self.names(server, new_conn); 509 } 510 } 511 512 // Next we add them 513 try self.members.append(server.gpa, .{ .user = user, .privileges = .none }); 514 // Add the channel to the users list of channels 515 try user.channels.append(server.gpa, self); 516 517 // Next we tell everyone about this user joining 518 for (self.members.items) |u| { 519 for (u.user.connections.items) |conn| { 520 try conn.print(server.gpa, ":{s} JOIN {s}\r\n", .{ user.nick, self.name }); 521 try server.queueWrite(conn.client, conn); 522 } 523 } 524 525 // This user just joined the channel, so we need to handle implicit names for each 526 // connection so all of the users connections receive the same information 527 for (user.connections.items) |conn| { 528 // Send the topic 529 if (self.topic.len > 0) { 530 // Send the topic 531 try new_conn.print( 532 server.gpa, 533 ":{s} 332 {s} {s} :{s}\r\n", 534 .{ server.hostname, user.nick, self.name, self.topic }, 535 ); 536 } 537 // See if this connection needs to have an implicit names sent 538 if (conn.caps.@"draft/no-implicit-names") continue; 539 540 // Send implicit NAMES 541 try self.names(server, conn); 542 } 543 544 try server.thread_pool.spawn(db.createChannelMembership, .{ server.db_pool, self.name, user.nick }); 545 } 546 547 /// Notifies anyone in the channel with away-notify that the user is away 548 pub fn notifyAway(self: *Channel, server: *Server, user: *User) Allocator.Error!void { 549 for (self.members.items) |u| { 550 for (u.user.connections.items) |c| { 551 if (!c.caps.@"away-notify") continue; 552 try c.print( 553 server.gpa, 554 ":{s} AWAY :{s} is away\r\n", 555 .{ user.nick, user.nick }, 556 ); 557 try server.queueWrite(c.client, c); 558 } 559 } 560 } 561 562 /// Notifies anyone in the channel with away-notify that the user is back 563 pub fn notifyBack(self: *Channel, server: *Server, user: *User) Allocator.Error!void { 564 for (self.members.items) |m| { 565 const u = m.user; 566 for (u.connections.items) |c| { 567 if (!c.caps.@"away-notify") continue; 568 try c.print( 569 server.gpa, 570 ":{s} AWAY\r\n", 571 .{user.nick}, 572 ); 573 try server.queueWrite(c.client, c); 574 } 575 } 576 } 577 578 pub fn setTopic(self: *Channel, server: *Server, conn: *Connection, topic: []const u8) Allocator.Error!void { 579 const user = conn.user orelse return; 580 allowed: { 581 // Network operators are always allowed to change the topic 582 if (user.modes.operator) break :allowed; 583 584 // First check if this user has permissions 585 for (self.members.items) |m| { 586 if (m.user == user and m.privileges.operator) { 587 break :allowed; 588 } 589 } 590 return server.errChanOpPrivsNeeded(conn, self.name); 591 } 592 593 // We have permissions to set the topic 594 const old = self.topic; 595 self.topic = try server.gpa.dupe(u8, topic); 596 server.gpa.free(old); 597 598 try server.thread_pool.spawn(db.updateTopic, .{ server, self.name, topic }); 599 600 // Tell all the users 601 for (self.members.items) |m| { 602 for (m.user.connections.items) |c| { 603 try c.print( 604 server.gpa, 605 ":{s} TOPIC {s} :{s}\r\n", 606 .{ server.hostname, self.name, self.topic }, 607 ); 608 try server.queueWrite(c.client, c); 609 } 610 } 611 } 612 613 // Removes the user from the channel. Sends a PART to all members 614 pub fn removeUser(self: *Channel, server: *Server, user: *User) Allocator.Error!void { 615 for (self.members.items, 0..) |m, i| { 616 const u = m.user; 617 if (u == user) { 618 _ = self.members.swapRemove(i); 619 break; 620 } 621 } else { 622 for (user.connections.items) |conn| { 623 try conn.print( 624 server.gpa, 625 ":{s} 442 {s} {s} :You're not in that channel\r\n", 626 .{ server.hostname, conn.nickname(), self.name }, 627 ); 628 } 629 return; 630 } 631 632 // Spawn a thread to remove the membership from the db 633 try server.thread_pool.spawn(db.removeChannelMembership, .{ server.db_pool, self.name, user.nick }); 634 635 // Remove the channel from the user struct 636 for (user.channels.items, 0..) |uc, i| { 637 if (uc == self) { 638 _ = user.channels.swapRemove(i); 639 } 640 } 641 642 // Send a PART message to all members 643 for (self.members.items) |m| { 644 const u = m.user; 645 for (u.connections.items) |c| { 646 try c.print( 647 server.gpa, 648 ":{s} PART {s} :User left\r\n", 649 .{ user.nick, self.name }, 650 ); 651 try server.queueWrite(c.client, c); 652 } 653 } 654 655 // Send a PART to the user who left too 656 for (user.connections.items) |c| { 657 try c.print( 658 server.gpa, 659 ":{s} PART {s} :User left\r\n", 660 .{ user.nick, self.name }, 661 ); 662 } 663 } 664 665 pub fn names(self: *Channel, server: *Server, conn: *Connection) Allocator.Error!void { 666 for (self.members.items) |us| { 667 try conn.print( 668 server.gpa, 669 ":{s} 353 {s} = {s} :{s}\r\n", 670 .{ server.hostname, conn.nickname(), self.name, us.user.nick }, 671 ); 672 } 673 try conn.print( 674 server.gpa, 675 ":{s} 366 {s} {s} :End of names list\r\n", 676 .{ server.hostname, conn.nickname(), self.name }, 677 ); 678 try server.queueWrite(conn.client, conn); 679 } 680 681 pub fn who(self: *Channel, server: *Server, conn: *Connection, msg: Message) Allocator.Error!void { 682 const client: []const u8 = if (conn.user) |user| user.nick else "*"; 683 var iter = msg.paramIterator(); 684 _ = iter.next(); // We already have the first param (the target) 685 686 // Get the WHOX args, if there aren't any we can use an empty string for the same logic 687 const args = iter.next() orelse ""; 688 const token = iter.next(); 689 690 if (args.len == 0) { 691 for (self.members.items) |member| { 692 const user = member.user; 693 var flag_buf: [3]u8 = undefined; 694 var flag_len: usize = 1; 695 flag_buf[0] = if (user.isAway()) 'G' else 'H'; 696 if (user.modes.operator) { 697 flag_buf[flag_len] = '*'; 698 flag_len += 1; 699 } 700 if (member.privileges.operator) { 701 flag_buf[flag_len] = '@'; 702 flag_len += 1; 703 } 704 705 const flags = flag_buf[0..flag_len]; 706 try conn.print( 707 server.gpa, 708 ":{s} 352 {s} {s} {s} {s} {s} {s} {s} :0 {s}\r\n", 709 .{ 710 server.hostname, 711 client, 712 self.name, 713 user.username, 714 server.hostname, 715 server.hostname, 716 user.nick, 717 flags, 718 user.real, 719 }, 720 ); 721 } 722 } else { 723 for (self.members.items) |member| { 724 const user = member.user; 725 try conn.print( 726 server.gpa, 727 ":{s} 354 {s}", 728 .{ server.hostname, client }, 729 ); 730 731 // Find the index of the standard field indicator 732 const std_idx = std.mem.indexOfScalar(u8, args, '%') orelse args.len; 733 // TODO: any nonstandard fields 734 735 // Handle standard fields, in order. The order is tcuihsnfdlaor 736 if (std.mem.indexOfScalarPos(u8, args, std_idx, 't')) |_| { 737 if (token) |t| try conn.print(server.gpa, " {s}", .{t}); 738 } 739 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'c')) |_| { 740 try conn.print(server.gpa, " {s}", .{self.name}); 741 } 742 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'u')) |_| { 743 try conn.print(server.gpa, " {s}", .{user.username}); 744 } 745 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'i')) |_| { 746 try conn.print(server.gpa, " {s}", .{"127.0.0.1"}); 747 } 748 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'h')) |_| { 749 try conn.print(server.gpa, " {s}", .{server.hostname}); 750 } 751 if (std.mem.indexOfScalarPos(u8, args, std_idx, 's')) |_| { 752 try conn.print(server.gpa, " {s}", .{server.hostname}); 753 } 754 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'n')) |_| { 755 try conn.print(server.gpa, " {s}", .{user.nick}); 756 } 757 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'f')) |_| { 758 const flag = if (user.isAway()) "G" else "H"; 759 try conn.print(server.gpa, " {s}", .{flag}); 760 if (user.modes.operator) { 761 try conn.print(server.gpa, "{s}", .{"*"}); 762 } 763 if (member.privileges.operator) { 764 try conn.print(server.gpa, "{s}", .{"@"}); 765 } 766 } 767 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'd')) |_| { 768 try conn.write(server.gpa, " 0"); 769 } 770 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'l')) |_| { 771 try conn.write(server.gpa, " 0"); 772 } 773 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'a')) |_| { 774 try conn.print(server.gpa, " {s}", .{user.username}); 775 } 776 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'o')) |_| { 777 // TODO: chan op level 778 try conn.print(server.gpa, " {s}", .{user.username}); 779 } 780 if (std.mem.indexOfScalarPos(u8, args, std_idx, 'r')) |_| { 781 try conn.print(server.gpa, " :{s}", .{user.real}); 782 } 783 try conn.write(server.gpa, "\r\n"); 784 } 785 } 786 try conn.print( 787 server.gpa, 788 ":{s} 315 {s} {s} :End of WHO list\r\n", 789 .{ server.hostname, client, self.name }, 790 ); 791 try server.queueWrite(conn.client, conn); 792 } 793 794 pub fn getPrivileges(self: *Channel, user: *User) ChannelPrivileges { 795 for (self.members.items) |m| { 796 if (m.user == user) { 797 return m.privileges; 798 } 799 } 800 return .none; 801 } 802 803 /// Updates the privileges of user. Saves to the db 804 pub fn storePrivileges(self: *Channel, server: *Server, user: *User, privs: ChannelPrivileges) !void { 805 for (self.members.items) |*m| { 806 if (m.user == user) { 807 m.privileges = privs; 808 // Save to the db 809 try server.thread_pool.spawn( 810 db.updatePrivileges, 811 .{ server.db_pool, user, privs, self.name }, 812 ); 813 return; 814 } 815 } 816 } 817 818 pub fn sendPrivMsgToStreams( 819 self: *Channel, 820 server: *Server, 821 sender: *User, 822 msg: Message, 823 ) Allocator.Error!void { 824 // We'll write the format once into buf. Then copy this to each stream for writing to the 825 // stream 826 var buf: std.ArrayListUnmanaged(u8) = .empty; 827 defer buf.deinit(server.gpa); 828 var writer = buf.writer(server.gpa); 829 830 const sender_sanitized: sanitize.Html = .{ .bytes = sender.nick }; 831 832 defer { 833 // save the state 834 self.state.last_sender = sender; 835 self.state.last_timestamp = msg.timestamp; 836 } 837 838 // Parse the message 839 var iter = msg.paramIterator(); 840 _ = iter.next(); // we can ignore the target 841 const content = iter.next() orelse return; 842 const content_sanitized: sanitize.Html = .{ .bytes = content }; 843 844 // We don't reprint the sender if the last message this message are from the same 845 // person. Unless enough time has elapsed (5 minutes) 846 if (self.state.last_sender == sender and 847 (self.state.last_timestamp.milliseconds + 5 * std.time.ms_per_min) >= msg.timestamp.milliseconds) 848 { 849 const fmt = 850 \\event: message 851 \\data: <div class="message"><p class="body">{s}</p></div> 852 \\ 853 \\ 854 ; 855 try writer.print(fmt, .{content_sanitized}); 856 } else { 857 const fmt = 858 \\event: message 859 \\data: <div class="message"><p class="nick"><b>{s}</b></p><p class="body">{s}</p></div> 860 \\ 861 \\ 862 ; 863 try writer.print(fmt, .{ sender_sanitized, content_sanitized }); 864 } 865 866 // Now the buf has the text we want to send to each stream. 867 for (self.streams.items) |stream| { 868 try stream.writeAll(server.gpa, buf.items); 869 try server.queueWriteEventStream(stream); 870 } 871 } 872}; 873 874pub const Timestamp = struct { 875 milliseconds: i64, 876 877 pub fn init() Timestamp { 878 return .{ .milliseconds = std.time.milliTimestamp() }; 879 } 880 881 pub fn format( 882 self: Timestamp, 883 comptime fmt: []const u8, 884 options: std.fmt.FormatOptions, 885 writer: anytype, 886 ) !void { 887 _ = options; 888 _ = fmt; 889 const instant = zeit.instant( 890 .{ .source = .{ .unix_nano = self.milliseconds * std.time.ns_per_ms } }, 891 ) catch unreachable; 892 const time = instant.time(); 893 try writer.print( 894 "{d}-{d:0>2}-{d:0>2}T{d:0>2}:{d:0>2}:{d:0>2}.{d:0>3}Z", 895 .{ 896 time.year, 897 @intFromEnum(time.month), 898 time.day, 899 time.hour, 900 time.minute, 901 time.second, 902 time.millisecond, 903 }, 904 ); 905 } 906};