Merge branch 'main' into fixtures

This commit is contained in:
Savanni D'Gerinel 2022-03-04 09:44:51 -05:00
commit 9a3676d696
46 changed files with 10039 additions and 1200 deletions

4
.gitignore vendored
View File

@ -1 +1,3 @@
**/target node_modules
target
*.db

1221
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,5 @@
[workspace] [workspace]
members = [ members = [
"common", "common",
"gtk", "servilo",
"server",
] ]

84
common/Cargo.lock generated Normal file
View File

@ -0,0 +1,84 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "common"
version = "0.1.0"
dependencies = [
"serde",
"serde_derive",
"thiserror",
]
[[package]]
name = "proc-macro2"
version = "1.0.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f84e92c0f7c9d58328b85a78557813e4bd845130db68d7184635344399423b1"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [
"proc-macro2",
]
[[package]]
name = "serde"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008"
[[package]]
name = "serde_derive"
version = "1.0.132"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ecb2e6da8ee5eb9a61068762a32fa9619cc591ceb055b3687f4cd4051ec2e06b"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"

View File

@ -172,8 +172,8 @@ impl CharacterSheet {
"a" "a"
}; };
format!( format!(
"{} {} {} who {}", "{} {} {:?} who {}",
article, self.descriptor, self.character_type.text, self.focus article, self.descriptor, self.character_type, self.focus
) )
} }
@ -191,72 +191,27 @@ impl CharacterSheet {
} }
fn might_pool(&self) -> Pool { fn might_pool(&self) -> Pool {
/* Pool {
let (base, edge) = match self.character_type { current: 0,
CharacterType::Glaive => (11, 1), max: 0,
CharacterType::Jack { edge: 0,
edge: PoolType::Might, }
} => (10, 1),
CharacterType::Jack { edge: _ } => (10, 0),
CharacterType::Nano => (7, 0),
CharacterType::Arkus => (8, 0),
CharacterType::Delve => (9, 0),
CharacterType::Wright => (9, 0),
};
let max = base + self.initial_points.might;
let current = max;
*/
let max = 10;
let current = 10;
let edge = 1;
Pool { max, current, edge }
} }
fn speed_pool(&self) -> Pool { fn speed_pool(&self) -> Pool {
/* Pool {
let (base, edge) = match self.character_type { current: 0,
CharacterType::Glaive => (10, 1), max: 0,
CharacterType::Jack { edge: 0,
edge: PoolType::Speed, }
} => (10, 1),
CharacterType::Jack { edge: _ } => (10, 0),
CharacterType::Nano => (9, 0),
CharacterType::Arkus => (9, 0),
CharacterType::Delve => (9, 1),
CharacterType::Wright => (7, 0),
};
let max = base + self.initial_points.speed;
let current = max;
*/
let max = 10;
let current = 10;
let edge = 1;
Pool { max, current, edge }
} }
fn intellect_pool(&self) -> Pool { fn intellect_pool(&self) -> Pool {
/* Pool {
let (base, edge) = match self.character_type { current: 0,
CharacterType::Glaive => (7, 0), max: 0,
CharacterType::Jack { edge: 0,
edge: PoolType::Intellect, }
} => (10, 1),
CharacterType::Jack { edge: _ } => (10, 0),
CharacterType::Nano => (12, 1),
CharacterType::Arkus => (11, 1),
CharacterType::Delve => (10, 1),
CharacterType::Wright => (12, 0),
};
let max = base + self.initial_points.intellect;
let current = max;
*/
let max = 10;
let current = 10;
let edge = 1;
Pool { max, current, edge }
} }
} }
@ -298,8 +253,6 @@ starting_pools:
edge: flexible edge: flexible
"; ";
/*
#[allow(dead_code)]
pub fn opal() -> CharacterSheet { pub fn opal() -> CharacterSheet {
CharacterSheet { CharacterSheet {
name: "Opal".to_owned(), name: "Opal".to_owned(),
@ -313,78 +266,13 @@ edge: flexible
speed: 2, speed: 2,
intellect: 3, intellect: 3,
}, },
/*
pools: Pools {
might: Pool {
current: 11,
max: 11,
edge: 0,
},
speed: Pool {
current: 12,
max: 12,
edge: 0,
},
intellect: Pool {
current: 13,
max: 13,
edge: 1,
},
},
*/
/*
tier: Tier::new(1).unwrap(),
effort: 1,
cypher_limit: 3,
special_abilities: vec![
SpecialAbility { name: "Versatile".to_owned(),
cost: Cost::Nothing,
description: "+2 to any pool. It can be reassigned after each ten-hour recovery roll.".to_owned() },
SpecialAbility { name: "Flex Skill".to_owned(),
cost: Cost::Nothing,
description: "at the beginning of each day, choose one skill (other than attack or defense). For the rest of the day, you are trained in that skill.".to_owned() },
SpecialAbility{ name: "Face Morph".to_owned(),
cost: Cost::Constant { stat: PoolType::Intellect, cost: 2 },
description: "You alter your features and coloration for one hour.".to_owned() },
],
skills: vec![
Skill{ skill: "Perception".to_owned(), rank: SkillRank::Trained },
Skill{ skill: "Resilient".to_owned(), rank: SkillRank::Trained },
Skill{ skill: "Social interactions".to_owned(), rank: SkillRank::Trained },
Skill{ skill: "Pleasant social interactiosn".to_owned(), rank: SkillRank::Specialized },
],
attacks: vec![
Attack { weapon: "Crank Crossbow".to_owned(), damage: 4, range: Range::Long, ammo: Some(25), },
Attack { weapon: "Rapier".to_owned(), damage: 2, range: Range::Immediate, ammo: None, },
],
defenses: vec![
Defense {
armor: "Brigandine".to_owned(),
defense: 2,
speed_cost: 2,
}
],
equipment: vec![
"A book for recording favorite words, inspiration stories, and speech anecdotes.".to_owned(),
"Explorer's pack".to_owned(),
],
cyphers: vec![
Cypher {
name: "Flying Cap".to_owned(),
level: 3,
form: "Hat".to_owned(),
description: "Whatever".to_owned(),
}
],
*/
} }
} }
*/
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::{test_data::*, *}; use super::test_data::*;
use serde_yaml; use serde_yaml;
#[test] #[test]
@ -404,7 +292,8 @@ mod test {
assert_eq!(jack.text, "Jack"); assert_eq!(jack.text, "Jack");
} }
/* use super::*;
#[test] #[test]
fn opals_character_sheet() { fn opals_character_sheet() {
let opal = super::test_data::opal(); let opal = super::test_data::opal();
@ -441,5 +330,4 @@ mod test {
} }
); );
} }
*/
} }

