Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion app/src/features/bots/CrewAccessList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,17 @@ export function CrewAccessList({ bot }: { bot: Bot }) {
className="flex items-center gap-2 rounded-lg border border-line bg-sunken py-1.5 pr-1 pl-2.5"
>
<Network aria-hidden size={14} className="shrink-0 text-muted" />
<span className="min-w-0 flex-1 text-xs">{what}</span>
<span className="min-w-0 flex-1 text-xs">
<span className="block">{what}</span>
<span className="block text-ink-soft">
{[
access.talk && words.crewsKinds.talk,
access.edit ? words.crewsKinds.edit : access.read && words.crewsKinds.read,
]
.filter(Boolean)
.join(" · ")}
</span>
</span>
<Button
variant="ghost"
size="sm"
Expand Down
8 changes: 7 additions & 1 deletion app/src/features/chat/CrewAccessCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,13 @@ export function CrewAccessCard({ approval, bot }: { approval: ApprovalItem; bot:
<ul className="flex flex-col gap-1">
{(asked.access ?? []).map((kind) => (
<li key={kind} className="text-ink-soft">
{kind === "talk" ? w.talk(target ? target : crew) : kind}
{kind === "talk"
? w.talk(target ?? crew)
: kind === "read"
? w.read(target ?? crew)
: kind === "edit"
? w.edit(target ?? crew)
: kind}
</li>
))}
</ul>
Expand Down
7 changes: 5 additions & 2 deletions app/src/features/chat/crewAccess.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ const request = (bot: string | null) =>
crew: "Blog",
bot,
handle: bot?.toLowerCase() ?? null,
access: ["talk"],
access: ["talk", "read"],
why: "To get this week's post.",
});

Expand All @@ -42,6 +42,7 @@ describe("a bot asking to reach another crew", () => {
name: "Scout wants to reach Writer, of the crew Blog",
});
expect(within(card).getByText(/^Talk with Writer/)).toBeDefined();
expect(within(card).getByText("Read the files of Writer and of its work folder")).toBeDefined();
expect(within(card).getByText("To get this week's post.")).toBeDefined();
expect(within(card).getByRole("button", { name: "Only now" })).toBeDefined();
expect(within(card).getByRole("button", { name: "Always the crew Blog" })).toBeDefined();
Expand All @@ -67,11 +68,13 @@ describe("a bot asking to reach another crew", () => {
test("lasting access shows in the bot's details and can be taken back", async () => {
const { fake, scout } = await openScout();
fake.crewAccess.add(scout.id, "Blog");
fake.crewAccess.add(scout.id, "Shop", { id: "bot_clerk", name: "Clerk" });
fake.crewAccess.add(scout.id, "Shop", { id: "bot_clerk", name: "Clerk" }, { edit: true });
fireEvent.click(screen.getByRole("button", { name: /^Show details/ }));
const details = screen.getByRole("complementary", { name: "About Scout" });
expect(await within(details).findByText("The whole crew Blog")).toBeDefined();
expect(within(details).getByText("Clerk, of the crew Shop")).toBeDefined();
expect(within(details).getByText("talk")).toBeDefined();
expect(within(details).getByText("edit files")).toBeDefined();
fireEvent.click(
within(details).getByRole("button", { name: "Take back: The whole crew Blog" }),
);
Expand Down
1 change: 1 addition & 0 deletions app/src/i18n/en/bots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,5 +137,6 @@ export const bots = {
crewsBot: (bot: string, crew: string) => `${bot}, of the crew ${crew}`,
crewsRemove: (what: string) => `Take back: ${what}`,
crewsRemoveFailed: "Could not take it back",
crewsKinds: { talk: "talk", read: "read files", edit: "edit files" },
},
};
2 changes: 2 additions & 0 deletions app/src/i18n/en/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,8 @@ export const chat = {
titleBot: (bot: string, other: string, crew: string) =>
`${bot} wants to reach ${other}, of the crew ${crew}`,
talk: (whom: string) => `Talk with ${whom}: see who is there and send messages and tasks`,
read: (whom: string) => `Read the files of ${whom} and of its work folder`,
edit: (whom: string) => `Change and add files of ${whom} and of its work folder`,
why: (bot: string) => `Why, says ${bot}:`,
explain: "Crews do not see each other. Only now lasts until the bot ends this turn.",
once: "Only now",
Expand Down
1 change: 1 addition & 0 deletions app/src/i18n/es/bots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,5 +135,6 @@ export const bots: Messages["bots"] = {
crewsBot: (bot, crew) => `${bot}, del equipo ${crew}`,
crewsRemove: (what) => `Quitar el acceso: ${what}`,
crewsRemoveFailed: "No se pudo quitar el acceso",
crewsKinds: { talk: "hablar", read: "leer archivos", edit: "editar archivos" },
},
};
2 changes: 2 additions & 0 deletions app/src/i18n/es/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,8 @@ export const chat: Messages["chat"] = {
titleCrew: (bot, crew) => `${bot} quiere acceder al equipo ${crew}`,
titleBot: (bot, other, crew) => `${bot} quiere acceder a ${other}, del equipo ${crew}`,
talk: (whom) => `Hablar con ${whom}: ver quién está y enviar mensajes y tareas`,
read: (whom) => `Leer los archivos de ${whom} y de su carpeta de trabajo`,
edit: (whom) => `Cambiar y crear archivos de ${whom} y de su carpeta de trabajo`,
why: (bot) => `Por qué, según ${bot}:`,
explain: "Los equipos no se ven entre sí. Solo ahora vale hasta que el bot termine este turno.",
once: "Solo ahora",
Expand Down
1 change: 1 addition & 0 deletions app/src/i18n/pt-BR/bots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,5 +134,6 @@ export const bots: Messages["bots"] = {
crewsBot: (bot, crew) => `${bot}, da equipe ${crew}`,
crewsRemove: (what) => `Tirar o acesso: ${what}`,
crewsRemoveFailed: "Não foi possível tirar o acesso",
crewsKinds: { talk: "conversar", read: "ler arquivos", edit: "editar arquivos" },
},
};
2 changes: 2 additions & 0 deletions app/src/i18n/pt-BR/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,8 @@ export const chat: Messages["chat"] = {
titleCrew: (bot, crew) => `${bot} quer acessar a equipe ${crew}`,
titleBot: (bot, other, crew) => `${bot} quer acessar ${other}, da equipe ${crew}`,
talk: (whom) => `Conversar com ${whom}: ver quem está lá e mandar mensagens e tarefas`,
read: (whom) => `Ler os arquivos de ${whom} e da pasta de trabalho`,
edit: (whom) => `Mudar e criar arquivos de ${whom} e da pasta de trabalho`,
why: (bot) => `Por quê, segundo ${bot}:`,
explain: "As equipes não se enxergam. Só agora vale até o bot terminar esta vez.",
once: "Só agora",
Expand Down
10 changes: 9 additions & 1 deletion app/src/lib/fakeCrewAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,22 @@ export class FakeCrewAccess {
constructor(private readonly fake: FakeBotloft) {}

/** `bot` may reach `crewName` for good, or only `target` in it. */
add(bot: BotId, crewName: string, target?: { id: BotId; name: string }): CrewAccess {
add(
bot: BotId,
crewName: string,
target?: { id: BotId; name: string },
kinds: { talk?: boolean; read?: boolean; edit?: boolean } = { talk: true },
): CrewAccess {
const access: CrewAccess = {
id: `cxa_${this.next++}`,
botId: bot,
crewId: `crw_${crewName.toLowerCase()}`,
crewName,
targetBotId: target?.id ?? null,
targetName: target?.name ?? null,
talk: kinds.talk ?? false,
read: (kinds.read ?? false) || (kinds.edit ?? false),
edit: kinds.edit ?? false,
createdAt: this.fake.now,
};
this.list.push(access);
Expand Down
12 changes: 12 additions & 0 deletions app/src/lib/protocol.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1019,6 +1019,18 @@ botId: BotId, crewId: CrewId, crewName: string,
* One bot of the crew; `null` for the whole crew.
*/
targetBotId: BotId | null, targetName: string | null,
/**
* Talking with its bots: roster, messages and tasks.
*/
talk: boolean,
/**
* Reading their files; true whenever `edit` is.
*/
read: boolean,
/**
* Changing and adding files.
*/
edit: boolean,
/**
* Unix time in milliseconds.
*/
Expand Down
6 changes: 6 additions & 0 deletions crates/botloft-core/src/protocol/crew_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ pub struct CrewAccess {
/// One bot of the crew; `null` for the whole crew.
pub target_bot_id: Option<BotId>,
pub target_name: Option<String>,
/// Talking with its bots: roster, messages and tasks.
pub talk: bool,
/// Reading their files; true whenever `edit` is.
pub read: bool,
/// Changing and adding files.
pub edit: bool,
/// Unix time in milliseconds.
pub created_at: i64,
}
Expand Down
3 changes: 3 additions & 0 deletions crates/botloft-store/migrations/0022_crew_access_files.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-- Reading and editing another crew's files, besides talking (spec 10.4).
ALTER TABLE crew_access ADD COLUMN read INTEGER NOT NULL DEFAULT 0;
ALTER TABLE crew_access ADD COLUMN edit INTEGER NOT NULL DEFAULT 0;
71 changes: 57 additions & 14 deletions crates/botloft-store/src/crew_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,28 @@ use rusqlite::{OptionalExtension, Row, params};

use crate::{Result, Store, parse_column};

/// What a bot may do in another crew. Editing files includes reading them.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AccessKinds {
/// Talking with its bots: roster, messages and tasks.
pub talk: bool,
/// Listing and reading their files.
pub read: bool,
/// Changing and adding files.
pub edit: bool,
}

impl AccessKinds {
/// Everything either one allows.
pub fn with(self, other: Self) -> Self {
Self {
talk: self.talk || other.talk,
read: self.read || other.read,
edit: self.edit || other.edit,
}
}
}

/// A bot's lasting access to another crew, or to one bot of it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrewAccessRecord {
Expand All @@ -15,12 +37,11 @@ pub struct CrewAccessRecord {
pub crew_id: CrewId,
/// One bot of the crew; `None` for the whole crew.
pub target_bot_id: Option<BotId>,
/// Talking with its bots: roster, messages and tasks.
pub talk: bool,
pub kinds: AccessKinds,
pub created_at: i64,
}

const COLUMNS: &str = "id, bot_id, crew_id, target_bot_id, talk, created_at";
const COLUMNS: &str = "id, bot_id, crew_id, target_bot_id, talk, read, edit, created_at";

fn from_row(row: &Row<'_>) -> rusqlite::Result<CrewAccessRecord> {
let target: Option<String> = row.get(3)?;
Expand All @@ -32,8 +53,12 @@ fn from_row(row: &Row<'_>) -> rusqlite::Result<CrewAccessRecord> {
Some(_) => Some(parse_column(row, 3)?),
None => None,
},
talk: row.get(4)?,
created_at: row.get(5)?,
kinds: AccessKinds {
talk: row.get(4)?,
read: row.get(5)?,
edit: row.get(6)?,
},
created_at: row.get(7)?,
})
}

Expand All @@ -45,20 +70,24 @@ impl Store {
bot: &BotId,
crew: &CrewId,
target: Option<&BotId>,
talk: bool,
kinds: AccessKinds,
now: i64,
) -> Result<()> {
self.conn.execute(
"INSERT INTO crew_access (id, bot_id, crew_id, target_bot_id, talk, created_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6) \
"INSERT INTO crew_access \
(id, bot_id, crew_id, target_bot_id, talk, read, edit, created_at) \
VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8) \
ON CONFLICT (bot_id, crew_id, coalesce(target_bot_id, '')) DO UPDATE SET \
talk = talk OR excluded.talk",
talk = talk OR excluded.talk, read = read OR excluded.read, \
edit = edit OR excluded.edit",
params![
CrewAccessId::generate().as_str(),
bot.as_str(),
crew.as_str(),
target.map(BotId::as_str),
talk,
kinds.talk,
kinds.read,
kinds.edit,
now,
],
)?;
Expand Down Expand Up @@ -95,8 +124,15 @@ impl Store {
mod tests {
use botloft_core::ids::CrewId;

use super::AccessKinds;
use crate::tests::Fixture;

const TALK: AccessKinds = AccessKinds {
talk: true,
read: false,
edit: false,
};

#[test]
fn access_is_kept_per_bot_and_reach_and_only_grows() {
let fx = Fixture::new();
Expand All @@ -110,19 +146,26 @@ mod tests {
})
.expect("blog");
fx.store
.grant_crew_access(scout, &blog, Some(writer), true, 10)
.grant_crew_access(scout, &blog, Some(writer), TALK, 10)
.expect("bot");
fx.store
.grant_crew_access(scout, &blog, None, false, 20)
.grant_crew_access(scout, &blog, None, AccessKinds::default(), 20)
.expect("crew");
fx.store
.grant_crew_access(scout, &blog, None, true, 30)
.grant_crew_access(scout, &blog, None, TALK, 30)
.expect("crew again");
let edit = AccessKinds {
edit: true,
..AccessKinds::default()
};
fx.store
.grant_crew_access(scout, &blog, None, edit, 40)
.expect("and files");
let access = fx.store.crew_access(scout).expect("list");
assert_eq!(access.len(), 2);
assert_eq!(access[0].target_bot_id.as_ref(), Some(writer));
assert!(access[1].target_bot_id.is_none());
assert!(access[1].talk);
assert!(access[1].kinds.talk && access[1].kinds.edit && !access[1].kinds.read);
assert_eq!(access[1].created_at, 20);
assert!(fx.store.crew_access(writer).expect("none").is_empty());

Expand Down
2 changes: 1 addition & 1 deletion crates/botloft-store/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ use rusqlite::{Connection, Row, ffi};

pub use approvals::ApprovalRecord;
pub use bots::BotRecord;
pub use crew_access::CrewAccessRecord;
pub use crew_access::{AccessKinds, CrewAccessRecord};
pub use deliveries::DeliveryOutcome;
pub use messages::MessageFilter;
pub use migrate::LATEST_VERSION;
Expand Down
1 change: 1 addition & 0 deletions crates/botloft-store/src/migrate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const MIGRATIONS: &[(u32, &str)] = &[
(19, include_str!("../migrations/0019_reactions.sql")),
(20, include_str!("../migrations/0020_desktop_grants.sql")),
(21, include_str!("../migrations/0021_crew_access.sql")),
(22, include_str!("../migrations/0022_crew_access_files.sql")),
];

/// Schema version after every migration has run.
Expand Down
Loading
Loading