From 2775d5aa0bd2f0a9ca085cee2ba976b51e3084cd Mon Sep 17 00:00:00 2001 From: Rory& Date: Mon, 23 Feb 2026 06:53:08 +0100 Subject: [PATCH] Faster pings --- assets/openapi.json | Bin 932945 -> 947180 bytes assets/schemas.json | Bin 405734 -> 417111 bytes src/api/util/handlers/Message.ts | 8 ++-- src/util/entities/ReadState.ts | 39 +++++++++++++----- .../1771825341528-MoreReadStateFields.ts | 25 +++++++++++ 5 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 src/util/migration/postgres/1771825341528-MoreReadStateFields.ts diff --git a/assets/openapi.json b/assets/openapi.json index ee3b874614f583b6cfcbe72b8ccb2e42cf14b598..b895e6a1784626eccf2869cbdd270e379d216d4c 100644 GIT binary patch delta 4176 zcmcgveMnnZ6z9En&wEMKHhwf8M$I!?t%Z`oduh zoyXUgmXn&-4#{gj&-x`NI`6OVcrmH2OKR>WHTV5e3#un2it4gx!$6qiWB2>gDU2s} zSb8X))k4F{kgt*Y8U=Tz#qak9J3G62gT3utojpg}`n{#QcR!5>t}9Mdf0jzLnV3!c zdTTMpB&iU+i()Z)_R1yb8#m_RM6Kk&=%`?$p$;KROfR?f`qy^D`T?m%n~fb6+kzM^ zmkX1dRa0}fu&~Yg@3+t*^8NJN?=gcMkf(LMHa_fH(dIpryZ;htYaD zd*t!3z(k~vhouMCG_S|{zkNFSU)EuLJ2>&ouVBT=yuIy--VEY$Cs8^EE?f!HrVcfu2dhfn868OnSGE>`mhf zi-T2Q$FGWsOq+*De-UT2{EH-qvd5H(GtgkEXs3Boq%wy$5Ia9DbT;Lhb1Yq=x8})0 z3TH%?kaoHpCl@UIfz}hlJOB!Y9?DjXO$*snT_qmQywuadV*-VK0FdkA>uCavvI=Y# z#?A;bwUxj<;ERX}e&V@ZV4#QH@Wj9wEkPL!?T~;*%Amlw8E9&|NYJ-cv8@$y^&n&@ zoN@J1=DEpAS*QwjazOU9kxY7graQtmarZWu?*@P%=w8iaM zvsH2Ep+we`pF&lJ3hb_A?)L^DvI+cMvqk%Jx&UYAh2!XP2{N{?68lG|f%j1)h^3j0 zCVc)Tn?|F)RJ>x_VyBTF_&tkr;0IMDTbpp1aQRumf^}iRNdI^fI(U~Soo|6AMml!R z2K0^ydvGN~k@3nq05mlMeVfAgqy-Azj(`abZ?P$~A`C7Jr2}B=hq9ekUw}>(z3B>& zU~5MkUJXJE?w^KjnAIe0*J7{l!&sgu=B5qGwJl7&}IawD_xg*t&Rv-;|x`Knd&c`jX(#)N)0ixOKKiCxtQ$ zE)WV$)ElLzE!T* delta 1059 zcmY+CUuYav6vnwT_uMeg-VL)>d1Z(lrEwa1aU*aYmKbyLm!Idk3uya8uT@>%1-B@`jW*#5$W)Zi2nb zvGXZ*9e3wM1{GYglDJOi^#mj!dTo( zA#CiU56;bm;x(r@GcZz zVTAl07Q@N`F0f~gVz@EE1m625)Qa&(%rI)*M%+!=I_bLTRE;CtB418vi*f>$RW^Wf zR_wx|6TC?+<@hceIYt9mVcbD7&n@&AJfU>A_d$KJUv+)OFNggOi^q)}C^qwMx5^2n z6%i}+>O7-2>1m9P8&c)3@To)~=Q^)O=D)>O6M36BrW$n6FMj4yIluFTNDyfLPyT+? z@9NWU4mo3!Q2IuP?Z>@_(LOz|_qwLBo-(@N|MmqFICNCBxuaEa>EbqO_s<^0@H89S z#H}>$(;~=TNg0;vAE25Qm?sa@L!rRD5|4q4(;wgdDf3wxXIPQs1AX!MeJp-i!ZRV9PMyjm9xOMW&BM z?@bmjWErW|H|atU(WjD9XWpR`p*qpVP+0vmOWn=BwOeZ0f!wDQRohB*#NTkgN-Mfn z&!xL|)mElu2iZw3HIm*hjS<`%Z5s(@d3FNDKi}IFaQ(Ps3vzLT@VUSA+KkzNbgL1CGpYTTETP}nU-a1# zWJmaA6j!MeXYS(>b#zcn-wr(Pu~xtK$**l63`)PSR(NR{QL~5Tu}=S6o!rgT_77}xDCpf&D(=H8>Nnx9 zGcmVTcVT2uBhjl72CRn?+{BQ&LD6>26tNl~E{tm{@WT+Fqrw&8k-AAnI{ZLekM2W! z37Wpp7FeKO5w2S)T(kxLIeI+~|DqRM?g-#lZ#e9aw6}M3MY>+-Xy08Q-RrNcs#;xv zE!C_MgWqXh3@+fMB0NxX?DfTB{K=K*JVmuQI=~zl7OVzEeQXmdLZsu!b}AFG#|n{n zmJIoJJJrBLxSZ>dq z08<}L-U)C|qbM?+_Q7qAph);X+HU)=iGC`_{XaDaejn9K(KJRLxPqEPzIlo_=Vkz~ zrH9rfsaaLXl1&*VWRB3HRVQVBagF9U28OwtHPr83{s zndthdF!`jqI31~{Z{PLfmsE3yT4`Sz(d|V9Hm#RP)y2p43?zS|kMi*SaXm*I*pV+o zduV4GDE1_zv7fqm>iOGB^in`sY*V-HLHBx=C1)L^Z_=oOEsQlb;GEXyChZhf>&f08 z`pw7yr6kr9>nI zDI){j5E<9ZpcS*`Zx_I zv1P5ABZ%yWEF0DPG!J4GyjgyBm&(&9y8T(zl(^wPq`5H?P}|(hyUc{^J?6y41)7PO zFIf&ou4p9t$7rw-ML}IjeTaW;y&}Dg$PjB5;l;D0bGiP(&D2}$e>_zUhVt2|N47oV nzUTC^E5dkuNd{v7#~9Sxn2zu@Jr~25l~*_$Hp`PsjRxay&EF#U delta 1143 zcmZuwe@L8V9OrqT@AKZBr@n8xOSk3ioTZ3#(}LQdi7_jK>}KR0GxSat(n@bP%xL7o z{SifLvUu!1HQ3HtgKCk-8az!#9?`Ht*%5>W8)`8tXnD{NLT2^6cY+E1@xu3g-{<)} zpI^`H912wXRUGSF6mT#h{pRwp_Tx@fqNUE$?OXN)SeJa0f|BQ6{didG!F zTL^^fMFrCHViz)XB7oc<%mX`aq>_H#gZL4n0*QA;4fgivp9_b07}=eo2$w&kPC44* z{@C9h_zCG9Mm4g#s1C8G=)vS!E|7}S1IQ*B;n%;|9!#AzOf`Hj3mFJhG80q#D22>1 z)|MO~4`Qc`PC3%pAOp+g(k`};-cP`%JB>SGudoBS)MV5kQ6p+)#`A~@G_$5seA!Rg zmyfnH{TS+KJ1+|%SHinh<7s}b%o!tCnA69nUm_pI1vgKs{W^%LH$0jgOT9}TfuoSbCaF+ayd0olG}2rOSVEPVYv zGgW_}PT;;#`VZX>D>bIMG)Lq!Kt<>0x z@$)*ay+JB>(i50?U)Wf9&!K&l_haO5ZmSzEy6BV9o`}rNc~#psx~n8FS=|z(u}$kT z{v&*b>_?qY? z-N2lem7@7}!;SkYSp*YftR(6~shbrJtupla;+PG6Ij z8gq+lpf^6$+rJ%)GpquAZ!@0*rfc)NE~j&Z(YqfVpR-%@1yJEXX;N$y2nSI8yhQq12E<`Jnc$r=TI*Gr$HfQJ`S|j2jc+wQs>*NP^cF0dXfSuz XZ?MR@;5xh|Q6`&1oyrq2A2t64#(|{5 diff --git a/src/api/util/handlers/Message.ts b/src/api/util/handlers/Message.ts index d19b73ad8..f78827edf 100644 --- a/src/api/util/handlers/Message.ts +++ b/src/api/util/handlers/Message.ts @@ -51,7 +51,7 @@ import { } from "@spacebar/util"; import { HTTPError } from "lambert-server"; import { In, Or, Equal, IsNull } from "typeorm"; -import { ChannelType, Embed, EmbedType, MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageType, Reaction } from "@spacebar/schemas"; +import { ChannelType, Embed, EmbedType, MessageCreateAttachment, MessageCreateCloudAttachment, MessageCreateSchema, MessageType, Reaction, ReadStateType } from "@spacebar/schemas"; const allow_empty = false; // TODO: check webhook, application, system author, stickers // TODO: embed gifs/videos/images @@ -444,7 +444,7 @@ export async function handleMessage(opts: MessageOptions): Promise { await fillInMissingIDs((await Member.find({ where: { guild_id: channel.guild_id } })).map(({ id }) => id)); } const repository = ReadState.getRepository(); - const condition = { channel_id: channel.id }; + const condition = { channel_id: channel.id, type: ReadStateType.CHANNEL }; await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); await repository.increment(condition, "mention_count", 1); } else { @@ -467,11 +467,9 @@ export async function handleMessage(opts: MessageOptions): Promise { } if (users.size) { const repository = ReadState.getRepository(); - const condition = { user_id: Or(...[...users].map((id) => Equal(id))), channel_id: channel.id }; + const condition = { user_id: Or(...[...users].map((id) => Equal(id))), channel_id: channel.id, read_state_type: ReadStateType.CHANNEL }; await fillInMissingIDs([...users]); - - await repository.update({ ...condition, mention_count: IsNull() }, { mention_count: 0 }); await repository.increment(condition, "mention_count", 1); } } diff --git a/src/util/entities/ReadState.ts b/src/util/entities/ReadState.ts index e334cd73c..b4638fdf5 100644 --- a/src/util/entities/ReadState.ts +++ b/src/util/entities/ReadState.ts @@ -20,6 +20,7 @@ import { Column, Entity, Index, JoinColumn, ManyToOne, RelationId } from "typeor import { BaseClass } from "./BaseClass"; import { Channel } from "./Channel"; import { User } from "./User"; +import { ReadStateFlags, ReadStateType } from "@spacebar/schemas"; // for read receipts // notification cursor and public read receipt need to be forwards-only (the former to prevent re-pinging when marked as unread, and the latter to be acceptable as a legal acknowledgement in criminal proceedings), and private read marker needs to be advance-rewind capable @@ -50,25 +51,43 @@ export class ReadState extends BaseClass { }) user: User; - // fully read marker @Column({ nullable: true }) - last_message_id: string; + last_message_id?: string; - // public read receipt @Column({ nullable: true }) - public_ack: string; + last_acked_id?: string; - // notification cursor / private read receipt @Column({ nullable: true }) notifications_cursor: string; + @Column({ default: 0 }) + mention_count: number; + + @Column({ default: 0 }) + badge_count: number; + @Column({ nullable: true }) last_pin_timestamp?: Date; - @Column({ nullable: true }) - mention_count: number; + @Column({ default: ReadStateType.CHANNEL }) + read_state_type: ReadStateType; - // @Column({ nullable: true }) - // TODO: derive this from (last_message_id=notifications_cursor=public_ack)=true - manual: boolean; + @Column({ default: 0 }) + flags: ReadStateFlags; + + toJSON() { + const res = { ...this } as Partial; + if (this.read_state_type === ReadStateType.CHANNEL) { + delete res.badge_count; + delete res.last_acked_id; + } else { + delete res.mention_count; // mutually exclusive with badge_count + delete res.last_message_id; // mutually exclusive with last_acked_id + // these only apply to channels: + delete res.last_pin_timestamp; + delete res.flags; + // delete res.last_viewed; // TODO + } + return res; + } } diff --git a/src/util/migration/postgres/1771825341528-MoreReadStateFields.ts b/src/util/migration/postgres/1771825341528-MoreReadStateFields.ts new file mode 100644 index 000000000..3b0d05af1 --- /dev/null +++ b/src/util/migration/postgres/1771825341528-MoreReadStateFields.ts @@ -0,0 +1,25 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class MoreReadStateFields1771825341528 implements MigrationInterface { + name = "MoreReadStateFields1771825341528"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "public_ack"`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "last_acked_id" character varying`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "badge_count" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "read_state_type" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "flags" integer NOT NULL DEFAULT '0'`); + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" SET DEFAULT '0'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" DROP DEFAULT`); + await queryRunner.query(`ALTER TABLE "read_states" ALTER COLUMN "mention_count" DROP NOT NULL`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "flags"`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "read_state_type"`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "badge_count"`); + await queryRunner.query(`ALTER TABLE "read_states" DROP COLUMN "last_acked_id"`); + await queryRunner.query(`ALTER TABLE "read_states" ADD "public_ack" character varying`); + } +}