2
desegnoj/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
resources
public

View File

@ -0,0 +1,6 @@
---
title: "{{ replace .Name "-" " " | title }}"
date: {{ .Date }}
draft: true
---

47
desegnoj/assets/main.scss Normal file
View File

@ -0,0 +1,47 @@
body {
background-color: rgb(200, 190, 180);
}
.columns {
display: flex;
// border: 1px solid red;
margin: auto;
padding: 1em;
> * {
margin: 0 1em 0 1em;
}
& :first-child {
margin: 0 1em 0 0;
}
& :last-child {
margin: 0 0 0 1em;
}
}
.c-2 {
> div {
width: 50%;
}
}
.statpool {
width: 50%;
flex-grow: 0;
flex-shrink: 1;
.pool {
border: 1px solid black;
border-radius: 5px;
padding: 0.5em;
margin: 0.25em;
}
.edge {
border: 1px solid black;
border-radius: 5px;
padding: 0.5em;
margin: 0.25em;
}
}

9
desegnoj/config.toml Normal file
View File

@ -0,0 +1,9 @@
baseURL = "http://example.org/"
languageCode = "en-us"
title = "My New Hugo Site"
[markup]
defaultMarkdownHandler = "goldmark"
[markup.goldmark]
[markup.goldmark.renderer]
unsafe = true

View File

View File

@ -0,0 +1,13 @@
---
title: Battle
---
<div class="initiative">
<ul>
<li> Priat </li>
<li> Opal </li>
<li> Mirrored Beast 1 </li>
<li> Mirrored Beast 2 </li>
<li> Richtor </li>
</ul>
</div>

102
desegnoj/content/priat.html Normal file
View File

@ -0,0 +1,102 @@
---
title: Priat
---
<div class="columns c-2">
<div>
<h1 id="name">Priat (Tier 1)</h1>
<p><em>A Intuitive Jack who Explores Yesterday</em></p>
<ul>
<li>Effort: 1</li>
<li>Cypher Limit: 2</li>
</ul>
</div>
<div>
{{< stat name="Might" value="12" edge="0" >}}
{{< stat name="Speed" value="14" edge="0" >}}
{{< stat name="Intellect" value="12" edge="1" >}}
</div>
</div>
<div class="columns c-2">
<div>
<h2>Special Abilities</h2>
<ul>
<li><em>Know what to do</em>: You can act immediately, even if it's not your turn. On your next regular turn, any action you take is hindered. You can do this once, per recovery roll.</li>
<li><em>Flex skill</em>: At the beginning of each day, chose one skill other than attacks or defense. You are trained in that skill for the rest of the day.</li>
<li><em>Fleet of Foot</em> (1+ speed): You can move a short distance as part of another action. You can move a long distance as your entire action for a turn. Applying a level of Effort, you can move a long distance and make an attack os your entire action, but the attack is hindered.</li>
<li><em>Vanish</em> (2 intellect): You become invisible for a short amount of time. While invisible, you have an asset on stealth and Speed defense tasks. Invisibility ends at the end of your next turn, or if you do something ot reveal your presence or position.</li>
</ul>
</div>
<div>
<h2>Skills</h2>
<table>
{{< skill name="Perception" level="Trained" >}}
{{< skill name="Climbing" level="Trained" >}}
{{< skill name="Salvaging Numenera" level="Trained" >}}
{{< skill name="Identifying" level="Trained" >}}
{{< skill name="Crafting Numenera" level="Trained" >}}
</table>
</div>
</div>
<h2 id="attack-and-defense">Attack and Defense</h2>
<div class="columns c-2">
<div>
<table>
<thead>
<tr>
<th>Weapon</th>
<th>Damage</th>
<th>Range</th>
<th>Ammo</th>
</tr>
</thead>
<tbody>
<tr>
<td>Battleaxe</td>
<td>4</td>
<td>Melee</td>
<td></td>
</tr>
<tr>
<td>Dagger</td>
<td>2</td>
<td>Melee</td>
<td></td>
</tr>
</tbody>
</table>
<p>You can use light or medium weapons. Your attacks are hindered with a heavy weapon.</p>
</div>
<div>
<table>
<thead>
<tr>
<th>Armor</th>
<th>Defense</th>
<th>Speed cost</th>
</tr>
</thead>
<tbody>
<tr>
<td>Brigandine</td>
<td>2</td>
<td>2</td>
</tr>
</tbody>
</table>
</div>
</div>
<h2 id="equipment">Equipment</h2>
<ul>
<li>Bag of excavation tools</li>
<li>Clothing</li>
<li>Explorer&rsquo;s Pack</li>
<li>Pack of light tools</li>
<li>A metal ring, wearable on a human finger, that issues a faint ringing noise when rubbed</li>
</ul>

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="en">
{{ partial "head.html" . }}
<title> {{ .Page.Title }} </title>
<body>
{{ block "main" . }}
{{ end }}
</body>
</html>

View File

@ -0,0 +1,3 @@
{{ define "main" }}
{{ .Content }}
{{ end }}

View File

@ -0,0 +1,7 @@
<!DOCTYPE html>
<html lang="en">
{{ partial "head.html" . }}
<body>
{{ partial "main_menu.html" . }}
</body>
</html>

View File

@ -0,0 +1,6 @@
<head>
<meta charset="utf-8">
{{ $style := resources.Get "main.scss" | resources.ToCSS }}
<link href={{ $style.RelPermalink }} rel="stylesheet" type="text/css" />
</head>

View File

@ -0,0 +1,6 @@
<ul>
<li> Campaign settings </li>
<li> <a href="battle">Combat tracker</a> </li>
<li> <a href="priat">Character sheets</a> </li>
</ul>

View File

@ -0,0 +1,4 @@
<tr>
<th> {{ .Get "name" }} </td>
<td> {{ .Get "level" }} </td>
</tr>

View File

