chiark / gitweb /
cli: Provide set-access subcommand
[otter.git] / cli / forgame.rs
1 // Copyright 2020-2021 Ian Jackson and contributors to Otter
2 // SPDX-License-Identifier: AGPL-3.0-or-later
3 // There is NO WARRANTY.
4
5 use super::*;
6 use super::usebundles::*;
7
8 // todo: list-players
9
10 //---------- reset ----------
11
12 mod reset_game {
13   use super::*;
14
15   #[derive(Default,Debug)]
16   struct Args {
17     table_file: Option<String>,
18     game_spec: String,
19     bundles: Vec<String>,
20     bundles_only: bool,
21   }
22
23   fn subargs(sa: &mut Args) -> ArgumentParser {
24     use argparse::*;
25     let mut ap = ArgumentParser::new();
26     ap.refer(&mut sa.table_file).metavar("TABLE-SPEC[-TOML")
27       .add_option(&["--reset-table"],StoreOption,
28                   "reset the players and access too");
29     ap.refer(&mut sa.game_spec).required()
30       .add_argument("GAME-SPEC",Store,
31                     "game spec, as found in server, \
32                      or local filename if it contains a '/')");
33     ap.refer(&mut sa.bundles).required()
34       .add_argument("BUNDLES",Collect,
35                     "Bundle files to use.  If any are specified, \
36                      all needed bundles must be specified, as any \
37                      not mentioned will be cleared from the server.");
38     let mut bundles_only = ap.refer(&mut sa.bundles_only);
39     bundles_only.add_option(&["--bundles-only"],StoreTrue,
40               "insist that server has only the specified BUNDLES \
41                (clearing out the server if no BUNDLES were specified)");
42     bundles_only.add_option(&["--bundles-at-least"],StoreFalse,
43               "don't care if the server has additional bundles uploaded \
44                earlier (default)");
45     ap
46   }
47
48   fn call(SCCA{ ma, args, mut out,.. }:SCCA) -> Result<(),AE> {
49     let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
50     let instance_name = ma.instance();
51     let mut chan = ma.access_game()?;
52
53     let reset_insn =
54       if let Some(filename) = spec_arg_is_path(&args.game_spec) {
55         let spec_toml = read_spec_from_path(
56           filename, SpecRaw::<GameSpec>::new())?;
57
58         let spec_toml = bundles::spec_macroexpand(spec_toml, &mut |what,data|{
59           if ma.verbose >= 2 {
60             for (lno,l) in data.split('\n').enumerate() {
61               writeln!(out, "spec {} {} {}", what, lno+1, l)?;
62             }
63           }
64           Ok(())
65         })
66           .context("failed to template expand game spec")?;
67
68         MGI::ResetFromGameSpec { spec_toml }
69       } else {
70         MGI::ResetFromNamedSpec { spec: args.game_spec.clone() }
71       };
72
73     let mut insns = vec![];
74
75     if let Some(table_file) = args.table_file {
76       let table_spec = read_spec(&ma, &table_file, SpecParseToml::new())?;
77       let game = chan.game.clone();
78       chan.cmd(&MgmtCommand::CreateGame {
79         game,
80         insns: vec![],
81       }).map(|_|()).or_else(|e| {
82         if let Some(&MgmtError::AlreadyExists) = e.downcast_ref() {
83           return Ok(())
84         }
85         Err(e)
86       })?;
87
88       insns.extend(setup_table(&ma, &instance_name, &table_spec, true)?);
89     }
90
91     if args.bundles_only || args.bundles.len() != 0 {
92       let n_bundles = args.bundles.len();
93       let progress = ma.progressbar()?;
94       let mut progress = termprogress::Nest::with_total
95         (n_bundles as f32, progress);
96       let bundle_i_msg = |i| Some(format!("{}/{}", i, n_bundles));
97
98       let local = args.bundles.into_iter().enumerate().map(|(i,file)| {
99         progress.start_phase(0., bundle_i_msg(i), "preparing".into());
100         BundleForUpload::prepare(file, &mut progress)
101       }).collect::<Result<Vec<_>,_>>()?;
102
103       let resp = chan.cmd(&MgmtCommand::ListBundles { game: ma.instance() })?;
104       let remote = match resp {
105         MR::Bundles { bundles } => bundles,
106         x => throw!(anyhow!("unexpected response to ListBundles: {:?}",x)),
107       };
108       progress.clear();
109
110       let bundles_only = args.bundles_only;
111       match Itertools::zip_longest(
112         local.iter().rev(),
113         remote.iter().rev(),
114       ).map(|eob| {
115         use EitherOrBoth::*;
116         use bundles::State::*;
117         match eob {
118           Right((id, remote)) => if bundles_only {
119             Err(format!("server has additional bundle(s) eg {} {}",
120                         id, remote))
121           } else {
122             Ok(())
123           },
124           Left(local) => {
125             Err(format!("server is missing {} {} {}",
126                         local.kind, local.hash, local.file))
127           },
128           Both(_local, (id, Uploading)) => {
129             Err(format!("server has incomplete upload :{}", id))
130           },
131           Both(local, (id, Loaded(remote))) => {
132             if (local.size,  local.hash) !=
133                (remote.size, remote.hash) {
134                Err(format!("server's {} does not match {}", id, &local.file))
135             } else {
136                Ok(())
137             }
138           }
139         }
140       }).find_map(Result::err).map_or_else(|| Ok(()), Err) {
141         Ok(()) => {
142           if ma.verbose >= 0 {
143             eprintln!("Reusing server's existing bundles");
144           }
145         },
146         Err(why) => {
147           if ma.verbose >= 0 {
148             eprintln!("Re-uploading bundles: {}", why);
149           }
150           if bundles_only {
151             clear_game(&ma, &mut chan)?;
152           }
153           for (i, bundle) in local.into_iter().enumerate() {
154             progress.start_phase(PROGFRAC_UPLOAD,
155                                  bundle_i_msg(i),
156                                  "uploading".into());
157             bundle.upload(&ma, &mut chan, &mut progress)?;
158           }
159         },
160       }
161     }
162
163     insns.push(reset_insn);
164
165     chan.alter_game(insns, None)?;
166
167     if ma.verbose >= 0 {
168       eprintln!("reset successful.");
169     }
170     Ok(())
171   }
172
173   inventory_subcmd!{
174     "reset",
175     "Reset the state of the game table",
176   }
177 }
178
179 //---------- set-access ----------
180
181 mod set_access {
182   use super::*;
183
184   #[derive(Default,Debug)]
185   struct Args {
186     table_file: String,
187   }
188
189   fn subargs(sa: &mut Args) -> ArgumentParser {
190     use argparse::*;
191     let mut ap = ArgumentParser::new();
192
193     ap.refer(&mut sa.table_file).required()
194       .add_argument("TABLE-SPEC[-TOML",Store,
195                     "table spec filename");
196     ap
197   }
198
199   fn call(SCCA{ ma, args,.. }:SCCA) -> Result<(),AE> {
200     let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
201     let instance_name = ma.instance();
202     let mut chan = ma.access_game()?;
203
204     let table_spec = read_spec(&ma, &args.table_file, SpecParseToml::new())?;
205     let insns = setup_table(&ma, &instance_name, &table_spec, false)?;
206     chan.alter_game(insns, None)?;
207
208     if ma.verbose >= 0 {
209       eprintln!("access update successful.");
210     }
211     Ok(())
212   }
213
214   inventory_subcmd!{
215     "set-access",
216     "Set the table's access control list",
217   }
218 }
219
220 //---------- set-link ----------
221
222 mod set_link {
223
224   use super::*;
225
226   #[derive(Debug,Default)]
227   struct Args {
228     kind: Option<LinkKind>,
229     url: Option<String>,
230   }
231
232   fn subargs(sa: &mut Args) -> ArgumentParser {
233     use argparse::*;
234     let mut ap = ArgumentParser::new();
235     ap.refer(&mut sa.kind)
236       .add_argument("LINK-KIND",StoreOption,"link kind");
237     ap.refer(&mut sa.url)
238       .add_argument("URL",StoreOption,"url (or empty for none)");
239     ap
240   }
241
242   #[throws(AE)]
243   fn call(SCCA{ mut out, ma, args,.. }:SCCA) {
244     let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
245     let mut chan = ma.access_game()?;
246
247     match args.url {
248       None => {
249         let MgmtGameResponseGameInfo { links, .. } = chan.info()?;
250         for (tk, v) in links {
251           let v: Url = (&v).try_into().context("reparse sererr's UrlSpec")?;
252           match args.kind {
253             None => {
254               writeln!(out, "{:<10} {}", tk, &v)?;
255             }
256             Some(wk) => {
257               if wk == tk {
258                 writeln!(out, "{}", &v)?;
259               }
260             }
261           }
262         }
263       },
264
265       Some(url) => {
266         let kind = args.kind.unwrap();
267         chan.alter_game(vec![
268           if url == "" {
269             MGI::RemoveLink { kind }
270           } else {
271             MGI::SetLink { kind, url: UrlSpec(url) }
272           }
273         ], None)?;
274       },
275     }
276   }
277
278   inventory_subcmd!{
279     "set-link",
280     "Set one of the info links visible from within the game",
281   }
282 }
283
284 //---------- join-game ----------
285
286 mod join_game {
287   use super::*;
288
289   #[derive(Default,Debug)]
290   struct Args {
291     reset_access: bool,
292   }
293
294   fn subargs(sa: &mut Args) -> ArgumentParser {
295     use argparse::*;
296     let mut ap = ArgumentParser::new();
297     ap.refer(&mut sa.reset_access)
298       .add_option(&["--reset"],StoreTrue,
299                   "generate and deliver new player access token");
300     ap
301   }
302
303   fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
304     let args = parse_args::<Args,_>(args, &subargs, &ok_id, None);
305     let mut chan = ma.access_game()?;
306
307     let mut insns = vec![];
308     match chan.has_player(&ma.account)? {
309       None => {
310         let nick = ma.nick.clone()
311           .unwrap_or_else(|| ma.account.default_nick());
312         let details = MgmtPlayerDetails { nick: Some(nick) };
313         insns.push(MGI::JoinGame { details });
314       }
315       Some((player, mpi)) => {
316         writeln!(out, "already in game, as player #{} {:?}",
317                  player.0.get_idx_version().0, &mpi.nick)?;
318         let MgmtPlayerInfo { nick, account:_ } = mpi;
319         if let Some(new_nick) = &ma.nick {
320           if &nick != new_nick {
321             writeln!(out, "changing nick to {:?}", &new_nick)?;
322             let details = MgmtPlayerDetails { nick: ma.nick.clone() };
323             insns.push(MGI::UpdatePlayer { player, details });
324           }
325         }
326         if args.reset_access {
327           writeln!(out, "resetting access token (invalidating other URLs)")?;
328           insns.push(MGI::ResetPlayerAccess(player));
329         } else {
330           writeln!(out, "redelivering existing access token")?;
331           insns.push(MGI::RedeliverPlayerAccess(player));
332         }
333       }
334     };
335
336     fn deliver(out: &mut CookedStdout, token: &AccessTokenReport) {
337       for l in &token.lines {
338         if l.contains(char::is_control) {
339           writeln!(out, "Server token info contains control chars! {:?}", &l)
340         } else {
341           writeln!(out, " {}", &l)
342         }.unwrap()
343       }
344     }
345
346     for resp in chan.alter_game(insns, None)? {
347       match resp {
348         MGR::JoinGame { nick, player, token } => {
349           writeln!(out, "joined game as player #{} {:?}",
350                    player.0.get_idx_version().0,
351                    &nick)?;
352           deliver(&mut out, &token);
353         }
354         MGR::PlayerAccessToken(token) => {
355           deliver(&mut out, &token);
356         }
357         MGR::Fine => {}
358         _ => throw!(anyhow!("unexpected response to instruction(s)")),
359       }
360     }
361
362     Ok(())
363   }
364
365   inventory_subcmd!{
366     "join-game",
367     "Join a game or reset access token (creating or updating account)",
368   }
369 }
370
371 //---------- leave-game ----------
372
373 mod leave_game {
374   use super::*;
375
376   type Args = NoArgs;
377
378   fn call(SCCA{ mut out, ma, args,.. }:SCCA) -> Result<(),AE> {
379     let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
380     let mut chan = ma.access_game()?;
381
382     let player = match chan.has_player(&ma.account)? {
383       None => {
384         writeln!(out, "this account is not a player in that game")?;
385         exit(EXIT_NOTFOUND);
386       }
387       Some((player, _)) => player,
388     };
389
390     chan.alter_game(vec![MGI::LeaveGame(player)], None)?;
391
392     Ok(())
393   }
394
395   inventory_subcmd!{
396     "leave-game",
397     "Leave a game",
398   }
399 }
400
401 //---------- clear game ----------
402
403 #[throws(AE)]
404 fn clear_game(ma: &MainOpts, chan: &mut MgmtChannelForGame) {
405   chan.alter_game(vec![MGI::ClearGame{ }], None)
406     .context("clear table")?;
407   chan.cmd(&MC::ClearBundles { game: ma.instance() })
408     .context("clear bundles")?;
409 }
410
411 mod clear_game {
412   use super::*;
413
414   type Args = NoArgs;
415
416   #[throws(AE)]
417   fn call(SCCA{ ma, args,.. }:SCCA) {
418     let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
419     let mut chan = ma.access_game()?;
420     clear_game(&ma, &mut chan)?;
421   }
422
423   inventory_subcmd!{
424     "clear-game",
425     "clear the table and clear out all bundles",
426   }
427 }
428
429 //---------- delete-game ----------
430
431 mod delete_game {
432   use super::*;
433
434   type Args = NoArgs;
435
436   fn call(SCCA{ ma, args,.. }:SCCA) -> Result<(),AE> {
437     let _args = parse_args::<Args,_>(args, &noargs, &ok_id, None);
438     let mut chan = ma.access_game()?;
439     let game = chan.game.clone();
440     chan.cmd(&MC::DestroyGame { game })?;
441     Ok(())
442   }
443
444   inventory_subcmd!{
445     "delete-game",
446     "Delete a game (throwing all the players out of it)",
447   }
448 }