refactor!: replace CLI with workspace scaffold for WASM provider service
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
/target
|
||||
|
||||
*.pending-snap
|
||||
components/
|
||||
|
||||
Generated
+2
-2770
File diff suppressed because it is too large
Load Diff
+6
-24
@@ -1,26 +1,8 @@
|
||||
[package]
|
||||
name = "whoareyou"
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/server", "crates/providers/hitta"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
authors = ["Anders Olsson <anders.e.olsson@gmail.com>"]
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
bincode = "1.1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
chrono-tz = "0.5"
|
||||
directories = "2.0"
|
||||
fern = { version = "0.5", features = ["colored"] }
|
||||
htmlescape = "0.3"
|
||||
lazy_static = "1.4"
|
||||
log = "0.4"
|
||||
regex = "1.3"
|
||||
reqwest = "0.9"
|
||||
scraper = "0.10"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
structopt = "0.3"
|
||||
tinytemplate = "1.0"
|
||||
toml = "0.5"
|
||||
|
||||
[dev-dependencies]
|
||||
insta = "0.11"
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
use std::env;
|
||||
use std::fs::read_dir;
|
||||
use std::fs::DirEntry;
|
||||
use std::fs::File;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
let out_dir = env::var("OUT_DIR").unwrap();
|
||||
let destination = Path::new(&out_dir).join("tests.rs");
|
||||
let mut test_file = File::create(&destination).unwrap();
|
||||
|
||||
// write_header(&mut test_file);
|
||||
|
||||
// let test_data_directories = read_dir("./tests/data/").unwrap();
|
||||
|
||||
/*
|
||||
for directory in test_data_directories {
|
||||
write_test(&mut test_file, &directory.unwrap());
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
fn write_header(test_file: &mut File) {
|
||||
write!(
|
||||
test_file,
|
||||
r#"
|
||||
use insta::assert_yaml_snapshot_matches;
|
||||
use whoareyou::*;
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn write_test(test_file: &mut File, directory: &DirEntry) {
|
||||
let directory = directory.path().canonicalize().unwrap();
|
||||
let path = directory.display();
|
||||
let test_name = format!("prefix_if_needed_{}", directory.file_name().unwrap().to_string_lossy());
|
||||
|
||||
write!(
|
||||
test_file,
|
||||
include_str!("./tests/test_template"),
|
||||
name = test_name,
|
||||
path = path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "whoareyou-provider-hitta"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "rlib"]
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -0,0 +1 @@
|
||||
// modules added as they are implemented
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "whoareyou-server"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
authors.workspace = true
|
||||
|
||||
[dependencies]
|
||||
|
||||
[dev-dependencies]
|
||||
@@ -0,0 +1 @@
|
||||
// modules added as they are implemented
|
||||
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
@@ -1,11 +0,0 @@
|
||||
name = "eniro.se"
|
||||
path = "https://gulasidorna.eniro.se/hitta:{ number }"
|
||||
|
||||
[[messages]]
|
||||
selector = ".CompanyResultListItem h3.name > a"
|
||||
|
||||
[[history]]
|
||||
selector = "div.PhoneNoHit div.search-info-container p"
|
||||
|
||||
[[history]]
|
||||
selector = "div.feedback-types div.feedback-type-item"
|
||||
@@ -1,5 +0,0 @@
|
||||
name = "konsumentinfo.se"
|
||||
path = "http://konsumentinfo.se/telefonnummer/sverige/{ number }"
|
||||
|
||||
[[messages]]
|
||||
selector = ".panel-heading > h1:nth-child(3)"
|
||||
@@ -1,29 +0,0 @@
|
||||
name = "telefonforsaljare.nu"
|
||||
path = "http://www.telefonforsaljare.nu/telefonnummer/{ number }/"
|
||||
|
||||
[[messages]]
|
||||
selector = "#content p:nth-child(2) i"
|
||||
|
||||
[[history]]
|
||||
selector = "#content p:nth-child(4)"
|
||||
|
||||
[[history]]
|
||||
selector = "#content p:nth-child(5)"
|
||||
|
||||
[[comments]]
|
||||
selector = "#kommentarer > [itemtype='http://data-vocabulary.org/Review']"
|
||||
|
||||
[comments.date_time]
|
||||
selector = "small"
|
||||
data = "attr:datetime"
|
||||
kind = "date_time"
|
||||
format = "%Y-%m-%d %H:%M:%S"
|
||||
tz = "Europe/Stockholm"
|
||||
|
||||
[comments.title]
|
||||
selector = "h3"
|
||||
data = "inner_html"
|
||||
|
||||
[comments.message]
|
||||
selector = "[itemprop='description']"
|
||||
data = "inner_html"
|
||||
@@ -1,18 +0,0 @@
|
||||
name = "vemringde.se"
|
||||
path = "http://vemringde.se/?q={ number }"
|
||||
|
||||
[[messages]]
|
||||
selector = "#toporganisations li"
|
||||
|
||||
[[comments]]
|
||||
selector = "#calls ol li"
|
||||
|
||||
[comments.date_time]
|
||||
selector = "div:nth-child(4)"
|
||||
data = "inner_html"
|
||||
kind = "date"
|
||||
format = "%Y-%m-%d"
|
||||
tz = "Europe/Stockholm"
|
||||
|
||||
[comments.message]
|
||||
selector = "div:nth-child(3)"
|
||||
@@ -1,17 +0,0 @@
|
||||
name: "vemringde.se"
|
||||
path: "http://vemringde.se/?q={ number }"
|
||||
|
||||
messages:
|
||||
- selector: "#toporganisations li"
|
||||
|
||||
comments:
|
||||
- selector: "#calls ol li"
|
||||
fields:
|
||||
date_time:
|
||||
selector: "div:nth-child(4)"
|
||||
data: "inner_html"
|
||||
kind: "date"
|
||||
format: "%Y-%m-%d"
|
||||
tz: "Europe/Stockholm"
|
||||
message:
|
||||
selector: "div:nth-child(3)"
|
||||
@@ -1,99 +0,0 @@
|
||||
use std::fs;
|
||||
use std::io;
|
||||
|
||||
use chrono::prelude::*;
|
||||
use chrono::Duration;
|
||||
use directories::ProjectDirs;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct Cache {
|
||||
timestamp: DateTime<Utc>,
|
||||
pub data: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct Context {
|
||||
dirs: ProjectDirs,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
pub fn new() -> Context {
|
||||
Context {
|
||||
dirs: ProjectDirs::from("com", "logaritmisk", "whoareyou").unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_get(&mut self, bin: &str, key: &str) -> Option<Cache> {
|
||||
let cache = self.dirs.cache_dir().join(format!("{}-{}.bin", bin, key));
|
||||
|
||||
if cache.exists() {
|
||||
debug!("cache: bin={} key={} path={:?} exists", bin, key, cache);
|
||||
|
||||
fs::File::open(cache)
|
||||
.and_then(|file| {
|
||||
bincode::deserialize_from(&file).map_err(|_| {
|
||||
debug!("cache: bin={} key={} faild to deserialize", bin, key);
|
||||
|
||||
io::Error::new(io::ErrorKind::Other, "failed to deserialize cache entry")
|
||||
})
|
||||
})
|
||||
.and_then(|cache: Cache| {
|
||||
if cache.timestamp > Utc::now() {
|
||||
debug!("cache: bin={} key={} ok", bin, key);
|
||||
|
||||
Ok(cache)
|
||||
} else {
|
||||
debug!(
|
||||
"cache: bin={} key={} outdated ({})",
|
||||
bin, key, cache.timestamp
|
||||
);
|
||||
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"failed to deserialize cache entry",
|
||||
))
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
} else {
|
||||
debug!("cache: bin={} key={} don't exists", bin, key);
|
||||
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_set<D>(&mut self, bin: &str, key: &str, data: D) -> Result<(), io::Error>
|
||||
where
|
||||
D: AsRef<[u8]>,
|
||||
{
|
||||
let entry = Cache {
|
||||
timestamp: Utc::now() + Duration::days(1),
|
||||
data: data.as_ref().to_vec(),
|
||||
};
|
||||
|
||||
let cache = self.dirs.cache_dir();
|
||||
|
||||
if !cache.exists() {
|
||||
fs::create_dir_all(&cache)?;
|
||||
}
|
||||
|
||||
let cache = cache.join(format!("{}-{}.bin", bin, key));
|
||||
|
||||
debug!(
|
||||
"cache: save: bin={} key={} path={:?} timestamp={}",
|
||||
bin, key, cache, entry.timestamp
|
||||
);
|
||||
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.open(cache)
|
||||
.and_then(|mut file| {
|
||||
bincode::serialize_into(&mut file, &entry).map_err(|_| {
|
||||
io::Error::new(io::ErrorKind::Other, "failed to serialize cache entry")
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,283 +0,0 @@
|
||||
use std::str;
|
||||
|
||||
use chrono_tz::Tz;
|
||||
use scraper::{ElementRef, Html, Selector};
|
||||
use serde::{de, Deserialize, Deserializer, Serialize};
|
||||
use tinytemplate::TinyTemplate;
|
||||
|
||||
use crate::entry::{self, Date, Entry};
|
||||
use crate::probe::Probe;
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct Context {
|
||||
number: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Definition {
|
||||
name: String,
|
||||
path: String,
|
||||
messages: Vec<Field>,
|
||||
#[serde(default)]
|
||||
history: Vec<Field>,
|
||||
#[serde(default)]
|
||||
comments: Vec<Comment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Comment {
|
||||
#[serde(deserialize_with = "deserialize_selector")]
|
||||
selector: Selector,
|
||||
#[serde(rename = "date_time")]
|
||||
datetime: Option<DateTime>,
|
||||
title: Option<Field>,
|
||||
message: Option<Field>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct DateTime {
|
||||
#[serde(flatten)]
|
||||
field: Field,
|
||||
kind: DateTimeKind,
|
||||
format: String,
|
||||
#[serde(deserialize_with = "deserialize_tz")]
|
||||
tz: Tz,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
enum DateTimeKind {
|
||||
Date,
|
||||
DateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "snake_case")]
|
||||
enum Filter {}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Field {
|
||||
#[serde(deserialize_with = "deserialize_selector")]
|
||||
selector: Selector,
|
||||
#[serde(default)]
|
||||
data: Data,
|
||||
#[serde(default)]
|
||||
filters: Vec<Filter>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Data {
|
||||
Text,
|
||||
InnerHtml,
|
||||
Attr { attr: String },
|
||||
}
|
||||
|
||||
impl Data {
|
||||
fn extract(&self, element: &ElementRef) -> Option<String> {
|
||||
match self {
|
||||
Data::Text => Some(
|
||||
element
|
||||
.text()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" "),
|
||||
),
|
||||
Data::InnerHtml => Some(element.inner_html()),
|
||||
Data::Attr { attr } => element.value().attr(attr).map(|data| data.to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Data {
|
||||
fn default() -> Self {
|
||||
Data::Text
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for Data {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Data, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
use std::fmt;
|
||||
|
||||
use serde::de::{self, Visitor};
|
||||
|
||||
struct StrVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for StrVisitor {
|
||||
type Value = Data;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
|
||||
formatter.write_str("an str")
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
|
||||
where
|
||||
E: de::Error,
|
||||
{
|
||||
match value {
|
||||
"text" => Ok(Data::Text),
|
||||
"inner_html" => Ok(Data::InnerHtml),
|
||||
s if s.starts_with("attr:") => {
|
||||
let attr = s.splitn(2, ":").nth(1).unwrap();
|
||||
|
||||
Ok(Data::Attr {
|
||||
attr: attr.to_string(),
|
||||
})
|
||||
}
|
||||
_ => Err(E::custom(format!("unknown data type: {}", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_str(StrVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
impl Probe for Definition {
|
||||
fn provider(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
fn uri(&self, number: &str) -> String {
|
||||
let mut tt = TinyTemplate::new();
|
||||
|
||||
tt.add_template("path", &self.path)
|
||||
.expect("failed to add path template");
|
||||
|
||||
let context = Context {
|
||||
number: number.to_string(),
|
||||
};
|
||||
|
||||
tt.render("path", &context)
|
||||
.expect("failed to render path template")
|
||||
}
|
||||
|
||||
fn fetch(&self, number: &str) -> Result<String, ()> {
|
||||
reqwest::get(&self.uri(number))
|
||||
.map_err(|_| ())?
|
||||
.text()
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn parse(&self, data: &str) -> Result<Entry, ()> {
|
||||
let html = Html::parse_document(data);
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut history = Vec::new();
|
||||
let mut comments = Vec::new();
|
||||
|
||||
for field in &self.messages {
|
||||
for element in html.select(&field.selector) {
|
||||
if let Some(data) = field.data.extract(&element) {
|
||||
messages.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for field in &self.history {
|
||||
for element in html.select(&field.selector) {
|
||||
if let Some(data) = field.data.extract(&element) {
|
||||
history.push(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for comment in &self.comments {
|
||||
for comments_element in html.select(&comment.selector) {
|
||||
let mut datetime: Option<Date> = None;
|
||||
let mut title: Option<String> = None;
|
||||
let mut message: Option<String> = None;
|
||||
|
||||
if let Some(ref datetime_field) = comment.datetime {
|
||||
for comment_element in comments_element.select(&datetime_field.field.selector) {
|
||||
if let Some(data) = datetime_field.field.data.extract(&comment_element) {
|
||||
// for filter in &datetime_field.field.filters {}
|
||||
|
||||
let data = match datetime_field.kind {
|
||||
DateTimeKind::Date => Date::date_from(
|
||||
datetime_field.tz,
|
||||
&data,
|
||||
&datetime_field.format,
|
||||
)
|
||||
.expect("failed to parse date"),
|
||||
DateTimeKind::DateTime => Date::datetime_from(
|
||||
datetime_field.tz,
|
||||
&data,
|
||||
&datetime_field.format,
|
||||
)
|
||||
.expect("failed to parse date time"),
|
||||
};
|
||||
|
||||
datetime = Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref title_field) = comment.title {
|
||||
for comment_element in comments_element.select(&title_field.selector) {
|
||||
if let Some(data) = title_field
|
||||
.data
|
||||
.extract(&comment_element)
|
||||
.filter(|data| !data.is_empty())
|
||||
{
|
||||
// for filter in &message_field.filters {}
|
||||
|
||||
title = Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref message_field) = comment.message {
|
||||
for comment_element in comments_element.select(&message_field.selector) {
|
||||
if let Some(data) = message_field.data.extract(&comment_element) {
|
||||
// for filter in &message_field.filters {}
|
||||
|
||||
message = Some(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if datetime.is_some() && message.is_some() {
|
||||
comments.push(entry::Comment {
|
||||
datetime: datetime.unwrap(),
|
||||
title,
|
||||
message: message.unwrap(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !messages.is_empty() || !history.is_empty() || !comments.is_empty() {
|
||||
Ok(Entry {
|
||||
messages,
|
||||
history,
|
||||
comments,
|
||||
})
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_selector<'de, D>(deserializer: D) -> Result<Selector, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
Selector::parse(&s).map_err(|_| de::Error::custom("failed to parse selector"))
|
||||
}
|
||||
|
||||
fn deserialize_tz<'de, D>(deserializer: D) -> Result<Tz, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
|
||||
s.parse::<Tz>()
|
||||
.map_err(|_| de::Error::custom("failed to parse tz"))
|
||||
}
|
||||
-131
@@ -1,131 +0,0 @@
|
||||
use std::fmt;
|
||||
|
||||
use chrono::offset::LocalResult;
|
||||
use chrono::{Local, NaiveDate, NaiveDateTime, TimeZone, Utc};
|
||||
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct Entry {
|
||||
pub messages: Vec<String>,
|
||||
pub history: Vec<String>,
|
||||
pub comments: Vec<Comment>,
|
||||
}
|
||||
|
||||
impl fmt::Display for Entry {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if !self.messages.is_empty() {
|
||||
for message in &self.messages {
|
||||
writeln!(f, " {}", message)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.history.is_empty() {
|
||||
for history in &self.history {
|
||||
writeln!(f, " {}", history)?;
|
||||
}
|
||||
}
|
||||
|
||||
if !self.comments.is_empty() {
|
||||
for comment in &self.comments {
|
||||
writeln!(f, " * {}", comment)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Serialize)]
|
||||
pub struct Comment {
|
||||
pub datetime: Date,
|
||||
pub title: Option<String>,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl fmt::Display for Comment {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if let Some(ref title) = self.title {
|
||||
write!(f, "{}: {} - {}", self.datetime, title, self.message)
|
||||
} else {
|
||||
write!(f, "{}: {}", self.datetime, self.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, PartialOrd, Ord)]
|
||||
pub enum Date {
|
||||
DateTime(chrono::DateTime<Utc>),
|
||||
#[serde(serialize_with = "serialize_date")]
|
||||
Date(chrono::Date<Utc>),
|
||||
}
|
||||
|
||||
impl Date {
|
||||
pub fn datetime_from<T>(tz: T, s: &str, fmt: &str) -> Result<Date, ()>
|
||||
where
|
||||
T: TimeZone,
|
||||
{
|
||||
let datetime = NaiveDateTime::parse_from_str(s, fmt).map_err(|_| ())?;
|
||||
let datetime = match tz.from_local_datetime(&datetime) {
|
||||
LocalResult::Single(datetime) => datetime,
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
Ok(Date::DateTime(datetime.with_timezone(&Utc)))
|
||||
}
|
||||
|
||||
pub fn date_from<T>(tz: T, s: &str, fmt: &str) -> Result<Date, ()>
|
||||
where
|
||||
T: TimeZone,
|
||||
{
|
||||
let date = NaiveDate::parse_from_str(s, fmt).map_err(|_| ())?;
|
||||
let date = match tz.from_local_date(&date) {
|
||||
LocalResult::Single(date) => date,
|
||||
_ => return Err(()),
|
||||
};
|
||||
|
||||
Ok(Date::Date(date.with_timezone(&Utc)))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Date {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Date::DateTime(datetime) => {
|
||||
let datetime = datetime.with_timezone(&Local);
|
||||
|
||||
write!(f, "{}", datetime.format("%Y-%m-%d %H:%M:%S"))
|
||||
}
|
||||
Date::Date(date) => {
|
||||
let date = date.with_timezone(&Local);
|
||||
|
||||
write!(f, "{}", date.format("%Y-%m-%d"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn serialize_date<S>(date: &chrono::Date<Utc>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
let date = date.with_timezone(&Local);
|
||||
let s = format!("{}", date.format("%Y-%m-%d"));
|
||||
|
||||
Serialize::serialize(&s, serializer)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn deserialize_date<'de, D>(deserializer: D) -> Result<chrono::Date<Utc>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
let date = NaiveDate::parse_from_str(&s, "%Y-%m-%d").map_err(de::Error::custom)?;
|
||||
let date = match Utc.from_local_date(&date) {
|
||||
LocalResult::Single(date) => date,
|
||||
_ => return Err(de::Error::custom("")),
|
||||
};
|
||||
|
||||
Ok(date.with_timezone(&Utc))
|
||||
}
|
||||
-37
@@ -1,37 +0,0 @@
|
||||
use std::str;
|
||||
|
||||
use scraper::{ElementRef, Html};
|
||||
|
||||
pub trait SelectExt {
|
||||
fn element(&self) -> ElementRef;
|
||||
|
||||
fn easy_text(&self) -> String {
|
||||
let data = self
|
||||
.element()
|
||||
.text()
|
||||
.map(str::trim)
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join(" ");
|
||||
|
||||
htmlescape::decode_html(&data).unwrap_or(data)
|
||||
}
|
||||
|
||||
fn easy_inner_html(&self) -> String {
|
||||
let data = self.element().inner_html();
|
||||
|
||||
htmlescape::decode_html(&data).unwrap_or(data)
|
||||
}
|
||||
}
|
||||
|
||||
impl SelectExt for Html {
|
||||
fn element(&self) -> ElementRef {
|
||||
self.root_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> SelectExt for ElementRef<'a> {
|
||||
fn element(&self) -> ElementRef {
|
||||
*self
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
mod context;
|
||||
pub mod definition;
|
||||
pub mod entry;
|
||||
mod html;
|
||||
mod probe;
|
||||
|
||||
pub use crate::context::Context;
|
||||
pub use crate::definition::Definition;
|
||||
pub use crate::probe::*;
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
use std::fs;
|
||||
use std::io::Read;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
use fern::colors::{Color, ColoredLevelConfig};
|
||||
use structopt::StructOpt;
|
||||
use whoareyou::*;
|
||||
|
||||
#[derive(Debug, StructOpt)]
|
||||
#[structopt(name = "whoareyou", about = "Search for swedish phone numbers.")]
|
||||
struct Opt {
|
||||
#[structopt(short = "v", parse(from_occurrences))]
|
||||
verbose: u8,
|
||||
|
||||
#[structopt(short = "o", long = "open")]
|
||||
open: bool,
|
||||
|
||||
#[structopt(short = "d", long = "definitions", parse(from_os_str))]
|
||||
definitions: Vec<PathBuf>,
|
||||
|
||||
number: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let opt = Opt::from_args();
|
||||
|
||||
let colors = ColoredLevelConfig::new()
|
||||
.error(Color::Red)
|
||||
.warn(Color::Yellow)
|
||||
.info(Color::White)
|
||||
.debug(Color::White)
|
||||
.trace(Color::BrightBlack);
|
||||
|
||||
let mut config = fern::Dispatch::new()
|
||||
.format(move |out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{}[{}][{}] {}",
|
||||
chrono::Local::now().format("[%Y-%m-%d %H:%M:%S]"),
|
||||
record.target(),
|
||||
colors.color(record.level()),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level_for("reqwest", log::LevelFilter::Off)
|
||||
.level_for("hyper", log::LevelFilter::Off)
|
||||
.level_for("tokio_reactor", log::LevelFilter::Off)
|
||||
.level_for("html5ever", log::LevelFilter::Off)
|
||||
.level_for("selectors", log::LevelFilter::Off)
|
||||
.chain(std::io::stdout());
|
||||
|
||||
config = match opt.verbose {
|
||||
0 => config.level(log::LevelFilter::Info),
|
||||
1 => config.level(log::LevelFilter::Debug),
|
||||
2 => config.level(log::LevelFilter::Debug),
|
||||
_ => config.level(log::LevelFilter::Trace),
|
||||
};
|
||||
|
||||
config.apply().expect("failed to init fern");
|
||||
|
||||
let mut probes: Vec<Box<dyn Probe>> = vec![Box::new(Hitta)];
|
||||
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
for definition in &opt.definitions {
|
||||
let definition = fs::File::open(&definition)
|
||||
.and_then(|mut file| {
|
||||
file.read_to_end(&mut buffer)
|
||||
.expect("failed to read definition file");
|
||||
|
||||
let definition: Definition =
|
||||
toml::from_slice(&buffer).expect("failed to parse definition file");
|
||||
|
||||
buffer.clear();
|
||||
|
||||
Ok(definition)
|
||||
})
|
||||
.expect("failed to open definition file");
|
||||
|
||||
probes.push(Box::new(definition));
|
||||
}
|
||||
|
||||
if opt.open {
|
||||
for probe in &mut probes {
|
||||
let uri = probe.uri(&opt.number);
|
||||
|
||||
Command::new("open")
|
||||
.arg(uri)
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
}
|
||||
} else {
|
||||
let mut ctx = Context::new();
|
||||
|
||||
let mut first = true;
|
||||
|
||||
for probe in &mut probes {
|
||||
let data = if let Some(cache) = ctx.cache_get(probe.provider(), &opt.number) {
|
||||
String::from_utf8(cache.data).unwrap()
|
||||
} else if let Ok(data) = probe.fetch(&opt.number) {
|
||||
ctx.cache_set(probe.provider(), &opt.number, data.as_bytes())
|
||||
.expect("wut?! why not?!");
|
||||
|
||||
data
|
||||
} else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(entry) = probe.parse(&data) {
|
||||
if first {
|
||||
print!("{}\n{}", probe.provider(), entry);
|
||||
|
||||
first = false;
|
||||
} else {
|
||||
print!("\n{}\n{}", probe.provider(), entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use crate::entry::Entry;
|
||||
|
||||
mod hitta;
|
||||
|
||||
pub use self::hitta::Hitta;
|
||||
|
||||
pub trait Probe {
|
||||
fn provider(&self) -> &str;
|
||||
fn uri(&self, _: &str) -> String;
|
||||
|
||||
fn fetch(&self, _: &str) -> Result<String, ()>;
|
||||
fn parse(&self, _: &str) -> Result<Entry, ()>;
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
use lazy_static::lazy_static;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
use crate::entry::Entry;
|
||||
use crate::html::SelectExt;
|
||||
use crate::probe::Probe;
|
||||
|
||||
lazy_static! {
|
||||
static ref MESSAGE: Selector = Selector::parse(".CompanyResultListItem h3.name > a").unwrap();
|
||||
static ref HISTORY_1: Selector =
|
||||
Selector::parse("div.PhoneNoHit div.search-info-container p").unwrap();
|
||||
static ref HISTORY_2: Selector =
|
||||
Selector::parse("div.feedback-types div.feedback-type-item").unwrap();
|
||||
}
|
||||
|
||||
fn from_html(document: &str) -> Result<Entry, ()> {
|
||||
let html = Html::parse_document(document);
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut history = Vec::new();
|
||||
let comments = Vec::new();
|
||||
|
||||
if let Some(message) = html
|
||||
.select(&MESSAGE)
|
||||
.next()
|
||||
.map(|element| element.easy_text())
|
||||
{
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
if let Some(message) = html
|
||||
.select(&HISTORY_1)
|
||||
.next()
|
||||
.map(|element| element.easy_text())
|
||||
{
|
||||
history.push(message);
|
||||
}
|
||||
|
||||
for message in html.select(&HISTORY_2).map(|element| element.easy_text()) {
|
||||
history.push(message);
|
||||
}
|
||||
|
||||
Ok(Entry {
|
||||
messages,
|
||||
history,
|
||||
comments,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Eniro;
|
||||
|
||||
impl Probe for Eniro {
|
||||
fn provider(&self) -> &'static str {
|
||||
"eniro.se"
|
||||
}
|
||||
|
||||
fn uri(&self, number: &str) -> String {
|
||||
format!("https://gulasidorna.eniro.se/hitta:{}", number)
|
||||
}
|
||||
|
||||
fn fetch(&self, number: &str) -> Result<String, ()> {
|
||||
reqwest::get(&self.uri(number))
|
||||
.map_err(|_| ())?
|
||||
.text()
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn parse(&self, data: &str) -> Result<Entry, ()> {
|
||||
from_html(&data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_yaml_snapshot_matches;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_0104754350() {
|
||||
let document = include_str!("../../fixtures/eniro/0104754350.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Företaget bedriver telefonförsäljning eller marknadsundersökningar
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0313908905() {
|
||||
let document = include_str!("../../fixtures/eniro/0313908905.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 3464 denna vecka och 6637 totalt.
|
||||
- 76 Försäljning
|
||||
- 47 Oseriös verksamhet
|
||||
- 37 Annat
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0702269893() {
|
||||
let document = include_str!("../../fixtures/eniro/0702269893.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Anonym Kund För Refill
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0726443387() {
|
||||
let document = include_str!("../../fixtures/eniro/0726443387.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 16 denna vecka och 98 totalt.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793426() {
|
||||
let document = include_str!("../../fixtures/eniro/0751793426.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 20 denna vecka och 602 totalt.
|
||||
- 11 Försäljning
|
||||
- 9 Annat
|
||||
- 7 Oseriös verksamhet
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793483() {
|
||||
let document = include_str!("../../fixtures/eniro/0751793483.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 29 denna vecka och 900 totalt.
|
||||
- 5 Annat
|
||||
- 4 Oseriös verksamhet
|
||||
- 3 Marknadsföring
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793499() {
|
||||
let document = include_str!("../../fixtures/eniro/0751793499.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 303 denna vecka och 304 totalt.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0701807618() {
|
||||
let document = include_str!("../../fixtures/eniro/0701807618.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 0 denna vecka och 1 totalt.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0546780862() {
|
||||
let document = include_str!("../../fixtures/eniro/0546780862.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Nya Wermlands-Tidningens AB
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
}
|
||||
@@ -1,335 +0,0 @@
|
||||
use chrono::{TimeZone, Utc};
|
||||
use log::{debug, trace};
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::entry::{self, Date, Entry};
|
||||
use crate::probe::Probe;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Data {
|
||||
props: Props,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Props {
|
||||
page_props: PageProps,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PageProps {
|
||||
status_code: Option<u16>,
|
||||
phone_data: Option<PhoneData>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct PhoneData {
|
||||
alternative_formats: Vec<String>,
|
||||
clean_number: String,
|
||||
#[serde(default)]
|
||||
comments: Vec<Comment>,
|
||||
statistics_text: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct Comment {
|
||||
comment: String,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
fn from_html(document: &str) -> Result<Entry, ()> {
|
||||
let re = Regex::new(r#"<script>__NEXT_DATA__ = (.*?);__NEXT_LOADED_PAGES__"#).unwrap();
|
||||
|
||||
let result = re.captures(&document).ok_or_else(|| {
|
||||
debug!("Hitta: failed to find __NEXT_DATA__");
|
||||
})?;
|
||||
|
||||
let json = result.get(1).unwrap().as_str();
|
||||
|
||||
trace!(
|
||||
"Hitta: {:#?}",
|
||||
serde_json::from_str::<serde_json::Value>(&json)
|
||||
);
|
||||
|
||||
if let Ok(data) = serde_json::from_str::<Data>(&json) {
|
||||
let messages = Vec::new();
|
||||
let mut history = Vec::new();
|
||||
let mut comments = Vec::new();
|
||||
|
||||
if let Some(phone_data) = data.props.page_props.phone_data {
|
||||
history.push(phone_data.statistics_text);
|
||||
|
||||
for comment in phone_data.comments {
|
||||
comments.push(entry::Comment {
|
||||
datetime: Date::DateTime(Utc.timestamp(
|
||||
(comment.timestamp / 1000) as i64,
|
||||
(comment.timestamp % 1000) as u32,
|
||||
)),
|
||||
title: None,
|
||||
message: comment.comment,
|
||||
});
|
||||
}
|
||||
|
||||
comments.sort_by(|a, b| b.datetime.cmp(&a.datetime));
|
||||
}
|
||||
|
||||
Ok(Entry {
|
||||
messages,
|
||||
history,
|
||||
comments,
|
||||
})
|
||||
} else {
|
||||
if let Err(error) = serde_json::from_str::<Data>(&json) {
|
||||
debug!("Hitta: failed to deserialize data: {:#?}", error);
|
||||
}
|
||||
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Hitta;
|
||||
|
||||
impl Probe for Hitta {
|
||||
fn provider(&self) -> &'static str {
|
||||
"hitta.se"
|
||||
}
|
||||
|
||||
fn uri(&self, number: &str) -> String {
|
||||
format!("https://www.hitta.se/vem-ringde/{}", number)
|
||||
}
|
||||
|
||||
fn fetch(&self, number: &str) -> Result<String, ()> {
|
||||
reqwest::get(&self.uri(number))
|
||||
.map_err(|_| ())?
|
||||
.text()
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn parse(&self, data: &str) -> Result<Entry, ()> {
|
||||
from_html(&data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_yaml_snapshot;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_0104754350() {
|
||||
let document = include_str!("../../fixtures/hitta/0104754350.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 42 andra har rapporterat detta nummer
|
||||
comments:
|
||||
- datetime:
|
||||
DateTime: "2019-01-17T17:29:22Z"
|
||||
title: ~
|
||||
message: Varmsälj från Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-12-14T13:45:28Z"
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-11-28T07:30:18Z"
|
||||
title: ~
|
||||
message: Höglandschskt
|
||||
- datetime:
|
||||
DateTime: "2018-11-20T19:18:09Z"
|
||||
title: ~
|
||||
message: "Försäljare "
|
||||
- datetime:
|
||||
DateTime: "2018-11-19T17:38:34Z"
|
||||
title: ~
|
||||
message: mögg från Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-11-12T16:00:41Z"
|
||||
title: ~
|
||||
message: Folksam försäkringsförsäljare
|
||||
- datetime:
|
||||
DateTime: "2018-10-25T10:28:36Z"
|
||||
title: ~
|
||||
message: folksam
|
||||
- datetime:
|
||||
DateTime: "2018-10-10T07:30:40Z"
|
||||
title: ~
|
||||
message: Telefonförsäljare
|
||||
- datetime:
|
||||
DateTime: "2018-10-04T10:04:55Z"
|
||||
title: ~
|
||||
message: Folksam säljare
|
||||
- datetime:
|
||||
DateTime: "2018-10-03T13:55:19Z"
|
||||
title: ~
|
||||
message: Sa inget.
|
||||
- datetime:
|
||||
DateTime: "2018-08-24T16:56:46Z"
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-08-24T09:42:43Z"
|
||||
title: ~
|
||||
message: Achmati azmut från folksam
|
||||
- datetime:
|
||||
DateTime: "2018-08-21T18:29:29Z"
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-08-16T18:56:56Z"
|
||||
title: ~
|
||||
message: Säljare från Folksam.
|
||||
- datetime:
|
||||
DateTime: "2018-08-16T14:48:59Z"
|
||||
title: ~
|
||||
message: "Folksam "
|
||||
- datetime:
|
||||
DateTime: "2018-08-09T16:30:28Z"
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-08-02T16:29:32Z"
|
||||
title: ~
|
||||
message: "Folksam "
|
||||
- datetime:
|
||||
DateTime: "2018-08-02T15:33:38Z"
|
||||
title: ~
|
||||
message: "Folksam "
|
||||
- datetime:
|
||||
DateTime: "2018-07-25T08:28:27Z"
|
||||
title: ~
|
||||
message: Säljare Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-07-17T21:20:51Z"
|
||||
title: ~
|
||||
message: "Inga Hansson "
|
||||
- datetime:
|
||||
DateTime: "2018-07-16T18:11:46Z"
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-07-06T15:45:46Z"
|
||||
title: ~
|
||||
message: "Folksam "
|
||||
- datetime:
|
||||
DateTime: "2018-07-05T17:24:07Z"
|
||||
title: ~
|
||||
message: folksam
|
||||
- datetime:
|
||||
DateTime: "2018-07-05T11:15:02Z"
|
||||
title: ~
|
||||
message: Vesran
|
||||
- datetime:
|
||||
DateTime: "2018-07-04T13:30:49Z"
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
DateTime: "2018-06-29T10:52:51Z"
|
||||
title: ~
|
||||
message: folksam
|
||||
- datetime:
|
||||
DateTime: "2018-06-28T13:33:01Z"
|
||||
title: ~
|
||||
message: Säljare folksam
|
||||
- datetime:
|
||||
DateTime: "2018-06-28T07:42:42Z"
|
||||
title: ~
|
||||
message: Folksam försäkringar
|
||||
- datetime:
|
||||
DateTime: "2018-06-26T12:59:33Z"
|
||||
title: ~
|
||||
message: Säljare Folksam"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0313908905() {
|
||||
let document = include_str!("../../fixtures/hitta/0313908905.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Ok:
|
||||
messages: []
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0702269893() {
|
||||
let document = include_str!("../../fixtures/hitta/0702269893.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Ok:
|
||||
messages: []
|
||||
history:
|
||||
- Tre andra har också sökt på detta nummer
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0726443387() {
|
||||
let document = include_str!("../../fixtures/hitta/0726443387.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Ok:
|
||||
messages: []
|
||||
history:
|
||||
- 1299 andra har också sökt på detta nummer
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793426() {
|
||||
let document = include_str!("../../fixtures/hitta/0751793426.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Ok:
|
||||
messages: []
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793483() {
|
||||
let document = include_str!("../../fixtures/hitta/0751793483.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Ok:
|
||||
messages: []
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793499() {
|
||||
let document = include_str!("../../fixtures/hitta/0751793499.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Ok:
|
||||
messages: []
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0701807618() {
|
||||
let document = include_str!("../../fixtures/hitta/0701807618.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Err: ~"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0546780862() {
|
||||
let document = include_str!("../../fixtures/hitta/0546780862.html");
|
||||
|
||||
assert_yaml_snapshot!(from_html(&document), @r###"---
|
||||
Err: ~"###);
|
||||
}
|
||||
}
|
||||
@@ -1,132 +0,0 @@
|
||||
use lazy_static::lazy_static;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
use crate::html::SelectExt;
|
||||
use crate::probe::{Entry, Probe};
|
||||
|
||||
lazy_static! {
|
||||
static ref MESSAGE: Selector = Selector::parse(".panel-heading > h1:nth-child(3)").unwrap();
|
||||
}
|
||||
|
||||
fn from_html(document: &str) -> Result<Entry, ()> {
|
||||
let html = Html::parse_document(document);
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let history = Vec::new();
|
||||
let comments = Vec::new();
|
||||
|
||||
if let Some(message) = html
|
||||
.select(&MESSAGE)
|
||||
.next()
|
||||
.map(|element| element.easy_text())
|
||||
{
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
if !messages.is_empty() {
|
||||
Ok(Entry {
|
||||
messages,
|
||||
history,
|
||||
comments,
|
||||
})
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct KonsumentInfo;
|
||||
|
||||
impl Probe for KonsumentInfo {
|
||||
fn provider(&self) -> &'static str {
|
||||
"konsumentinfo.se"
|
||||
}
|
||||
|
||||
fn uri(&self, number: &str) -> String {
|
||||
format!("http://konsumentinfo.se/telefonnummer/sverige/{}", number)
|
||||
}
|
||||
|
||||
fn fetch(&self, number: &str) -> Result<String, ()> {
|
||||
reqwest::get(&self.uri(number))
|
||||
.map_err(|_| ())?
|
||||
.text()
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn parse(&self, data: &str) -> Result<Entry, ()> {
|
||||
from_html(&data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_yaml_snapshot_matches;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_0104754350() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0104754350.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0313908905() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0313908905.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0702269893() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0702269893.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Hydroscand AB
|
||||
history: []
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0726443387() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0726443387.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793426() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0751793426.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793483() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0751793483.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793499() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0751793499.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0701807618() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0701807618.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0546780862() {
|
||||
let document = include_str!("../../fixtures/konsumentinfo/0546780862.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
use chrono_tz::Europe::Stockholm;
|
||||
use lazy_static::lazy_static;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
use crate::entry::{Comment, Date, Entry};
|
||||
use crate::html::SelectExt;
|
||||
use crate::probe::Probe;
|
||||
|
||||
lazy_static! {
|
||||
static ref MESSAGE: Selector = Selector::parse("#content p:nth-child(2) i").unwrap();
|
||||
static ref HISTORY_1: Selector = Selector::parse("#content p:nth-child(4)").unwrap();
|
||||
static ref HISTORY_2: Selector = Selector::parse("#content p:nth-child(5)").unwrap();
|
||||
static ref COMMENTS: Selector =
|
||||
Selector::parse("#kommentarer > [itemtype='http://data-vocabulary.org/Review']").unwrap();
|
||||
static ref COMMENT_DATETIME: Selector = Selector::parse("small").unwrap();
|
||||
static ref COMMENT_TITLE: Selector = Selector::parse("h3").unwrap();
|
||||
static ref COMMENT_MESSAGE: Selector = Selector::parse("[itemprop='description']").unwrap();
|
||||
}
|
||||
|
||||
fn from_html(document: &str) -> Result<Entry, ()> {
|
||||
let html = Html::parse_document(document);
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let mut history = Vec::new();
|
||||
let mut comments = Vec::new();
|
||||
|
||||
if let Some(element) = html.select(&MESSAGE).next() {
|
||||
let message = element.inner_html();
|
||||
let message = htmlescape::decode_html(&message).unwrap();
|
||||
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
if let Some(message) = html
|
||||
.select(if messages.is_empty() {
|
||||
&HISTORY_1
|
||||
} else {
|
||||
&HISTORY_2
|
||||
})
|
||||
.next()
|
||||
.map(|element| element.easy_text())
|
||||
{
|
||||
history.push(message);
|
||||
}
|
||||
|
||||
for comment in html.select(&COMMENTS) {
|
||||
let datetime = comment
|
||||
.select(&COMMENT_DATETIME)
|
||||
.next()
|
||||
.unwrap()
|
||||
.value()
|
||||
.attr("datetime")
|
||||
.unwrap()
|
||||
.to_string();
|
||||
|
||||
let title = comment
|
||||
.select(&COMMENT_TITLE)
|
||||
.next()
|
||||
.map(|element| element.easy_inner_html())
|
||||
.filter(|title| !title.is_empty());
|
||||
|
||||
let message = comment
|
||||
.select(&COMMENT_MESSAGE)
|
||||
.next()
|
||||
.map(|element| element.easy_inner_html())
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
comments.push(Comment {
|
||||
datetime: Date::datetime_from(Stockholm, &datetime, "%Y-%m-%d %H:%M:%S")
|
||||
.expect("failed to parse datetime"),
|
||||
title,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Entry {
|
||||
messages,
|
||||
history,
|
||||
comments,
|
||||
})
|
||||
}
|
||||
|
||||
pub struct Telefonforsaljare;
|
||||
|
||||
impl Probe for Telefonforsaljare {
|
||||
fn provider(&self) -> &'static str {
|
||||
"telefonforsaljare.nu"
|
||||
}
|
||||
|
||||
fn uri(&self, number: &str) -> String {
|
||||
format!("http://www.telefonforsaljare.nu/telefonnummer/{}/", number)
|
||||
}
|
||||
|
||||
fn fetch(&self, number: &str) -> Result<String, ()> {
|
||||
reqwest::get(&self.uri(number))
|
||||
.map_err(|_| ())?
|
||||
.text()
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn parse(&self, data: &str) -> Result<Entry, ()> {
|
||||
from_html(&data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_yaml_snapshot_matches;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_0104754350() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0104754350.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Folksam
|
||||
history:
|
||||
- De senaste 24 timmarna har 9 personer sökt efter numret 0104754350. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 4786 personer sökt efter numret.
|
||||
comments:
|
||||
- datetime:
|
||||
DateTime: "2018-05-09T12:31:39Z"
|
||||
title: Folksam
|
||||
message: Svara inte på okända nummer. Blockerat!
|
||||
- datetime:
|
||||
DateTime: "2017-12-05T16:33:10Z"
|
||||
title: Folksam
|
||||
message: Svarade aldrig men när jag ringde upp var det Folksam
|
||||
- datetime:
|
||||
DateTime: "2017-11-28T10:30:10Z"
|
||||
title: ~
|
||||
message: Ringde och la på
|
||||
- datetime:
|
||||
DateTime: "2017-11-20T14:53:16Z"
|
||||
title: Folksam
|
||||
message: färsäljare
|
||||
- datetime:
|
||||
DateTime: "2017-11-16T12:38:07Z"
|
||||
title: Folksam
|
||||
message: "missat samtal, ringde tillbaka och automatsvar sa att det var folksam som sökt mig för att presentera ett erbjudande."
|
||||
- datetime:
|
||||
DateTime: "2017-10-25T05:59:26Z"
|
||||
title: Folksam
|
||||
message: Försäljare"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0313908905() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0313908905.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 301 personer sökt efter numret.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0702269893() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0702269893.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Alnö Design & Produktion AB
|
||||
history:
|
||||
- De senaste 24 timmarna har 3 personer sökt efter numret 0702269893. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 4 personer sökt efter numret.
|
||||
comments:
|
||||
- datetime:
|
||||
DateTime: "2019-01-18T13:30:55Z"
|
||||
title: Alnö Design & Produktion AB
|
||||
message: "Renhållning, service, kemprodukter""###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0726443387() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0726443387.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Tele2
|
||||
history:
|
||||
- De senaste 24 timmarna har 1 personer sökt efter numret 0726443387. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 231 personer sökt efter numret.
|
||||
comments:
|
||||
- datetime:
|
||||
DateTime: "2018-10-31T17:48:27Z"
|
||||
title: Tele2
|
||||
message: Bättre priser som inte finns online"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793426() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0751793426.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 38 personer sökt efter numret.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793483() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0751793483.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 25 personer sökt efter numret.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793499() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0751793499.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- Du är den första de senaste 24 timmarna som söker efter detta nummer. Det tyder på att numret inte används av telefonförsäljare. Totalt har minst 22 personer sökt efter numret.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0701807618() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0701807618.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- De senaste 24 timmarna har 1 personer sökt efter numret 0701807618. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 2 personer sökt efter numret.
|
||||
comments: []"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0546780862() {
|
||||
let document = include_str!("../../fixtures/telefonforsaljare/0546780862.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history:
|
||||
- De senaste 24 timmarna har 1 personer sökt efter numret 0546780862. Det kan tyda på att numret används av telefonförsäljare. Totalt har minst 12 personer sökt efter numret.
|
||||
comments: []"###);
|
||||
}
|
||||
}
|
||||
@@ -1,218 +0,0 @@
|
||||
use std::str;
|
||||
|
||||
use chrono_tz::Europe::Stockholm;
|
||||
use lazy_static::lazy_static;
|
||||
use scraper::{Html, Selector};
|
||||
|
||||
use crate::entry::{Comment, Date, Entry};
|
||||
use crate::html::SelectExt;
|
||||
use crate::probe::Probe;
|
||||
|
||||
lazy_static! {
|
||||
static ref MESSAGE: Selector = Selector::parse("#toporganisations li").unwrap();
|
||||
static ref COMMENTS: Selector = Selector::parse("#calls ol li").unwrap();
|
||||
static ref COMMENT_DATETIME: Selector = Selector::parse("div:nth-child(4)").unwrap();
|
||||
static ref COMMENT_MESSAGE: Selector = Selector::parse("div:nth-child(3)").unwrap();
|
||||
}
|
||||
|
||||
fn from_html(document: &str) -> Result<Entry, ()> {
|
||||
let html = Html::parse_document(document);
|
||||
|
||||
let mut messages = Vec::new();
|
||||
let history = Vec::new();
|
||||
let mut comments = Vec::new();
|
||||
|
||||
for message in html.select(&MESSAGE).map(|element| element.easy_text()) {
|
||||
messages.push(message);
|
||||
}
|
||||
|
||||
for element in html.select(&COMMENTS) {
|
||||
let date = element
|
||||
.select(&COMMENT_DATETIME)
|
||||
.next()
|
||||
.map(|element| element.easy_inner_html())
|
||||
.expect("failed to find datetime");
|
||||
|
||||
let message = element
|
||||
.select(&COMMENT_MESSAGE)
|
||||
.next()
|
||||
.map(|element| element.easy_text())
|
||||
.unwrap_or_else(String::new);
|
||||
|
||||
comments.push(Comment {
|
||||
datetime: Date::date_from(Stockholm, &date, "%Y-%m-%d").expect("failed to parse date"),
|
||||
title: None,
|
||||
message,
|
||||
});
|
||||
}
|
||||
|
||||
if !messages.is_empty() || !comments.is_empty() {
|
||||
Ok(Entry {
|
||||
messages,
|
||||
history,
|
||||
comments,
|
||||
})
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct VemRingde;
|
||||
|
||||
impl Probe for VemRingde {
|
||||
fn provider(&self) -> &'static str {
|
||||
"vemringde.se"
|
||||
}
|
||||
|
||||
fn uri(&self, number: &str) -> String {
|
||||
format!("http://vemringde.se/?q={}", number)
|
||||
}
|
||||
|
||||
fn fetch(&self, number: &str) -> Result<String, ()> {
|
||||
reqwest::get(&self.uri(number))
|
||||
.map_err(|_| ())?
|
||||
.text()
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
fn parse(&self, data: &str) -> Result<Entry, ()> {
|
||||
from_html(&data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use insta::assert_yaml_snapshot_matches;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_0104754350() {
|
||||
let document = include_str!("../../fixtures/vemringde/0104754350.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages:
|
||||
- Folksam (5 samtal)
|
||||
history: []
|
||||
comments:
|
||||
- datetime:
|
||||
Date: 2018-11-07
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
Date: 2018-06-05
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
Date: 2018-04-18
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
Date: 2018-03-19
|
||||
title: ~
|
||||
message: okänd
|
||||
- datetime:
|
||||
Date: 2018-03-07
|
||||
title: ~
|
||||
message: okänd
|
||||
- datetime:
|
||||
Date: 2018-02-06
|
||||
title: ~
|
||||
message: Folksam spam
|
||||
- datetime:
|
||||
Date: 2017-12-20
|
||||
title: ~
|
||||
message: svarade ej
|
||||
- datetime:
|
||||
Date: 2017-12-07
|
||||
title: ~
|
||||
message: okänd
|
||||
- datetime:
|
||||
Date: 2017-12-05
|
||||
title: ~
|
||||
message: okänd
|
||||
- datetime:
|
||||
Date: 2017-11-21
|
||||
title: ~
|
||||
message: Försäljare folksam
|
||||
- datetime:
|
||||
Date: 2017-11-14
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
Date: 2017-11-06
|
||||
title: ~
|
||||
message: Folksam
|
||||
- datetime:
|
||||
Date: 2017-10-24
|
||||
title: ~
|
||||
message: telemarketing
|
||||
- datetime:
|
||||
Date: 2017-10-23
|
||||
title: ~
|
||||
message: okänd"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0313908905() {
|
||||
let document = include_str!("../../fixtures/vemringde/0313908905.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @r###"Ok:
|
||||
messages: []
|
||||
history: []
|
||||
comments:
|
||||
- datetime:
|
||||
Date: 2018-11-26
|
||||
title: ~
|
||||
message: callcenter"###);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0702269893() {
|
||||
let document = include_str!("../../fixtures/vemringde/0702269893.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0726443387() {
|
||||
let document = include_str!("../../fixtures/vemringde/0726443387.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793426() {
|
||||
let document = include_str!("../../fixtures/vemringde/0751793426.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793483() {
|
||||
let document = include_str!("../../fixtures/vemringde/0751793483.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0751793499() {
|
||||
let document = include_str!("../../fixtures/vemringde/0751793499.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0701807618() {
|
||||
let document = include_str!("../../fixtures/vemringde/0701807618.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_0546780862() {
|
||||
let document = include_str!("../../fixtures/vemringde/0546780862.html");
|
||||
|
||||
assert_yaml_snapshot_matches!(from_html(&document), @"Err: ~");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package whoareyou:provider@0.1.0;
|
||||
|
||||
interface lookup {
|
||||
record provider-info {
|
||||
name: string,
|
||||
version: string,
|
||||
}
|
||||
|
||||
record request {
|
||||
url: string,
|
||||
}
|
||||
|
||||
record response {
|
||||
status: u16,
|
||||
body: string,
|
||||
}
|
||||
|
||||
record comment {
|
||||
timestamp: option<s64>,
|
||||
title: option<string>,
|
||||
message: string,
|
||||
}
|
||||
|
||||
record entry {
|
||||
messages: list<string>,
|
||||
history: list<string>,
|
||||
comments: list<comment>,
|
||||
}
|
||||
|
||||
variant lookup-error {
|
||||
no-data,
|
||||
parse-failed(string),
|
||||
}
|
||||
|
||||
metadata: func() -> provider-info;
|
||||
requests: func(number: string) -> list<request>;
|
||||
parse: func(number: string, responses: list<response>) -> result<entry, lookup-error>;
|
||||
}
|
||||
|
||||
world provider {
|
||||
export lookup;
|
||||
}
|
||||
Reference in New Issue
Block a user