@ -0,0 +1,7 @@
<div class="statpool">
<h2> {{ .Get "name" }} </h2>
<div class="columns">
<div class="pool"> {{ .Get "value" }} / {{ .Get "value" }} </div>
<div class="edge"> {{ .Get "edge" }} </div>
</div>
</div>

View File

@ -1,25 +0,0 @@
# Tasks
- Players
- need to be able to see their character sheets
- need to be able to edit their character sheets
- need to record damage
- need to mark abilities as used
- need to see active status effects ("flex skill")
- need to trigger recovery (regain pool points, reset skills that reset on recovery)
- need to mark artifacts as depleted
- need to expend cyphers
- GM
- needs to see all character sheet
- needs to send items to players
- needs a pool of potential cyphers
- Other
- all actions must be undoable
Cypher:
- name
- level
- form
- description
Character Sheet

1
kliento/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
build

7188
kliento/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

34
kliento/package.json Normal file
View File

@ -0,0 +1,34 @@
{
"name": "kampanja-kontrolada-kliento",
"version": "1.0.0",
"description": "",
"main": "src/main.tsx",
"scripts": {
"start": "snowpack dev",
"build": "snowpack build",
"test": "jest"
},
"author": "Savanni D'Gerinel <savanni@luminescent-dreams.com>",
"license": "AGPL-3.0-or-later",
"dependencies": {
"history": "^5.2.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.2.1",
"styled-components": "^5.3.3"
},
"devDependencies": {
"@snowpack/plugin-react-refresh": "^2.5.0",
"@snowpack/plugin-sass": "^1.4.0",
"@snowpack/plugin-typescript": "^1.2.1",
"@types/react": "^17.0.35",
"@types/react-dom": "^17.0.11",
"@types/styled-components": "^5.1.19",
"eslint": "^8.2.0",
"jest": "^27.4.5",
"plugin-typescript": "^8.0.0",
"snowpack": "^3.8.8",
"ts-jest": "^27.0.7",
"typescript": "^4.5.2"
}
}

15
kliento/public/index.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<style type="text/css">
body {
background-color: rgb(200, 190, 180);
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="/dist/index.js"></script>
</body>
</html>

View File

@ -0,0 +1,25 @@
// Snowpack Configuration File
// See all supported options: https://www.snowpack.dev/reference/configuration
/** @type {import("snowpack").SnowpackUserConfig } */
module.exports = {
mount: {
public: { url: '/', static: true },
src: { url: '/dist' },
},
routes: [
{ match: 'routes', src: '/.*', dest: '/' },
],
plugins: [
'@snowpack/plugin-typescript',
],
packageOptions: {
types: true,
},
devOptions: {
/* ... */
},
buildOptions: {
sourceMap: true,
},
};

40
kliento/src/AppPage.tsx Normal file
View File

@ -0,0 +1,40 @@
import React, { createContext, useState } from "react"
import ReactDOM from "react-dom"
import { BrowserRouter, Route, Routes, useParams } from "react-router-dom"
import styled from "styled-components"
import AppProvider from "./appContext"
import Menu from "./components/Menu"
import PlayerListView from "./views/PlayerListView"
const Columns = styled.div`
display: flex;
padding: 1em;
`
const Column = styled.div`
width: 50%;
`
const render = () => (<AppProvider>
<h1>Numenera Datasphere</h1>
<Columns>
<Column>
<Menu />
</Column>
<Column>
<Routes>
<Route path="/" element={<div>root</div>} />
<Route path="campaign" element={<div>campaign</div>} />
<Route path="players" element={<PlayerListView />}>
<Route path=":name" element={<PlayerListView />} />
</Route>
<Route path="cyphers" element={<div>cyphers</div>} />
<Route path="battles" element={<div>battles</div>} />
</Routes>
</Column>
</Columns>
</AppProvider>
)
export default render

View File

@ -0,0 +1,28 @@
import React, { ReactNode } from "react";
import { PlayerCharacter } from "./types"
export type AppState = {
playerCharacters: { [name: string]: PlayerCharacter };
}
export const AppContext = React.createContext<{ state: AppState }>({state: {
playerCharacters: {}
}})
const AppProvider = ({ children }: { children: ReactNode }) => {
const [state, setState] = React.useState<AppState>({playerCharacters: {
"priat": {
name: "Priat",
concept: "An Intuitive Jack who Explores Yesterday",
effort: 1,
cypherLimit: 2,
might: { value: 12, max: 12, edge: 0 },
speed: { value: 14, max: 14, edge: 0 },
intellect: { value: 12, max: 12, edge: 1 },
}}})
return (<AppContext.Provider value={{state}}>{children}</AppContext.Provider>)
}
export default AppProvider

View File

@ -0,0 +1,14 @@
import React from "react"
import { Link } from "react-router-dom"
const MainMenu = () => (<nav>
<ul className="menu">
<li> <Link to="/campaign">Campaign Settings</Link> </li>
<li> <Link to="/players">Player Characters</Link> </li>
<li> <Link to="/cyphers">Cyphers and Artifacts</Link> </li>
<li> <Link to="/battles">Battles</Link> </li>
</ul>
</nav>)
export default MainMenu

View File

@ -0,0 +1,3 @@
.menu {
list-style-type: none;
}

View File

@ -0,0 +1,20 @@
import React from "react"
import ReactDOM from "react-dom"
import styled from "styled-components"
interface StatPoolProps {
name: string;
value: number;
max: number;
edge: number;
}
const StatPool = styled.div`
border: 1px solid black;
`
const render = ({ name, value, max, edge }: StatPoolProps) => (<StatPool>
<div>{value} / {max}</div> <div>{edge}</div>
</StatPool>)
export default render

17
kliento/src/index.tsx Normal file
View File

@ -0,0 +1,17 @@
import React, { createContext, useState } from "react"
import ReactDOM from "react-dom"
import { BrowserRouter, Route, Routes, useParams } from "react-router-dom"
import styled from "styled-components"
import AppPage from "./AppPage"
const main = () => {
ReactDOM.render(
<BrowserRouter>
<AppPage />
</BrowserRouter>
, document.getElementById('root')
)
}
main()

28
kliento/src/styles.scss Normal file
View File

@ -0,0 +1,28 @@
body {
background-color: rgb(200, 190, 180);
}
.columns {
display: flex;
// border: 1px solid red;
margin: auto;
padding: 1em;
> * {
margin: 0 1em 0 1em;
}
& > :first-child {
margin: 0 1em 0 0;
}
& > :last-child {
margin: 0 0 0 1em;
}
}
.c-2 {
> div {
width: 50%;
}
}

15
kliento/src/types.ts Normal file
View File

@ -0,0 +1,15 @@
type StatPool = {
value: number;
max: number;
edge: number;
}
export type PlayerCharacter = {
name: string;
concept: string;
effort: number;
cypherLimit: number;
might: StatPool;
speed: StatPool;
intellect: StatPool;
}

View File

@ -0,0 +1,26 @@
import React from "react"
import ReactDOM from "react-dom"
import StatPool from "../components/StatPool"
import { PlayerCharacter } from "../types"
interface PlayerCharacterProps extends PlayerCharacter { }
const PlayerCharacter = ({ name, concept, might, speed, intellect }: PlayerCharacterProps) => {
return (<div>
<h2>{name}</h2>
<div className="columns c-2">
<div>
<div>{concept}</div>
</div>
<div>
<StatPool name="Might" {...might} />
<StatPool name="Speed" {...speed} />
<StatPool name="Intellect" {...intellect} />
</div>
</div>
</div>)
}
export default PlayerCharacter

View File

@ -0,0 +1,24 @@
import React, { useContext } from "react"
import ReactDOM from "react-dom"
import { Link, Route, Routes, useParams } from "react-router-dom"
import { AppContext } from "../appContext"
import PlayerCharacter from "./PlayerCharacter"
const PlayerListView = () => {
const params = useParams()
const { state } = useContext(AppContext)
const character = params.name ? state.playerCharacters[params.name] : null
return (<div>
<nav>
<Link to="priat">Priat</Link> |
<Link to="dorian">Dorian </Link> |
<Link to="ember">Ember</Link> |
<Link to="lise">Lise</Link>
</nav>
{character ? <PlayerCharacter {...character} /> : null }
</div>);
}
export default PlayerListView

12
kliento/tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"target": "es2016",
"jsx": "react",
"module": "commonjs",
"rootDir": "src",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noEmit": true,
}
}

