atagen

malice

public
Star0
Watch0
5dc8b4770e1cb5fafe24d0b08076a46bda0d9605
2
ssh://brewery@pls.lobotomise.me:33/atagen/malice.git

Blame: web/src/lib/packs.ts

132eeceeb362atagen2026-10-25 03:34 UTC1
import type { CommandRequest, PackTarget } from './transport';
132eeceeb362atagen2026-10-25 03:34 UTC2
import type { CloneSource } from './pack-clone';
132eeceeb362atagen2026-10-25 03:34 UTC3
import type { ImagePack, ImagePacks, PackImage, PackUsage, RoomSummary } from './types';
d095131eec0eatagen2026-10-24 08:39 UTC4
d095131eec0eatagen2026-10-24 08:39 UTC5
export interface PackChoice {
d095131eec0eatagen2026-10-24 08:39 UTC6
  label: string;
d095131eec0eatagen2026-10-24 08:39 UTC7
  target: PackTarget;
d095131eec0eatagen2026-10-24 08:39 UTC8
  pack: ImagePack | null;
d095131eec0eatagen2026-10-24 08:39 UTC9
}
d095131eec0eatagen2026-10-24 08:39 UTC10
d095131eec0eatagen2026-10-24 08:39 UTC11
export function packTarget(pack: ImagePack): PackTarget {
d095131eec0eatagen2026-10-24 08:39 UTC12
  if (pack.source === 'User') return { kind: 'user' };
d095131eec0eatagen2026-10-24 08:39 UTC13
  return { kind: 'room', room_id: pack.source.Room.room_id, state_key: pack.source.Room.state_key };
d095131eec0eatagen2026-10-24 08:39 UTC14
}
d095131eec0eatagen2026-10-24 08:39 UTC15
d095131eec0eatagen2026-10-24 08:39 UTC16
export function packLabel(pack: ImagePack, roomName: (roomId: string) => string): string {
d095131eec0eatagen2026-10-24 08:39 UTC17
  if (pack.display_name) return pack.display_name;
d095131eec0eatagen2026-10-24 08:39 UTC18
  if (pack.source === 'User') return 'Your pack';
d095131eec0eatagen2026-10-24 08:39 UTC19
  return roomName(pack.source.Room.room_id);
d095131eec0eatagen2026-10-24 08:39 UTC20
}
d095131eec0eatagen2026-10-24 08:39 UTC21
d095131eec0eatagen2026-10-24 08:39 UTC22
export function allPacks(packs: ImagePacks): ImagePack[] {
d095131eec0eatagen2026-10-24 08:39 UTC23
  const seen = new Set<string>();
d095131eec0eatagen2026-10-24 08:39 UTC24
  const out: ImagePack[] = [];
d095131eec0eatagen2026-10-24 08:39 UTC25
  for (const pack of [...packs.global, ...Object.values(packs.rooms).flat()]) {
d095131eec0eatagen2026-10-24 08:39 UTC26
    if (seen.has(pack.id)) continue;
d095131eec0eatagen2026-10-24 08:39 UTC27
    seen.add(pack.id);
d095131eec0eatagen2026-10-24 08:39 UTC28
    out.push(pack);
d095131eec0eatagen2026-10-24 08:39 UTC29
  }
d095131eec0eatagen2026-10-24 08:39 UTC30
  return out;
d095131eec0eatagen2026-10-24 08:39 UTC31
}
d095131eec0eatagen2026-10-24 08:39 UTC32
132eeceeb362atagen2026-10-25 03:34 UTC33
export function packsForRoom(
132eeceeb362atagen2026-10-25 03:34 UTC34
  packs: ImagePacks,
132eeceeb362atagen2026-10-25 03:34 UTC35
  roomId: string | null,
132eeceeb362atagen2026-10-25 03:34 UTC36
  rooms: Pick<RoomSummary, 'room_id' | 'parent_spaces' | 'children' | 'space'>[] = []
132eeceeb362atagen2026-10-25 03:34 UTC37
): ImagePack[] {
132eeceeb362atagen2026-10-25 03:34 UTC38
  const seen = new Set<string>();
132eeceeb362atagen2026-10-25 03:34 UTC39
  const out: ImagePack[] = [];
132eeceeb362atagen2026-10-25 03:34 UTC40
  const add = (pack: ImagePack) => {
132eeceeb362atagen2026-10-25 03:34 UTC41
    if (seen.has(pack.id) || pack.images.length === 0) return;
132eeceeb362atagen2026-10-25 03:34 UTC42
    seen.add(pack.id);
132eeceeb362atagen2026-10-25 03:34 UTC43
    out.push(pack);
132eeceeb362atagen2026-10-25 03:34 UTC44
  };
132eeceeb362atagen2026-10-25 03:34 UTC45
  for (const pack of packs.global) if (pack.source === 'User') add(pack);
132eeceeb362atagen2026-10-25 03:34 UTC46
  if (roomId) {
132eeceeb362atagen2026-10-25 03:34 UTC47
    const visited = new Set<string>();
132eeceeb362atagen2026-10-25 03:34 UTC48
    const queue = [roomId];
132eeceeb362atagen2026-10-25 03:34 UTC49
    for (const id of queue) {
132eeceeb362atagen2026-10-25 03:34 UTC50
      if (visited.has(id)) continue;
132eeceeb362atagen2026-10-25 03:34 UTC51
      visited.add(id);
132eeceeb362atagen2026-10-25 03:34 UTC52
      for (const pack of packs.rooms[id] ?? []) add(pack);
132eeceeb362atagen2026-10-25 03:34 UTC53
      const room = rooms.find((entry) => entry.room_id === id);
132eeceeb362atagen2026-10-25 03:34 UTC54
      queue.push(...(room?.parent_spaces ?? []));
132eeceeb362atagen2026-10-25 03:34 UTC55
      for (const space of rooms) {
132eeceeb362atagen2026-10-25 03:34 UTC56
        if (space.space && space.children.includes(id)) queue.push(space.room_id);
132eeceeb362atagen2026-10-25 03:34 UTC57
      }
132eeceeb362atagen2026-10-25 03:34 UTC58
    }
132eeceeb362atagen2026-10-25 03:34 UTC59
  }
132eeceeb362atagen2026-10-25 03:34 UTC60
  for (const pack of packs.global) add(pack);
132eeceeb362atagen2026-10-25 03:34 UTC61
  return out;
132eeceeb362atagen2026-10-25 03:34 UTC62
}
132eeceeb362atagen2026-10-25 03:34 UTC63
d095131eec0eatagen2026-10-24 08:39 UTC64
/** Packs you can write to, with your own pack first even before it exists. */
d095131eec0eatagen2026-10-24 08:39 UTC65
export function editableChoices(packs: ImagePacks, roomName: (roomId: string) => string): PackChoice[] {
d095131eec0eatagen2026-10-24 08:39 UTC66
  const all = allPacks(packs);
d095131eec0eatagen2026-10-24 08:39 UTC67
  const user = all.find((p) => p.source === 'User') ?? null;
d095131eec0eatagen2026-10-24 08:39 UTC68
  const choices: PackChoice[] = [{ label: user?.display_name ?? 'Your pack', target: { kind: 'user' }, pack: user }];
d095131eec0eatagen2026-10-24 08:39 UTC69
  for (const pack of all) {
d095131eec0eatagen2026-10-24 08:39 UTC70
    if (pack.source === 'User' || !pack.editable) continue;
132eeceeb362atagen2026-10-25 03:34 UTC71
    choices.push({ label: `${packLabel(pack, roomName)} · ${roomName(pack.source.Room.room_id)}`, target: packTarget(pack), pack });
d095131eec0eatagen2026-10-24 08:39 UTC72
  }
d095131eec0eatagen2026-10-24 08:39 UTC73
  return choices;
d095131eec0eatagen2026-10-24 08:39 UTC74
}
d095131eec0eatagen2026-10-24 08:39 UTC75
d095131eec0eatagen2026-10-24 08:39 UTC76
export function hasUsage(image: PackImage, usage: PackUsage): boolean {
d095131eec0eatagen2026-10-24 08:39 UTC77
  return image.usage.length === 0 || image.usage.includes(usage);
d095131eec0eatagen2026-10-24 08:39 UTC78
}
d095131eec0eatagen2026-10-24 08:39 UTC79
d095131eec0eatagen2026-10-24 08:39 UTC80
export function stickersOf(pack: ImagePack): PackImage[] {
d095131eec0eatagen2026-10-24 08:39 UTC81
  return pack.images.filter((image) => hasUsage(image, 'Sticker'));
d095131eec0eatagen2026-10-24 08:39 UTC82
}
d095131eec0eatagen2026-10-24 08:39 UTC83
132eeceeb362atagen2026-10-25 03:34 UTC84
export function reactionShortcode(key: string, shortcode: string | undefined, packs: ImagePack[]): string | undefined {
132eeceeb362atagen2026-10-25 03:34 UTC85
  if (!key.startsWith('mxc://')) return undefined;
132eeceeb362atagen2026-10-25 03:34 UTC86
  const provided = shortcode?.trim();
132eeceeb362atagen2026-10-25 03:34 UTC87
  if (provided) return provided;
132eeceeb362atagen2026-10-25 03:34 UTC88
  for (const pack of packs) {
132eeceeb362atagen2026-10-25 03:34 UTC89
    const image = pack.images.find((item) => item.reaction_key === key);
132eeceeb362atagen2026-10-25 03:34 UTC90
    if (image) return image.shortcode;
132eeceeb362atagen2026-10-25 03:34 UTC91
  }
132eeceeb362atagen2026-10-25 03:34 UTC92
  return undefined;
132eeceeb362atagen2026-10-25 03:34 UTC93
}
132eeceeb362atagen2026-10-25 03:34 UTC94
132eeceeb362atagen2026-10-25 03:34 UTC95
export function findSticker(packs: ImagePack[], shortcode: string): { pack: ImagePack; sticker: PackImage } {
132eeceeb362atagen2026-10-25 03:34 UTC96
  const name = shortcode.toLowerCase();
132eeceeb362atagen2026-10-25 03:34 UTC97
  const matches = packs.flatMap((pack) => stickersOf(pack)
132eeceeb362atagen2026-10-25 03:34 UTC98
    .filter((sticker) => sticker.shortcode.toLowerCase() === name)
132eeceeb362atagen2026-10-25 03:34 UTC99
    .map((sticker) => ({ pack, sticker })));
132eeceeb362atagen2026-10-25 03:34 UTC100
  if (matches.length === 0) throw new Error(`No sticker named ${shortcode} is available in this room`);
132eeceeb362atagen2026-10-25 03:34 UTC101
  if (matches.length > 1) throw new Error(`More than one sticker is named ${shortcode}, choose one from the sticker menu`);
132eeceeb362atagen2026-10-25 03:34 UTC102
  return matches[0];
132eeceeb362atagen2026-10-25 03:34 UTC103
}
132eeceeb362atagen2026-10-25 03:34 UTC104
d095131eec0eatagen2026-10-24 08:39 UTC105
/** A shortcode from a file name, unique within `taken`. */
d095131eec0eatagen2026-10-24 08:39 UTC106
export function shortcodeFor(filename: string, taken: Set<string>): string {
d095131eec0eatagen2026-10-24 08:39 UTC107
  const stem = filename.replace(/\.[^.]+$/, '');
d095131eec0eatagen2026-10-24 08:39 UTC108
  const base =
d095131eec0eatagen2026-10-24 08:39 UTC109
    stem
d095131eec0eatagen2026-10-24 08:39 UTC110
      .toLowerCase()
d095131eec0eatagen2026-10-24 08:39 UTC111
      .replace(/[^a-z0-9_-]+/g, '_')
d095131eec0eatagen2026-10-24 08:39 UTC112
      .replace(/^_+|_+$/g, '')
d095131eec0eatagen2026-10-24 08:39 UTC113
      .slice(0, 64) || 'image';
d095131eec0eatagen2026-10-24 08:39 UTC114
  let candidate = base;
d095131eec0eatagen2026-10-24 08:39 UTC115
  for (let n = 2; taken.has(candidate); n += 1) candidate = `${base}_${n}`;
d095131eec0eatagen2026-10-24 08:39 UTC116
  taken.add(candidate);
d095131eec0eatagen2026-10-24 08:39 UTC117
  return candidate;
d095131eec0eatagen2026-10-24 08:39 UTC118
}
d095131eec0eatagen2026-10-24 08:39 UTC119
d095131eec0eatagen2026-10-24 08:39 UTC120
export function validShortcode(code: string): boolean {
d095131eec0eatagen2026-10-24 08:39 UTC121
  return /^[^\s:]{1,100}$/.test(code);
d095131eec0eatagen2026-10-24 08:39 UTC122
}
d095131eec0eatagen2026-10-24 08:39 UTC123
d095131eec0eatagen2026-10-24 08:39 UTC124
export function packSlug(name: string): string {
d095131eec0eatagen2026-10-24 08:39 UTC125
  return (
d095131eec0eatagen2026-10-24 08:39 UTC126
    name
d095131eec0eatagen2026-10-24 08:39 UTC127
      .toLowerCase()
d095131eec0eatagen2026-10-24 08:39 UTC128
      .replace(/[^a-z0-9]+/g, '-')
d095131eec0eatagen2026-10-24 08:39 UTC129
      .replace(/^-+|-+$/g, '')
d095131eec0eatagen2026-10-24 08:39 UTC130
      .slice(0, 48) || 'pack'
d095131eec0eatagen2026-10-24 08:39 UTC131
  );
d095131eec0eatagen2026-10-24 08:39 UTC132
}
d095131eec0eatagen2026-10-24 08:39 UTC133
132eeceeb362atagen2026-10-25 03:34 UTC134
export function freePackStateKey(packs: ImagePacks, roomId: string, name: string): string {
132eeceeb362atagen2026-10-25 03:34 UTC135
  const taken = new Set(allPacks(packs).flatMap((pack) =>
132eeceeb362atagen2026-10-25 03:34 UTC136
    pack.source !== 'User' && pack.source.Room.room_id === roomId ? [pack.source.Room.state_key] : []));
132eeceeb362atagen2026-10-25 03:34 UTC137
  const base = packSlug(name);
132eeceeb362atagen2026-10-25 03:34 UTC138
  let key = base;
132eeceeb362atagen2026-10-25 03:34 UTC139
  for (let n = 2; taken.has(key); n += 1) key = `${base}-${n}`;
132eeceeb362atagen2026-10-25 03:34 UTC140
  return key;
132eeceeb362atagen2026-10-25 03:34 UTC141
}
132eeceeb362atagen2026-10-25 03:34 UTC142
132eeceeb362atagen2026-10-25 03:34 UTC143
export function cloneRequest(
132eeceeb362atagen2026-10-25 03:34 UTC144
  source: CloneSource,
132eeceeb362atagen2026-10-25 03:34 UTC145
  to: PackTarget,
132eeceeb362atagen2026-10-25 03:34 UTC146
  usage: PackUsage[],
132eeceeb362atagen2026-10-25 03:34 UTC147
  displayName: string | null,
132eeceeb362atagen2026-10-25 03:34 UTC148
  destination: ImagePack | null
132eeceeb362atagen2026-10-25 03:34 UTC149
): CommandRequest {
132eeceeb362atagen2026-10-25 03:34 UTC150
  if (usage.length === 0) throw new Error('Choose at least one use for the copied images');
132eeceeb362atagen2026-10-25 03:34 UTC151
  if (source.kind === 'pack') {
132eeceeb362atagen2026-10-25 03:34 UTC152
    return { op: 'clone_pack', from_pack: source.pack.id, to, display_name: displayName, usage };
132eeceeb362atagen2026-10-25 03:34 UTC153
  }
132eeceeb362atagen2026-10-25 03:34 UTC154
  if (source.kind === 'image') {
132eeceeb362atagen2026-10-25 03:34 UTC155
    return { op: 'clone_pack_image', from_pack: source.pack.id, shortcode: source.image.shortcode, to, usage };
132eeceeb362atagen2026-10-25 03:34 UTC156
  }
132eeceeb362atagen2026-10-25 03:34 UTC157
  const taken = new Set(destination?.images.map((image) => image.shortcode) ?? []);
132eeceeb362atagen2026-10-25 03:34 UTC158
  return {
132eeceeb362atagen2026-10-25 03:34 UTC159
    op: 'clone_sticker', room_id: source.room_id, event_id: source.event_id, to,
132eeceeb362atagen2026-10-25 03:34 UTC160
    shortcode: shortcodeFor(source.body || 'sticker', taken), usage
132eeceeb362atagen2026-10-25 03:34 UTC161
  };
132eeceeb362atagen2026-10-25 03:34 UTC162
}
132eeceeb362atagen2026-10-25 03:34 UTC163
d095131eec0eatagen2026-10-24 08:39 UTC164
export interface CloneDestination {
d095131eec0eatagen2026-10-24 08:39 UTC165
  label: string;
d095131eec0eatagen2026-10-24 08:39 UTC166
  target: PackTarget;
d095131eec0eatagen2026-10-24 08:39 UTC167
  display_name: string | null;
d095131eec0eatagen2026-10-24 08:39 UTC168
}
d095131eec0eatagen2026-10-24 08:39 UTC169
d095131eec0eatagen2026-10-24 08:39 UTC170
/** Where a whole pack can be cloned to: merged into your pack, or as a new pack in a room you can edit. */
d095131eec0eatagen2026-10-24 08:39 UTC171
export function clonePackDestinations(
d095131eec0eatagen2026-10-24 08:39 UTC172
  pack: ImagePack,
d095131eec0eatagen2026-10-24 08:39 UTC173
  rooms: { room_id: string; name: string }[],
d095131eec0eatagen2026-10-24 08:39 UTC174
  roomName: (roomId: string) => string
d095131eec0eatagen2026-10-24 08:39 UTC175
): CloneDestination[] {
d095131eec0eatagen2026-10-24 08:39 UTC176
  const name = packLabel(pack, roomName);
d095131eec0eatagen2026-10-24 08:39 UTC177
  return [
d095131eec0eatagen2026-10-24 08:39 UTC178
    { label: 'Merge into your pack', target: { kind: 'user' }, display_name: null },
d095131eec0eatagen2026-10-24 08:39 UTC179
    ...rooms.map((room) => ({
d095131eec0eatagen2026-10-24 08:39 UTC180
      label: `New pack in ${room.name}`,
d095131eec0eatagen2026-10-24 08:39 UTC181
      target: { kind: 'room', room_id: room.room_id, state_key: packSlug(name) } as PackTarget,
d095131eec0eatagen2026-10-24 08:39 UTC182
      display_name: name
d095131eec0eatagen2026-10-24 08:39 UTC183
    }))
d095131eec0eatagen2026-10-24 08:39 UTC184
  ];
d095131eec0eatagen2026-10-24 08:39 UTC185
}