View File

@ -1,11 +0,0 @@
[package]
name = "datasphere-server"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
orizentic = { path = "../../orizentic/" }
tokio = { version = "1", features = ["full"] }
warp = { version = "0.3.1" }

View File

@ -1,71 +0,0 @@
use orizentic::{OrizenticCtx, Secret, Username};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::sync::{Arc, RwLock};
use warp::{reject, Filter, Rejection};
fn with_auth(
auth_ctx: Arc<RwLock<OrizenticCtx>>,
) -> impl Filter<Extract = (Username,), Error = Rejection> + Clone {
let auth_ctx = auth_ctx.clone();
warp::header("authentication").and_then({
let auth_ctx = auth_ctx.clone();
move |text| {
let auth_ctx = auth_ctx.clone();
async move {
match auth_ctx.read().unwrap().decode_and_validate_text(text) {
Ok(token) => Ok(token.claims.audience),
Err(_) => Err(reject()),
}
}
}
})
}
/*
fn gm_routes(
auth_ctx: Arc<RwLock<OrizenticCtx>>,
) -> impl Filter<Extract = (String,), Error = Rejection> + Clone {
let base_route = with_auth(auth_ctx).and(warp::path("gm"));
let character_sheet = {
let auth_ctx = auth_ctx.clone();
with_auth(auth_ctx)
.and(warp::path!("gm" / "character" / String))
.map(|auth: Username, name| format!("name: {} {}", String::from(auth), name))
};
unimplemented!()
}
*/
#[tokio::main]
pub async fn main() {
let auth_ctx = Arc::new(RwLock::new(OrizenticCtx::new(
Secret(Vec::from("abcdefg".as_bytes())),
vec![],
)));
let send_item = {
let auth_ctx = auth_ctx.clone();
with_auth(auth_ctx)
.and(warp::path!("gm" / "send_item"))
.map(|auth: Username| format!("send_item: {}", String::from(auth)))
};
let send_resource = warp::header("authentication")
.and(warp::path!("gm" / "send_resource"))
.map(|auth: String| format!("send_resource"));
let pc_sheet = warp::header("authentication")
.and(warp::path!("player" / "character" / String))
.map(|authentication: String, name: String| format!("name: {}", name));
let filter = pc_sheet.or(send_item).or(send_resource).or(pc_sheet);
let server = warp::serve(filter);
server
.run(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
8000,
))
.await;
}

1016
servilo/Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

19
servilo/Cargo.toml Normal file
View File

@ -0,0 +1,19 @@
[package]
name = "kampanja-kontrolada-servilo"
version = "0.1.0"
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { version = "1" }
rand = { version = "0.8" }
rusqlite = { version = "0.26" }
sha2 = { version = "0.10" }
thiserror = { version = "1" }
tokio = { version = "1", features = ["full"] }
uuid = { version = "0.8", features = ["v4"] }
warp = { version = "0.3" }
[dev-dependencies]
tempfile = { version = "3" }

461
servilo/src/aŭtentigo.rs Normal file
View File

@ -0,0 +1,461 @@
use crate::datumbazo::Datumbazo;
use rand::{distributions::Alphanumeric, thread_rng, Rng};
use rusqlite::{
params,
types::{FromSql, FromSqlError, FromSqlResult, ToSqlOutput, ValueRef},
OptionalExtension, ToSql,
};
use sha2::{Digest, Sha256};
use std::{
collections::HashMap,
convert::Infallible,
str::FromStr,
sync::{Arc, RwLock},
};
use uuid::{adapter::Hyphenated, Uuid};
use warp::{reject, reject::Reject, Filter, Rejection};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct SesiaĴetono(String);
impl From<&str> for SesiaĴetono {
fn from(s: &str) -> Self {
SesiaĴetono(s.to_owned())
}
}
impl FromStr for SesiaĴetono {
type Err = Infallible;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(SesiaĴetono(s.to_owned()))
}
}
impl From<SesiaĴetono> for String {
fn from(s: SesiaĴetono) -> Self {
s.0.clone()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct UzantIdentigilo(String);
impl From<&str> for UzantIdentigilo {
fn from(s: &str) -> Self {
UzantIdentigilo(s.to_owned())
}
}
impl From<UzantIdentigilo> for String {
fn from(s: UzantIdentigilo) -> Self {
s.0.clone()
}
}
impl FromSql for UzantIdentigilo {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Text(t) => Ok(UzantIdentigilo::from(
String::from_utf8(Vec::from(t)).unwrap().as_ref(),
)),
_ => Err(FromSqlError::InvalidType),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Uzantnomo(String);
impl From<&str> for Uzantnomo {
fn from(s: &str) -> Self {
Uzantnomo(s.to_owned())
}
}
impl From<&Uzantnomo> for String {
fn from(s: &Uzantnomo) -> Self {
s.0.clone()
}
}
impl From<Uzantnomo> for String {
fn from(s: Uzantnomo) -> Self {
s.0.clone()
}
}
impl FromSql for Uzantnomo {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Text(t) => Ok(Uzantnomo::from(
String::from_utf8(Vec::from(t)).unwrap().as_ref(),
)),
_ => Err(FromSqlError::InvalidType),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Pasvorto(String);
impl Pasvorto {
fn haku(&self, salo: &Salo) -> HakitaPasvorto {
let mut haketilo = Sha256::new();
haketilo.update(String::from(self).as_bytes());
haketilo.update(salo);
HakitaPasvorto(Vec::from(haketilo.finalize().as_slice()))
}
}
impl From<&str> for Pasvorto {
fn from(s: &str) -> Self {
Pasvorto(s.to_owned())
}
}
impl From<&Pasvorto> for String {
fn from(s: &Pasvorto) -> Self {
s.0.clone()
}
}
impl From<Pasvorto> for String {
fn from(s: Pasvorto) -> Self {
s.0.clone()
}
}
#[derive(PartialEq)]
struct HakitaPasvorto(Vec<u8>);
impl ToSql for HakitaPasvorto {
fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
Ok(ToSqlOutput::Borrowed(ValueRef::Blob(self.0.as_ref())))
}
}
impl FromSql for HakitaPasvorto {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Blob(t) => Ok(HakitaPasvorto(Vec::from(t))),
_ => Err(FromSqlError::InvalidType),
}
}
}
struct Salo(Vec<u8>);
impl AsRef<[u8]> for Salo {
fn as_ref(&self) -> &[u8] {
self.0.as_ref()
}
}
impl ToSql for Salo {
fn to_sql(&self) -> Result<ToSqlOutput<'_>, rusqlite::Error> {
Ok(ToSqlOutput::Borrowed(ValueRef::Blob(self.0.as_ref())))
}
}
impl FromSql for Salo {
fn column_result(val: ValueRef<'_>) -> FromSqlResult<Self> {
match val {
ValueRef::Blob(t) => Ok(Salo(Vec::from(t))),
_ => Err(FromSqlError::InvalidType),
}
}
}
pub trait AŭtentigoDB: Send {
fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo;
fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono;
fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto);
fn aŭtentigu_per_pasvorto(
&self,
uzantnomo: &Uzantnomo,
pasvorto: &Pasvorto,
) -> Option<UzantIdentigilo>;
fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)>;
fn uzanto_ekzistas(&self, uzantnomo: Uzantnomo) -> bool;
}
pub struct MemAŭtentigo {
inversa_uzantoj: HashMap<Uzantnomo, UzantIdentigilo>,
pasvortoj: HashMap<UzantIdentigilo, Pasvorto>,
sesioj: HashMap<SesiaĴetono, UzantIdentigilo>,
uzantoj: HashMap<UzantIdentigilo, Uzantnomo>,
}
impl MemAŭtentigo {
pub fn new() -> Self {
Self {
inversa_uzantoj: HashMap::new(),
pasvortoj: HashMap::new(),
sesioj: HashMap::new(),
uzantoj: HashMap::new(),
}
}
}
impl AŭtentigoDB for MemAŭtentigo {
fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo {
let uzant_id =
UzantIdentigilo::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
self.uzantoj.insert(uzant_id.clone(), uzantnomo.clone());
self.pasvortoj.insert(uzant_id.clone(), pasvorto);
self.inversa_uzantoj.insert(uzantnomo, uzant_id.clone());
uzant_id
}
fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono {
let identigilo = self.inversa_uzantoj.get(&uzantnomo).cloned().unwrap();
let ĵetono =
SesiaĴetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
self.sesioj.insert(ĵetono.clone(), identigilo);
ĵetono
}
fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto) {
self.pasvortoj.insert(identigilo.clone(), pasvorto);
}
fn aŭtentigu_per_pasvorto(
&self,
uzantnomo: &Uzantnomo,
pasvorto: &Pasvorto,
) -> Option<UzantIdentigilo> {
self.inversa_uzantoj.get(&uzantnomo).and_then(|id| {
self.pasvortoj.get(id).and_then(|kandidato| {
if *pasvorto == *kandidato {
Some(id.clone())
} else {
None
}
})
})
}
fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)> {
let identigilo = self.sesioj.get(&ĵetono).cloned()?;
let uzantnomo = self.uzantoj.get(&identigilo).cloned()?;
Some((identigilo, uzantnomo))
}
fn uzanto_ekzistas(&self, uzantnomo: Uzantnomo) -> bool {
self.inversa_uzantoj.get(&uzantnomo).is_some()
}
}
pub struct DBAŭtentigo {
pool: Datumbazo,
}
impl DBAŭtentigo {
pub fn kreu(pool: Datumbazo) -> Self {
DBAŭtentigo { pool }
}
}
impl AŭtentigoDB for DBAŭtentigo {
fn kreu_uzanton(&mut self, uzantnomo: Uzantnomo, pasvorto: Pasvorto) -> UzantIdentigilo {
let mut konekto = self.pool.konektu().unwrap();
let tr = konekto.transaction().unwrap();
let identigilo =
UzantIdentigilo::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
let cnt: usize = tr
.query_row(
"SELECT count(*) from uzantoj WHERE nomo = ?",
params![String::from(uzantnomo.clone())],
|row| row.get("count(*)"),
)
.unwrap();
if cnt > 0 {
panic!("uzanto jam ekzistas");
} else {
let salo = Salo(
thread_rng()
.sample_iter(&Alphanumeric)
.take(10)
.collect::<Vec<u8>>(),
);
let pasvorto = pasvorto.haku(&salo);
tr.execute(
"INSERT INTO uzantoj VALUES(?, ?, ?, ?)",
params![
String::from(identigilo.clone()),
String::from(uzantnomo),
pasvorto,
salo
],
)
.unwrap();
}
tr.commit().unwrap();
identigilo
}
fn kreu_sesion(&mut self, uzantnomo: &Uzantnomo) -> SesiaĴetono {
let ĵetono =
SesiaĴetono::from(format!("{}", Hyphenated::from_uuid(Uuid::new_v4())).as_str());
let mut konekto = self.pool.konektu().unwrap();
let tr = konekto.transaction().unwrap();
let uzanta_id: Option<String> = tr
.query_row(
"SELECT id FROM uzantoj WHERE nomo = ?",
[String::from(uzantnomo.clone())],
|row| row.get("id"),
)
.optional()
.unwrap();
match uzanta_id {
None => panic!("uzanto ne ekzistas"),
Some(id) => {
tr.execute(
"INSERT INTO sesioj VALUES(?, ?)",
[String::from(ĵetono.clone()), id],
)
.unwrap();
}
}
tr.commit().unwrap();
ĵetono
}
fn ŝanĝu_pasvorton(&mut self, identigilo: &UzantIdentigilo, pasvorto: Pasvorto) {}
fn aŭtentigu_per_pasvorto(
&self,
uzantnomo: &Uzantnomo,
pasvorto: &Pasvorto,
) -> Option<UzantIdentigilo> {
let konekto = self.pool.konektu().unwrap();
let mut demando = konekto
.prepare_cached("SELECT * FROM uzantoj WHERE nomo = ?")
.unwrap();
let rezultoj = demando.query_map(params![String::from(uzantnomo)], |row| {
let id = row.get("id")?;
let nomo = row.get("nomo")?;
let pasvorto = row.get("pasvorto")?;
let salo = row.get("pasvortsalo")?;
Ok((id, nomo, pasvorto, salo))
});
let rows: Vec<(UzantIdentigilo, Uzantnomo, HakitaPasvorto, Salo)> = match rezultoj {
Ok(r) => {
let mut rows = Vec::new();
for row in r {
let (id, nomo, pasvorto, salo) = row.unwrap();
rows.push((id, nomo, pasvorto, salo));
}
rows
}
Err(_) => panic!("eraro en datumbazo"),
};
match rows.len() {
1 => {
let (ref identigilo, _, ref hakita_pasvorto, ref salo) = rows[0];
if pasvorto.haku(&salo) == *hakita_pasvorto {
Some(identigilo.clone())
} else {
None
}
}
0 => None,
_ => panic!("pli ol unu kongruo trovis por uzantnomo"),
}
}
fn aŭtentigu_per_sesio(&self, ĵetono: &SesiaĴetono) -> Option<(UzantIdentigilo, Uzantnomo)> {
let konekto = self.pool.konektu().unwrap();
konekto.query_row(
"SELECT uzantoj.id, uzantoj.nomo FROM sesioj INNER JOIN uzantoj on sesioj.uzanto == uzantoj.id WHERE sesioj.id = ?",
params![String::from(ĵetono.clone())],
|row| {
let identigilo = row.get("id")
.map(|s: String| UzantIdentigilo::from(s.as_str())).unwrap();
let nomo = row.get("nomo")
.map(|s: String| Uzantnomo::from(s.as_str())).unwrap();
Ok((identigilo, nomo))
},
)
.optional()
.unwrap()
}
fn uzanto_ekzistas(&self, uzantnomo: Uzantnomo) -> bool {
let konekto = self.pool.konektu().unwrap();
let cnt: usize = konekto
.query_row(
"SELECT count(*) from uzantoj WHERE nomo = ?",
params![String::from(uzantnomo.clone())],
|row| row.get("count(*)"),
)
.unwrap();
cnt > 0
}
}
#[derive(Debug)]
pub struct AŭtentigoPostulas;
impl Reject for AŭtentigoPostulas {}
pub fn kun_aŭtentigo(
auth_ctx: Arc<RwLock<impl AŭtentigoDB + Sync>>,
) -> impl Filter<Extract = ((UzantIdentigilo, Uzantnomo),), Error = Rejection> + Clone {
let auth_ctx = auth_ctx.clone();
warp::header("authentication").and_then({
let auth_ctx = auth_ctx.clone();
move |text: SesiaĴetono| {
println!("teksto: {:?}", text);
let auth_ctx = auth_ctx.clone();
async move {
match auth_ctx.read().unwrap().aŭtentigu_per_sesio(&text) {
Some(salutiloj) => Ok(salutiloj),
None => Err(reject::custom(AŭtentigoPostulas)),
}
}
}
})
}
#[cfg(test)]
mod test {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn ĝi_povas_krei_uzanto() {
let vojo = NamedTempFile::new().unwrap().into_temp_path();
let datumbazo = Datumbazo::kreu(vojo.to_path_buf()).unwrap();
let mut aŭtentigo = DBAŭtentigo::kreu(datumbazo);
let identigilo =
aŭtentigo.kreu_uzanton(Uzantnomo::from("savanni"), Pasvorto::from("abcdefg"));
let aŭtentita_identigilo = aŭtentigo
.aŭtentigu_per_pasvorto(&Uzantnomo::from("savanni"), &Pasvorto::from("abcdefg"));
assert_eq!(aŭtentita_identigilo, Some(identigilo));
}
#[test]
fn ĝi_scias_kiel_uzanto_ekzistas() {
let vojo = NamedTempFile::new().unwrap().into_temp_path();
let datumbazo = Datumbazo::kreu(vojo.to_path_buf()).unwrap();
let mut aŭtentigo = DBAŭtentigo::kreu(datumbazo);
assert!(!(aŭtentigo.uzanto_ekzistas(Uzantnomo::from("savanni"))));
let _ = aŭtentigo.kreu_uzanton(Uzantnomo::from("savanni"), Pasvorto::from("abcdefg"));
assert!(aŭtentigo.uzanto_ekzistas(Uzantnomo::from("savanni")));
}
}

116
servilo/src/datumbazo.rs Normal file
View File

@ -0,0 +1,116 @@
use rusqlite::{params, Connection};
use std::{
ops::{Deref, DerefMut},
path::PathBuf,
sync::{Arc, Mutex},
};
pub struct ManagedConnection<'a> {
pool: &'a Datumbazo,
konekto: Option<Connection>,
}
#[derive(Clone)]
pub struct Datumbazo {
dosierindiko: PathBuf,
pool: Arc<Mutex<Vec<Connection>>>,
}
impl Datumbazo {
pub fn kreu(dosierindiko: PathBuf) -> Result<Datumbazo, anyhow::Error> {
let mut konekto = Connection::open(dosierindiko.clone())?;
let tx = konekto.transaction()?;
let versio: i32 = tx.pragma_query_value(None, "user_version", |r| r.get(0))?;
println!("versio: {}", versio);
if versio == 0 {
tx.execute_batch(
"CREATE TABLE uzantoj (id string primary key, nomo text, pasvorto text, pasvortsalo text);
CREATE TABLE rajtoj (uzanto string, rajto string, foreign key(uzanto) references uzanto(id));
CREATE TABLE sesioj (id string primary key not null, uzanto string, foreign key(uzanto) references uzantoj(id));
PRAGMA user_version = 1;",
)?;
}
let versio: i32 = tx.pragma_query_value(None, "user_version", |r| r.get(0))?;
println!("versio: {}", versio);
tx.commit()?;
Ok(Datumbazo {
dosierindiko,
pool: Arc::new(Mutex::new(vec![konekto])),
})
}
pub fn konektu<'a>(&'a self) -> Result<ManagedConnection<'a>, anyhow::Error> {
let mut pool = self.pool.lock().unwrap();
match pool.pop() {
Some(konekto) => Ok(ManagedConnection {
pool: &self,
konekto: Some(konekto),
}),
None => {
let konekto = Connection::open(self.dosierindiko.clone())?;
Ok(ManagedConnection {
pool: &self,
konekto: Some(konekto),
})
}
}
}
pub fn revenu(&self, konekto: Connection) {
let mut pool = self.pool.lock().unwrap();
pool.push(konekto);
}
}
impl Deref for ManagedConnection<'_> {
type Target = Connection;
fn deref(&self) -> &Connection {
self.konekto.as_ref().unwrap()
}
}
impl DerefMut for ManagedConnection<'_> {
fn deref_mut(&mut self) -> &mut Connection {
self.konekto.as_mut().unwrap()
}
}
impl Drop for ManagedConnection<'_> {
fn drop(&mut self) {
self.pool.revenu(self.konekto.take().unwrap());
}
}
#[cfg(test)]
mod test {
use super::*;
use tempfile::NamedTempFile;
#[test]
fn povas_krei_uzanton() {
let vojo = NamedTempFile::new().unwrap().into_temp_path();
let datumbazo = Datumbazo::kreu(vojo.to_path_buf()).unwrap();
let mut konekto = datumbazo.konektu().unwrap();
let tr = konekto.transaction().unwrap();
tr.execute(
"INSERT INTO uzantoj VALUES(?, ?, ?, ?)",
params!["abcdfeg", "mia-uzantnomo", "pasvorto", "abcdefg"],
)
.unwrap();
tr.commit().unwrap();
let konekto = datumbazo.konektu().unwrap();
let id: Option<String> = konekto
.query_row(
"SELECT id FROM uzantoj WHERE nomo = ?",
[String::from("mia-uzantnomo")],
|row| row.get("id"),
)
.unwrap();
assert_eq!(id, Some(String::from("abcdfeg")));
}
}

102
servilo/src/main.rs Normal file
View File

@ -0,0 +1,102 @@
use std::convert::Infallible;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use std::sync::{Arc, RwLock};
use warp::{Filter, Rejection};
#[path = "aŭtentigo.rs"]
mod aŭtentigo;
use aŭtentigo::{kun_aŭtentigo, AŭtentigoDB, AŭtentigoPostulas, Pasvorto, Uzantnomo};
mod datumbazo;
use datumbazo::Datumbazo;
mod rajtigo;
use rajtigo::{DBRajtigo, Rajtigo, Rajto};
use crate::aŭtentigo::{DBAŭtentigo, UzantIdentigilo};
async fn traktilo_de_listigu_ludantojn(
identigilo: UzantIdentigilo,
rajtigo: Arc<RwLock<impl Rajtigo>>,
) -> Result<impl warp::Reply, Rejection> {
if rajtigo.read().unwrap().havas_rajton(identigilo, |rajtoj| {
rajtoj.fold(false, |acc, r| acc || r == Rajto::from("admin"))
}) {
Ok(warp::reply::json(&vec!["Alice", "Betty", "Charles"]))
} else {
Err(warp::reject::not_found())
}
}
async fn traktilu_erarojn(err: Rejection) -> Result<impl warp::Reply, Infallible> {
let kodo;
let mesaĝo;
if let Some(_) = err.find::<AŭtentigoPostulas>() {
kodo = warp::http::StatusCode::UNAUTHORIZED;
mesaĝo = "Ensalutu";
} else {
kodo = warp::http::StatusCode::INTERNAL_SERVER_ERROR;
mesaĝo = "Netraktita mesaĝo";
}
Ok(warp::reply::with_status(
warp::reply::json(&mesaĝo.to_owned()),
kodo,
))
}
#[tokio::main]
pub async fn main() {
let db = Datumbazo::kreu(PathBuf::from("../servilo.db")).unwrap();
let auth_ctx = Arc::new(RwLock::new(DBAŭtentigo::kreu(db.clone())));
let rajtigo = Arc::new(RwLock::new(DBRajtigo::kreu(db)));
{
let mut auth_ctx = auth_ctx.write().unwrap();
let mut rajtigo = rajtigo.write().unwrap();
if !auth_ctx.uzanto_ekzistas(Uzantnomo::from("savanni")) {
let uzant_identigilo =
auth_ctx.kreu_uzanton(Uzantnomo::from("savanni"), Pasvorto::from("abcdefg"));
rajtigo.aldonu_rajtojn(
uzant_identigilo,
&mut [Rajto::from("admin")].iter().cloned(),
);
}
}
let ĵetono = auth_ctx
.write()
.unwrap()
.kreu_sesion(&Uzantnomo::from("savanni"));
println!("ĵetono: {}", String::from(ĵetono));
let listigu_ludantojn = {
let auth_ctx = auth_ctx.clone();
kun_aŭtentigo(auth_ctx)
.and(warp::path!("api" / "ludantoj"))
.and_then({
let rajtigo = rajtigo.clone();
move |(identigilo, uzantnomo): (UzantIdentigilo, Uzantnomo)| {
println!(
"[{}] {}",
String::from(identigilo.clone()),
String::from(uzantnomo.clone())
);
let rajtigo = rajtigo.clone();
traktilo_de_listigu_ludantojn(identigilo, rajtigo)
}
})
};
let filter = listigu_ludantojn.recover(traktilu_erarojn);
let server = warp::serve(filter);
server
.run(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
8001,
))
.await;
}

148
servilo/src/rajtigo.rs Normal file
View File

@ -0,0 +1,148 @@
use crate::{aŭtentigo::UzantIdentigilo, datumbazo::Datumbazo};
use rusqlite::{params, OptionalExtension};
use std::collections::{HashMap, HashSet};
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Rajto(String);
impl From<&str> for Rajto {
fn from(s: &str) -> Self {
Rajto(s.to_owned())
}
}
impl From<Rajto> for String {
fn from(s: Rajto) -> Self {
s.0.clone()
}
}
pub trait Rajtigo {
fn havas_rajton<'a, F>(&'a self, identigilo: UzantIdentigilo, provo: F) -> bool
where
F: FnOnce(&mut (dyn Iterator<Item = Rajto>)) -> bool;
fn rajtoj(&self, identigilo: &UzantIdentigilo) -> Vec<Rajto>;
fn aldonu_rajtojn(
&mut self,
identigilo: UzantIdentigilo,
rajtoj: &mut (dyn Iterator<Item = Rajto>),
);
fn foriru_rajtoj(
&mut self,
identigilo: UzantIdentigilo,
rajtoj: &mut (dyn Iterator<Item = Rajto>),
);
fn foriru_uzanto(&mut self, identigilo: UzantIdentigilo);
}
/*
pub struct MemRajtigo {
rajtoj: HashMap<UzantIdentigilo, HashSet<Rajto>>,
}
impl MemRajtigo {
pub fn new() -> Self {
MemRajtigo {
rajtoj: HashMap::new(),
}
}
}
impl Rajtigo for MemRajtigo {
fn havas_rajton<'a, F>(&'a self, identigilo: UzantIdentigilo, provo: F) -> bool
where
F: FnOnce(&mut (dyn Iterator<Item = Rajto> + 'a)) -> bool,
{
match self.rajtoj.get(&identigilo) {
Some(rajtoj) => provo(&mut rajtoj.clone().into_iter()),
None => false,
}
}
fn rajtoj(&self, identigilo: &UzantIdentigilo) -> Vec<Rajto> {
self.rajtoj
.get(&identigilo)
.map(|r| r.into_iter().cloned().collect::<Vec<Rajto>>())
.unwrap_or(vec![])
}
fn aldonu_rajtoj(
&mut self,
identigilo: UzantIdentigilo,
rajtoj: &mut (dyn Iterator<Item = Rajto>),
) {
let valoro = self.rajtoj.entry(identigilo).or_insert(HashSet::new());
while let Some(r) = rajtoj.next() {
valoro.insert(r);
}
}
fn foriru_rajtoj(
&mut self,
identigilo: UzantIdentigilo,
rajtoj: &mut (dyn Iterator<Item = Rajto>),
) {
let valoro = self.rajtoj.entry(identigilo).or_insert(HashSet::new());
while let Some(r) = rajtoj.next() {
valoro.remove(&r);
}
}
fn foriru_uzanto(&mut self, identigilo: UzantIdentigilo) {
let _ = self.rajtoj.remove(&identigilo);
}
}
*/
pub struct DBRajtigo {
pool: Datumbazo,
}
impl DBRajtigo {
pub fn kreu(pool: Datumbazo) -> Self {
DBRajtigo { pool }
}
}
impl Rajtigo for DBRajtigo {
fn havas_rajton<'a, F>(&'a self, identigilo: UzantIdentigilo, provo: F) -> bool
where
F: FnOnce(&mut (dyn Iterator<Item = Rajto>)) -> bool,
{
let konekto = self.pool.konektu().unwrap();
let mut stmt = konekto
.prepare("SELECT rajto FROM rajtoj WHERE uzanto = ?")
.unwrap();
let mut rajtoj = stmt
.query_map([String::from(identigilo)], |row| {
row.get("rajto").map(|s: String| Rajto::from(s.as_ref()))
})
.unwrap()
.map(|r| r.unwrap());
provo(&mut rajtoj)
}
fn rajtoj(&self, identigilo: &UzantIdentigilo) -> Vec<Rajto> {
vec![]
}
fn aldonu_rajtojn(
&mut self,
identigilo: UzantIdentigilo,
rajtoj: &mut (dyn Iterator<Item = Rajto>),
) {
let konekto = self.pool.konektu().unwrap();
let mut stmt = konekto.prepare("INSERT INTO rajtoj VALUES(?, ?)").unwrap();
rajtoj.for_each(|r| {
stmt.execute(params![String::from(identigilo.clone()), String::from(r)])
.unwrap();
});
}
fn foriru_rajtoj(
&mut self,
identigilo: UzantIdentigilo,
rajtoj: &mut (dyn Iterator<Item = Rajto>),
) {
}
fn foriru_uzanto(&mut self, identigilo: UzantIdentigilo) {}
}

View File

@ -7,15 +7,16 @@ let
}; };
in pkgs.mkShell { in pkgs.mkShell {
name = "datasphere"; name = "kampanja-kontrolado";
nativeBuildInputs = [ nativeBuildInputs = [
pkgs.gnome.webkitgtk
pkgs.glib pkgs.glib
pkgs.gtk3 pkgs.gtk3
pkgs.libpng pkgs.libpng
pkgs.nodejs
pkgs.openssl pkgs.openssl
pkgs.pkg-config pkgs.pkg-config
pkgs.sqlite
pkgs.wrapGAppsHook pkgs.wrapGAppsHook
rust rust
unstable.rust-analyzer unstable.rust-analyzer