mirror of
https://github.com/muerwre/vk-tg-bot.git
synced 2025-04-24 22:46:41 +07:00
commit
65070fd2eb
33 changed files with 3569 additions and 204 deletions
1
.env
1
.env
|
@ -1 +0,0 @@
|
||||||
EXPOSE=7003
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -2,3 +2,4 @@ config.yml
|
||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.idea
|
.idea
|
||||||
|
/key.local.json
|
|
@ -22,6 +22,9 @@ templates:
|
||||||
wall_post_new: templates/post_new.md
|
wall_post_new: templates/post_new.md
|
||||||
group_join: templates/group_join.md
|
group_join: templates/group_join.md
|
||||||
group_leave: templates/group_leave.md
|
group_leave: templates/group_leave.md
|
||||||
|
calendar:
|
||||||
|
keyFile: # key.json
|
||||||
|
timezone:
|
||||||
# groups:
|
# groups:
|
||||||
# - id: 0
|
# - id: 0
|
||||||
# name: 'Group name'
|
# name: 'Group name'
|
||||||
|
@ -29,6 +32,9 @@ templates:
|
||||||
# secretKey: 'groupSecretKey'
|
# secretKey: 'groupSecretKey'
|
||||||
# apiKey: 'callbackApiKey'
|
# apiKey: 'callbackApiKey'
|
||||||
# post_types: ['post','copy','reply','postpone','suggest']
|
# post_types: ['post','copy','reply','postpone','suggest']
|
||||||
|
# calendar:
|
||||||
|
# id: abcd@group.calendar.google.com
|
||||||
|
# enabled: true
|
||||||
# templates: # group's custom templates (optional)
|
# templates: # group's custom templates (optional)
|
||||||
# message_new: templates/custom/message_new.md
|
# message_new: templates/custom/message_new.md
|
||||||
# channels:
|
# channels:
|
||||||
|
|
15
jest.config.cjs
Normal file
15
jest.config.cjs
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
/** @type {import('@jest/types').Config.ProjectConfig} */
|
||||||
|
module.exports = {
|
||||||
|
transform: {
|
||||||
|
"\\.[jt]sx?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
useESM: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
moduleNameMapper: {
|
||||||
|
"(.+)\\.js": "$1",
|
||||||
|
},
|
||||||
|
extensionsToTreatAsEsm: [".ts"],
|
||||||
|
};
|
13
key.json
Normal file
13
key.json
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"type": "service_account",
|
||||||
|
"project_id": "",
|
||||||
|
"private_key_id": "",
|
||||||
|
"private_key": "",
|
||||||
|
"client_email": "",
|
||||||
|
"client_id": "",
|
||||||
|
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
||||||
|
"token_uri": "https://oauth2.googleapis.com/token",
|
||||||
|
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
||||||
|
"client_x509_cert_url": "",
|
||||||
|
"universe_domain": "googleapis.com"
|
||||||
|
}
|
15
package.json
15
package.json
|
@ -7,12 +7,16 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node ./dist/index.js",
|
"start": "node ./dist/index.js",
|
||||||
"dev": "NODE_ENV=dev node -r ts-node/register ./src/index.ts --config=./config.yml",
|
"dev": "NODE_ENV=dev node -r ts-node/register ./src/index.ts --config=./config.yml",
|
||||||
"build": "rm -rf ./dist && tsc && copyfiles -f ./config*.yml ./dist && copyfiles ./templates/*.md ./dist"
|
"build": "rm -rf ./dist && tsc && copyfiles -f ./config*.yml ./dist && copyfiles ./templates/*.md ./dist",
|
||||||
|
"test": "jest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"cross-fetch": "^3.1.4",
|
"cross-fetch": "^3.1.4",
|
||||||
|
"date-fns": "^3.2.0",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"google-auth-library": "^9.4.1",
|
||||||
|
"googleapis": "^130.0.0",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"http": "^0.0.1-security",
|
"http": "^0.0.1-security",
|
||||||
"js-yaml": "^4.0.0",
|
"js-yaml": "^4.0.0",
|
||||||
|
@ -33,8 +37,8 @@
|
||||||
"telegraf": "^4.3.0",
|
"telegraf": "^4.3.0",
|
||||||
"to-vfile": "^6.1.0",
|
"to-vfile": "^6.1.0",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"typeorm": "^0.2.32",
|
"typeorm": "^0.3.19",
|
||||||
"typescript": "^4.2.3",
|
"typescript": "^4.8.2",
|
||||||
"unist-util-filter": "^3.0.0",
|
"unist-util-filter": "^3.0.0",
|
||||||
"url": "^0.11.0",
|
"url": "^0.11.0",
|
||||||
"vk-io": "^4.8.3",
|
"vk-io": "^4.8.3",
|
||||||
|
@ -46,11 +50,14 @@
|
||||||
"@types/axios": "^0.14.0",
|
"@types/axios": "^0.14.0",
|
||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.11",
|
||||||
"@types/handlebars": "^4.1.0",
|
"@types/handlebars": "^4.1.0",
|
||||||
|
"@types/jest": "^29.5.11",
|
||||||
"@types/node": "^14.14.37",
|
"@types/node": "^14.14.37",
|
||||||
"@types/winston": "^2.4.4",
|
"@types/winston": "^2.4.4",
|
||||||
"@types/yargs": "^16.0.1",
|
"@types/yargs": "^16.0.1",
|
||||||
"@types/yup": "^0.29.11",
|
"@types/yup": "^0.29.11",
|
||||||
"copyfiles": "^2.4.1",
|
"copyfiles": "^2.4.1",
|
||||||
"prettier": "^2.2.1"
|
"jest": "^29.7.0",
|
||||||
|
"prettier": "^2.2.1",
|
||||||
|
"ts-jest": "^29.1.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,4 +23,7 @@ export const defaultConfig: Config = {
|
||||||
group_join: "templates/group_join.md",
|
group_join: "templates/group_join.md",
|
||||||
group_leave: "templates/group_leave.md",
|
group_leave: "templates/group_leave.md",
|
||||||
},
|
},
|
||||||
|
calendar: {
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,6 +6,10 @@ import { validateConfig } from "./validate";
|
||||||
import { getCmdArg } from "../utils/args";
|
import { getCmdArg } from "../utils/args";
|
||||||
import { defaultConfig } from "./default";
|
import { defaultConfig } from "./default";
|
||||||
import { merge } from "lodash";
|
import { merge } from "lodash";
|
||||||
|
import {
|
||||||
|
CalendarKeyFile,
|
||||||
|
calendarKeyValidator,
|
||||||
|
} from "../service/calendar/config";
|
||||||
|
|
||||||
const configPath = getCmdArg("config");
|
const configPath = getCmdArg("config");
|
||||||
const data = fs.readFileSync(
|
const data = fs.readFileSync(
|
||||||
|
@ -21,6 +25,18 @@ const config =
|
||||||
export default function prepareConfig() {
|
export default function prepareConfig() {
|
||||||
validateConfig(config);
|
validateConfig(config);
|
||||||
|
|
||||||
|
if (config.calendar?.keyFile) {
|
||||||
|
try {
|
||||||
|
const key = JSON.parse(
|
||||||
|
fs.readFileSync(config.calendar?.keyFile).toString()
|
||||||
|
) as CalendarKeyFile;
|
||||||
|
calendarKeyValidator.validateSync(key);
|
||||||
|
config.calendarKey = key;
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("tried to parse calendar key, got error", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
config.telegram.templates = {
|
config.telegram.templates = {
|
||||||
help: config.templates.help,
|
help: config.templates.help,
|
||||||
help_admin: config.templates.help_admin,
|
help_admin: config.templates.help_admin,
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { VkConfig, VkEvent } from "../service/vk/types";
|
||||||
import { HttpConfig } from "../api/http/types";
|
import { HttpConfig } from "../api/http/types";
|
||||||
import { LoggerConfig } from "../service/logger/types";
|
import { LoggerConfig } from "../service/logger/types";
|
||||||
import { PostgresConfig } from "../service/db/postgres/types";
|
import { PostgresConfig } from "../service/db/postgres/types";
|
||||||
|
import { CalendarConfig, CalendarKeyFile } from "src/service/calendar/config";
|
||||||
|
|
||||||
export type TemplateConfig = Record<VkEvent, string> &
|
export type TemplateConfig = Record<VkEvent, string> &
|
||||||
Partial<Record<"help" | "help_admin", string>>;
|
Partial<Record<"help" | "help_admin", string>>;
|
||||||
|
@ -14,4 +15,6 @@ export interface Config extends Record<string, any> {
|
||||||
logger: LoggerConfig;
|
logger: LoggerConfig;
|
||||||
templates: TemplateConfig;
|
templates: TemplateConfig;
|
||||||
postgres: PostgresConfig;
|
postgres: PostgresConfig;
|
||||||
|
calendar?: Partial<CalendarConfig>;
|
||||||
|
calendarKey?: CalendarKeyFile;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { vkConfigSchema } from "../service/vk/validation";
|
||||||
import { telegramConfigSchema } from "../service/telegram/validation";
|
import { telegramConfigSchema } from "../service/telegram/validation";
|
||||||
import { loggerConfigSchema } from "../service/logger/config";
|
import { loggerConfigSchema } from "../service/logger/config";
|
||||||
import { dbConfigValidatior } from "../service/db/postgres/validation";
|
import { dbConfigValidatior } from "../service/db/postgres/validation";
|
||||||
|
import { calendarConfigValidator } from "../service/calendar/config";
|
||||||
|
|
||||||
export const templateConfigSchema = object().required().shape({
|
export const templateConfigSchema = object().required().shape({
|
||||||
message_new: string().required(),
|
message_new: string().required(),
|
||||||
|
@ -29,6 +30,7 @@ const configSchema = object<Config>().required().shape({
|
||||||
logger: loggerConfigSchema,
|
logger: loggerConfigSchema,
|
||||||
templates: templateConfigSchema,
|
templates: templateConfigSchema,
|
||||||
postgres: dbConfigValidatior,
|
postgres: dbConfigValidatior,
|
||||||
|
calendar: calendarConfigValidator.optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const validateConfig = (config: Config) =>
|
export const validateConfig = (config: Config) =>
|
||||||
|
|
23
src/index.ts
23
src/index.ts
|
@ -7,6 +7,7 @@ import { HttpApi } from "./api/http";
|
||||||
import { PostgresDB } from "./service/db/postgres";
|
import { PostgresDB } from "./service/db/postgres";
|
||||||
import { PgTransport } from "./service/db/postgres/loggerTransport";
|
import { PgTransport } from "./service/db/postgres/loggerTransport";
|
||||||
import { roll } from "./commands/roll";
|
import { roll } from "./commands/roll";
|
||||||
|
import { setupCalendar } from "./service/calendar/setup";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
|
@ -18,7 +19,27 @@ async function main() {
|
||||||
logger.add(new PgTransport(db, { level: "warn" }));
|
logger.add(new PgTransport(db, { level: "warn" }));
|
||||||
|
|
||||||
const telegram = new TelegramService(config.telegram);
|
const telegram = new TelegramService(config.telegram);
|
||||||
const vkService = new VkService(config.vk, telegram, config.templates, db);
|
const calendar = await setupCalendar(
|
||||||
|
logger.warn,
|
||||||
|
config.calendar,
|
||||||
|
config.calendarKey
|
||||||
|
);
|
||||||
|
|
||||||
|
if (calendar) {
|
||||||
|
logger.info(
|
||||||
|
`calendar service started for ${config.calendarKey?.project_id}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
logger.warn("calendar service not started");
|
||||||
|
}
|
||||||
|
|
||||||
|
const vkService = new VkService(
|
||||||
|
config.vk,
|
||||||
|
telegram,
|
||||||
|
config.templates,
|
||||||
|
db,
|
||||||
|
calendar
|
||||||
|
);
|
||||||
|
|
||||||
const telegramApi = new TelegramApi(telegram, db, config);
|
const telegramApi = new TelegramApi(telegram, db, config);
|
||||||
telegramApi.listen();
|
telegramApi.listen();
|
||||||
|
|
29
src/service/calendar/config.ts
Normal file
29
src/service/calendar/config.ts
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
import { Asserts, boolean, object, string } from "yup";
|
||||||
|
|
||||||
|
export const calendarGroupConfigValidator = object({
|
||||||
|
id: string().optional().email(),
|
||||||
|
enabled: boolean().default(false),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const calendarConfigValidator = object({
|
||||||
|
keyFile: string().optional(),
|
||||||
|
timezone: string().required().default(""),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const calendarKeyValidator = object({
|
||||||
|
type: string().required(),
|
||||||
|
project_id: string().required(),
|
||||||
|
private_key_id: string().required(),
|
||||||
|
private_key: string().required(),
|
||||||
|
client_email: string().required(),
|
||||||
|
client_id: string().required(),
|
||||||
|
auth_uri: string().required(),
|
||||||
|
token_uri: string().required(),
|
||||||
|
auth_provider_x509_cert_url: string().required(),
|
||||||
|
client_x509_cert_url: string().required(),
|
||||||
|
universe_domain: string().required(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type CalendarConfig = Asserts<typeof calendarConfigValidator>;
|
||||||
|
export type CalendarGroupConfig = Asserts<typeof calendarGroupConfigValidator>;
|
||||||
|
export type CalendarKeyFile = Asserts<typeof calendarKeyValidator>;
|
74
src/service/calendar/index.ts
Normal file
74
src/service/calendar/index.ts
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import { calendar_v3, google } from "googleapis";
|
||||||
|
import { KeyFile } from "./types";
|
||||||
|
import { JWT } from "google-auth-library";
|
||||||
|
|
||||||
|
export class CalendarService {
|
||||||
|
private auth!: JWT;
|
||||||
|
private calendar!: calendar_v3.Calendar;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
key: KeyFile,
|
||||||
|
private timeZone: string,
|
||||||
|
private log: (...vals: any) => void
|
||||||
|
) {
|
||||||
|
this.auth = new google.auth.JWT(
|
||||||
|
key.client_email,
|
||||||
|
undefined,
|
||||||
|
key.private_key,
|
||||||
|
[
|
||||||
|
"https://www.googleapis.com/auth/calendar",
|
||||||
|
"https://www.googleapis.com/auth/calendar.events",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
this.calendar = google.calendar({
|
||||||
|
version: "v3",
|
||||||
|
auth: this.auth,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async authenticate() {
|
||||||
|
await this.auth.authorize();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async isEventExist(calendarId: string, eventID: string) {
|
||||||
|
const event = await this.calendar.events.list({
|
||||||
|
calendarId: calendarId,
|
||||||
|
iCalUID: eventID,
|
||||||
|
showDeleted: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return Boolean(event.data.items?.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createEvent(
|
||||||
|
calendarId: string,
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
summary: string,
|
||||||
|
description: string,
|
||||||
|
eventId: string
|
||||||
|
) {
|
||||||
|
if (await this.isEventExist(calendarId, eventId)) {
|
||||||
|
this.log("event already exist, quitting");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.calendar.events.insert({
|
||||||
|
calendarId,
|
||||||
|
requestBody: {
|
||||||
|
summary,
|
||||||
|
description,
|
||||||
|
start: {
|
||||||
|
dateTime: start.toISOString(),
|
||||||
|
timeZone: this.timeZone,
|
||||||
|
},
|
||||||
|
end: {
|
||||||
|
dateTime: end.toISOString(),
|
||||||
|
timeZone: this.timeZone,
|
||||||
|
},
|
||||||
|
iCalUID: eventId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
27
src/service/calendar/setup.ts
Normal file
27
src/service/calendar/setup.ts
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
import { CalendarConfig, CalendarKeyFile } from "./config";
|
||||||
|
import { CalendarService } from "../calendar";
|
||||||
|
|
||||||
|
export const setupCalendar = async (
|
||||||
|
logger: (...vals: any) => void,
|
||||||
|
config?: Partial<CalendarConfig>,
|
||||||
|
keyConfig?: CalendarKeyFile
|
||||||
|
) => {
|
||||||
|
if (!keyConfig) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const service = new CalendarService(
|
||||||
|
keyConfig,
|
||||||
|
config?.timezone ?? "",
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
|
||||||
|
await service.authenticate();
|
||||||
|
|
||||||
|
return service;
|
||||||
|
} catch (error) {
|
||||||
|
logger("can't init calendar", error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
13
src/service/calendar/types.ts
Normal file
13
src/service/calendar/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export interface KeyFile {
|
||||||
|
type: string;
|
||||||
|
project_id: string;
|
||||||
|
private_key_id: string;
|
||||||
|
private_key: string;
|
||||||
|
client_email: string;
|
||||||
|
client_id: string;
|
||||||
|
auth_uri: string;
|
||||||
|
token_uri: string;
|
||||||
|
auth_provider_x509_cert_url: string;
|
||||||
|
client_x509_cert_url: string;
|
||||||
|
universe_domain: string;
|
||||||
|
}
|
|
@ -9,14 +9,14 @@ export interface Storage {
|
||||||
tgMessageId: number,
|
tgMessageId: number,
|
||||||
groupId: number,
|
groupId: number,
|
||||||
channel: string
|
channel: string
|
||||||
): Promise<Event | undefined>;
|
): Promise<Event | null>;
|
||||||
getEventById(eventId: number): Promise<Event | undefined>;
|
getEventById(eventId: number): Promise<Event | null>;
|
||||||
getEventByVKEventId(
|
getEventByVKEventId(
|
||||||
type: VkEvent,
|
type: VkEvent,
|
||||||
eventId: number,
|
eventId: number,
|
||||||
groupId: number,
|
groupId: number,
|
||||||
channel: string
|
channel: string
|
||||||
): Promise<Event | undefined>;
|
): Promise<Event | null>;
|
||||||
createEvent(
|
createEvent(
|
||||||
type: VkEvent,
|
type: VkEvent,
|
||||||
eventId: number,
|
eventId: number,
|
||||||
|
@ -24,7 +24,7 @@ export interface Storage {
|
||||||
channel: string,
|
channel: string,
|
||||||
tgMessageId: number,
|
tgMessageId: number,
|
||||||
text: Record<any, any>
|
text: Record<any, any>
|
||||||
): Promise<Event | undefined>;
|
): Promise<Event | null>;
|
||||||
createOrUpdateLike(
|
createOrUpdateLike(
|
||||||
messageId: number,
|
messageId: number,
|
||||||
channel: string,
|
channel: string,
|
||||||
|
@ -36,12 +36,12 @@ export interface Storage {
|
||||||
channel: string,
|
channel: string,
|
||||||
messageId: number,
|
messageId: number,
|
||||||
author: number
|
author: number
|
||||||
): Promise<Like | undefined>;
|
): Promise<Like | null>;
|
||||||
createPost(
|
createPost(
|
||||||
eventId: number,
|
eventId: number,
|
||||||
text: string,
|
text: string,
|
||||||
vkPostId: number
|
vkPostId: number
|
||||||
): Promise<Post | undefined>;
|
): Promise<Post | null>;
|
||||||
findPostByEvent(eventId: number): Promise<Post | undefined>;
|
findPostByEvent(eventId: number): Promise<Post | null>;
|
||||||
healthcheck(): Promise<void>;
|
healthcheck(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -55,10 +55,12 @@ export class PostgresDB implements Storage {
|
||||||
channel: string
|
channel: string
|
||||||
) => {
|
) => {
|
||||||
return await this.events.findOne({
|
return await this.events.findOne({
|
||||||
|
where: {
|
||||||
type,
|
type,
|
||||||
tgMessageId,
|
tgMessageId,
|
||||||
vkGroupId,
|
vkGroupId,
|
||||||
channel,
|
channel,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,15 +71,19 @@ export class PostgresDB implements Storage {
|
||||||
channel: string
|
channel: string
|
||||||
) => {
|
) => {
|
||||||
return await this.events.findOne({
|
return await this.events.findOne({
|
||||||
|
where: {
|
||||||
type,
|
type,
|
||||||
vkEventId,
|
vkEventId,
|
||||||
vkGroupId,
|
vkGroupId,
|
||||||
channel,
|
channel,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
getEventById = async (id: number) => {
|
getEventById = async (id: number) => {
|
||||||
return await this.events.findOne({
|
return await this.events.findOne({
|
||||||
|
where: {
|
||||||
id,
|
id,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -109,14 +115,18 @@ export class PostgresDB implements Storage {
|
||||||
|
|
||||||
getLikeBy = async (channel, messageId, author) => {
|
getLikeBy = async (channel, messageId, author) => {
|
||||||
return this.likes.findOne({
|
return this.likes.findOne({
|
||||||
|
where: {
|
||||||
channel,
|
channel,
|
||||||
messageId,
|
messageId,
|
||||||
author,
|
author,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
createOrUpdateLike = async (messageId, channel, author, text) => {
|
createOrUpdateLike = async (messageId, channel, author, text) => {
|
||||||
const like = await this.likes.findOne({ channel, author, messageId });
|
const like = await this.likes.findOne({
|
||||||
|
where: { channel, author, messageId },
|
||||||
|
});
|
||||||
|
|
||||||
if (like) {
|
if (like) {
|
||||||
return await this.likes.save({ ...like, text });
|
return await this.likes.save({ ...like, text });
|
||||||
|
@ -131,7 +141,7 @@ export class PostgresDB implements Storage {
|
||||||
};
|
};
|
||||||
|
|
||||||
findPostByEvent = async (eventId: number) => {
|
findPostByEvent = async (eventId: number) => {
|
||||||
return this.posts.findOne({ eventId });
|
return this.posts.findOne({ where: { eventId } });
|
||||||
};
|
};
|
||||||
|
|
||||||
createPost = async (eventId: number, text: string, vkPostId: number) => {
|
createPost = async (eventId: number, text: string, vkPostId: number) => {
|
||||||
|
|
86
src/service/vk/handlers/PostNewCalendarHandler.ts
Normal file
86
src/service/vk/handlers/PostNewCalendarHandler.ts
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
import { addMinutes, differenceInMonths, isBefore, secondsToMilliseconds } from "date-fns";
|
||||||
|
import { NextMiddleware } from "middleware-io";
|
||||||
|
import { WallPostContext } from "vk-io";
|
||||||
|
import { getDateFromText } from "../../../utils/date/getDateFromText";
|
||||||
|
import logger from "../../logger";
|
||||||
|
import { VkEventHandler } from "./VkEventHandler";
|
||||||
|
import { getSummaryFromText } from "../../../utils/text/getSummaryFromText";
|
||||||
|
import { maybeTrim } from "../../../utils/text/maybeTrim";
|
||||||
|
|
||||||
|
|
||||||
|
export class PostNewCalendarHandler extends VkEventHandler {
|
||||||
|
constructor(...props: ConstructorParameters<typeof VkEventHandler>) {
|
||||||
|
super(...props);
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute = async (context: WallPostContext, next: NextMiddleware) => {
|
||||||
|
try {
|
||||||
|
const id = context?.wall?.id;
|
||||||
|
const postType = context?.wall?.postType;
|
||||||
|
const text = context.wall.text;
|
||||||
|
const createdAt = context.wall.createdAt && new Date(secondsToMilliseconds(context.wall.createdAt));
|
||||||
|
const eventId = context.wall.id.toString();
|
||||||
|
|
||||||
|
if (context.isRepost || !id || !this.group.calendar?.id || !this.group.calendar.enabled || !this.calendar || !text || !createdAt) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.isValidPostType(postType)) {
|
||||||
|
logger.info(
|
||||||
|
`skipping wall_post_new for ${this.group.name
|
||||||
|
}#${id} since it have type '${postType}', which is not in [${this.channel.post_types.join(
|
||||||
|
","
|
||||||
|
)}]`
|
||||||
|
);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const summary = getSummaryFromText(text);
|
||||||
|
|
||||||
|
const start = getDateFromText(summary, createdAt);
|
||||||
|
if (!start) {
|
||||||
|
logger.warn(`can't extract date from summary: ${summary}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isBefore(start, createdAt)) {
|
||||||
|
logger.warn(`extracted date was from the past: ${start?.toISOString()}, summary was: ${summary}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const diff = differenceInMonths(start, new Date());
|
||||||
|
if (diff > 1) {
|
||||||
|
logger.warn(`extracted date was too far in a future: ${start?.toISOString()}, summary was: ${summary}`)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: event is deeply in future, like 2 months or more
|
||||||
|
|
||||||
|
const end = addMinutes(start, 15);
|
||||||
|
const description = [this.generateVkPostUrl(context.wall.id), maybeTrim(text, 512)].join('\n\n');
|
||||||
|
|
||||||
|
// TODO: event exist and summary contains "отмен" --> delete post
|
||||||
|
|
||||||
|
this.calendar?.createEvent(this.group.calendar?.id, start, end, maybeTrim(summary, 96), description, eventId)
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('error occurred in calendar handler', error);
|
||||||
|
} finally {
|
||||||
|
await next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if event of type we can handle
|
||||||
|
*/
|
||||||
|
private isValidPostType(type?: string) {
|
||||||
|
return !!type && type === "post";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates urls for postId
|
||||||
|
*/
|
||||||
|
generateVkPostUrl = (postId?: number) =>
|
||||||
|
`https://vk.com/wall-${this.group.id}_${postId}`;
|
||||||
|
|
||||||
|
}
|
|
@ -49,8 +49,7 @@ const PHOTO_CAPTION_LIMIT = 1000;
|
||||||
const POST_TEXT_LIMIT = 4096;
|
const POST_TEXT_LIMIT = 4096;
|
||||||
|
|
||||||
export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
export class PostNewHandler extends VkEventHandler<Fields, Values> {
|
||||||
constructor(...props: any) {
|
constructor(...props: ConstructorParameters<typeof VkEventHandler<Fields, Values>>) {
|
||||||
// @ts-ignore
|
|
||||||
super(...props);
|
super(...props);
|
||||||
this.onInit();
|
this.onInit();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import { NextMiddleware } from "middleware-io";
|
import { NextMiddleware } from "middleware-io";
|
||||||
import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types";
|
import {
|
||||||
|
Calendar,
|
||||||
|
ConfigGroup,
|
||||||
|
GroupChannel,
|
||||||
|
GroupInstance,
|
||||||
|
VkEvent,
|
||||||
|
} from "../types";
|
||||||
import { VkService } from "../index";
|
import { VkService } from "../index";
|
||||||
import { TelegramService } from "../../telegram";
|
import { TelegramService } from "../../telegram";
|
||||||
import { Template } from "../../template";
|
import { Template } from "../../template";
|
||||||
import { Storage } from "../../db";
|
import { Storage } from "../../db";
|
||||||
import { Event } from "../../db/postgres/entities/Event";
|
|
||||||
import logger from "../../logger";
|
import logger from "../../logger";
|
||||||
import safeJson from "safe-json-stringify";
|
import safeJson from "safe-json-stringify";
|
||||||
|
|
||||||
|
@ -20,7 +25,8 @@ export class VkEventHandler<
|
||||||
protected vk: VkService,
|
protected vk: VkService,
|
||||||
protected telegram: TelegramService,
|
protected telegram: TelegramService,
|
||||||
protected template: Template<F, V>,
|
protected template: Template<F, V>,
|
||||||
protected db: Storage
|
protected db: Storage,
|
||||||
|
protected calendar: Calendar | null
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public execute: (
|
public execute: (
|
||||||
|
@ -57,7 +63,7 @@ export class VkEventHandler<
|
||||||
/**
|
/**
|
||||||
* Checks for duplicates
|
* Checks for duplicates
|
||||||
*/
|
*/
|
||||||
getEventById = async (id?: number): Promise<Event | undefined> => {
|
getEventById = async (id?: number) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -68,7 +74,7 @@ export class VkEventHandler<
|
||||||
/**
|
/**
|
||||||
* Checks for duplicates
|
* Checks for duplicates
|
||||||
*/
|
*/
|
||||||
getEventByVkEventId = async (id?: number): Promise<Event | undefined> => {
|
getEventByVkEventId = async (id?: number) => {
|
||||||
if (!id) {
|
if (!id) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@ -84,9 +90,7 @@ export class VkEventHandler<
|
||||||
/**
|
/**
|
||||||
* Checks for duplicates
|
* Checks for duplicates
|
||||||
*/
|
*/
|
||||||
getEventByTgMessageId = async (
|
getEventByTgMessageId = async (tgMessageId?: number) => {
|
||||||
tgMessageId?: number
|
|
||||||
): Promise<Event | undefined> => {
|
|
||||||
if (!tgMessageId) {
|
if (!tgMessageId) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
import { ConfigGroup, GroupChannel, GroupInstance, VkEvent } from "../types";
|
import {
|
||||||
|
Calendar,
|
||||||
|
ConfigGroup,
|
||||||
|
GroupChannel,
|
||||||
|
GroupInstance,
|
||||||
|
VkEvent,
|
||||||
|
} from "../types";
|
||||||
import { VkEventHandler } from "./VkEventHandler";
|
import { VkEventHandler } from "./VkEventHandler";
|
||||||
import { MessageNewHandler } from "./MessageNewHandler";
|
import { MessageNewHandler } from "./MessageNewHandler";
|
||||||
import { StubHandler } from "./StubHandler";
|
|
||||||
import { VkService } from "../index";
|
import { VkService } from "../index";
|
||||||
import { TelegramService } from "../../telegram";
|
import { TelegramService } from "../../telegram";
|
||||||
import { Template } from "../../template";
|
import { Template } from "../../template";
|
||||||
import { PostNewHandler } from "./PostNewHandler";
|
import { PostNewHandler } from "./PostNewHandler";
|
||||||
import { Storage } from "../../db";
|
import { Storage } from "../../db";
|
||||||
import { JoinLeaveHandler } from "./JoinLeaveHandler";
|
import { JoinLeaveHandler } from "./JoinLeaveHandler";
|
||||||
|
import { PostNewCalendarHandler } from "./PostNewCalendarHandler";
|
||||||
|
|
||||||
interface Handler {
|
interface Handler {
|
||||||
new (
|
new (
|
||||||
|
@ -18,13 +24,14 @@ interface Handler {
|
||||||
vk: VkService,
|
vk: VkService,
|
||||||
telegram: TelegramService,
|
telegram: TelegramService,
|
||||||
template: Template<any, any>,
|
template: Template<any, any>,
|
||||||
db: Storage
|
db: Storage,
|
||||||
|
calendar: Calendar | null
|
||||||
): VkEventHandler;
|
): VkEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const vkEventToHandler: Record<VkEvent, Handler> = {
|
export const vkEventToHandler: Record<VkEvent, Handler[]> = {
|
||||||
[VkEvent.GroupJoin]: JoinLeaveHandler,
|
[VkEvent.GroupJoin]: [JoinLeaveHandler],
|
||||||
[VkEvent.GroupLeave]: JoinLeaveHandler,
|
[VkEvent.GroupLeave]: [JoinLeaveHandler],
|
||||||
[VkEvent.MessageNew]: MessageNewHandler,
|
[VkEvent.MessageNew]: [MessageNewHandler],
|
||||||
[VkEvent.WallPostNew]: PostNewHandler,
|
[VkEvent.WallPostNew]: [PostNewHandler, PostNewCalendarHandler],
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { ConfigGroup, GroupInstance, VkConfig, VkEvent } from "./types";
|
||||||
import { API, Updates, Upload } from "vk-io";
|
import { API, Updates, Upload } from "vk-io";
|
||||||
import logger from "../logger";
|
import logger from "../logger";
|
||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import { flatten, has, keys, prop } from "ramda";
|
import { flatten, has, keys } from "ramda";
|
||||||
import { NextFunction } from "connect";
|
import { NextFunction } from "connect";
|
||||||
import { VkEventHandler } from "./handlers/VkEventHandler";
|
import { VkEventHandler } from "./handlers/VkEventHandler";
|
||||||
import { vkEventToHandler } from "./handlers";
|
import { vkEventToHandler } from "./handlers";
|
||||||
|
@ -10,6 +10,7 @@ import { TelegramService } from "../telegram";
|
||||||
import { Template } from "../template";
|
import { Template } from "../template";
|
||||||
import { TemplateConfig } from "../../config/types";
|
import { TemplateConfig } from "../../config/types";
|
||||||
import { PostgresDB } from "../db/postgres";
|
import { PostgresDB } from "../db/postgres";
|
||||||
|
import { CalendarService } from "../calendar";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service to handle VK to Telegram interactions
|
* Service to handle VK to Telegram interactions
|
||||||
|
@ -26,7 +27,8 @@ export class VkService {
|
||||||
private config: VkConfig,
|
private config: VkConfig,
|
||||||
private telegram: TelegramService,
|
private telegram: TelegramService,
|
||||||
private templates: TemplateConfig,
|
private templates: TemplateConfig,
|
||||||
private db: PostgresDB
|
private db: PostgresDB,
|
||||||
|
private calendar: CalendarService | null
|
||||||
) {
|
) {
|
||||||
if (!config.groups.length) {
|
if (!config.groups.length) {
|
||||||
throw new Error("No vk groups to handle. Specify them in config");
|
throw new Error("No vk groups to handle. Specify them in config");
|
||||||
|
@ -120,9 +122,11 @@ export class VkService {
|
||||||
const handlers = this.setupHandlers(group, instance);
|
const handlers = this.setupHandlers(group, instance);
|
||||||
|
|
||||||
handlers.forEach((channel) => {
|
handlers.forEach((channel) => {
|
||||||
keys(channel).forEach((event) => {
|
keys(channel).forEach((event: VkEvent) => {
|
||||||
logger.info(` - ${group.name} listens for ${String(event)}`);
|
logger.info(` - ${group.name} listens for ${String(event)}`);
|
||||||
updates.on(event as any, channel[event].execute);
|
channel[event].forEach((handler) => {
|
||||||
|
updates.on(event as any, handler.execute);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -135,17 +139,19 @@ export class VkService {
|
||||||
private setupHandlers(
|
private setupHandlers(
|
||||||
group: ConfigGroup,
|
group: ConfigGroup,
|
||||||
instance: GroupInstance
|
instance: GroupInstance
|
||||||
): Record<VkEvent, VkEventHandler>[] {
|
): Record<VkEvent, VkEventHandler[]>[] {
|
||||||
return flatten(
|
return flatten(
|
||||||
group.channels.map((chan) =>
|
group.channels.map((chan) =>
|
||||||
chan.events.reduce((acc, event) => {
|
chan.events.reduce((acc, event) => {
|
||||||
const template = new Template(
|
const template = new Template(
|
||||||
prop(event, chan?.templates) ||
|
chan?.templates?.[event] ??
|
||||||
prop(event, group?.templates) ||
|
group?.templates?.[event] ??
|
||||||
prop(event, this.templates)
|
this.templates?.[event]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handler = new vkEventToHandler[event](
|
const handlers = vkEventToHandler[event]?.map(
|
||||||
|
(handler) =>
|
||||||
|
new handler(
|
||||||
event,
|
event,
|
||||||
group,
|
group,
|
||||||
chan,
|
chan,
|
||||||
|
@ -153,9 +159,11 @@ export class VkService {
|
||||||
this,
|
this,
|
||||||
this.telegram,
|
this.telegram,
|
||||||
template,
|
template,
|
||||||
this.db
|
this.db,
|
||||||
|
this.calendar
|
||||||
|
)
|
||||||
);
|
);
|
||||||
return { ...acc, [event]: handler };
|
return { ...acc, [event]: handlers };
|
||||||
}, {} as Record<VkEvent, VkEventHandler>[])
|
}, {} as Record<VkEvent, VkEventHandler>[])
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { API, Upload, Updates } from "vk-io";
|
import { API, Upload, Updates } from "vk-io";
|
||||||
import { WallPostType } from "vk-io/lib/api/schemas/objects";
|
import { WallPostType } from "vk-io/lib/api/schemas/objects";
|
||||||
import { TemplateConfig } from "../../config/types";
|
import { TemplateConfig } from "../../config/types";
|
||||||
|
import { CalendarGroupConfig } from "../calendar/config";
|
||||||
|
|
||||||
export interface VkConfig extends Record<string, any> {
|
export interface VkConfig extends Record<string, any> {
|
||||||
groups: ConfigGroup[];
|
groups: ConfigGroup[];
|
||||||
|
@ -15,6 +16,7 @@ export interface ConfigGroup {
|
||||||
apiKey: string;
|
apiKey: string;
|
||||||
channels: GroupChannel[];
|
channels: GroupChannel[];
|
||||||
templates: Partial<TemplateConfig>;
|
templates: Partial<TemplateConfig>;
|
||||||
|
calendar?: Partial<CalendarGroupConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GroupChannel {
|
export interface GroupChannel {
|
||||||
|
@ -37,3 +39,14 @@ export interface GroupInstance {
|
||||||
upload: Upload;
|
upload: Upload;
|
||||||
updates: Updates;
|
updates: Updates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Calendar {
|
||||||
|
createEvent: (
|
||||||
|
calendarId: string,
|
||||||
|
start: Date,
|
||||||
|
end: Date,
|
||||||
|
summary: string,
|
||||||
|
description: string,
|
||||||
|
eventId: string
|
||||||
|
) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { VkConfig, VkEvent } from "./types";
|
import { VkConfig, VkEvent } from "./types";
|
||||||
import { templateOptionalSchema } from "../../config/validate";
|
import { templateOptionalSchema } from "../../config/validate";
|
||||||
|
import { calendarGroupConfigValidator } from "../calendar/config";
|
||||||
|
|
||||||
const vkChannelEventSchema = yup.string().oneOf(Object.values(VkEvent));
|
const vkChannelEventSchema = yup.string().oneOf(Object.values(VkEvent));
|
||||||
|
|
||||||
|
@ -34,6 +35,7 @@ export const vkConfigSchema = yup
|
||||||
apiKey: yup.string().required(),
|
apiKey: yup.string().required(),
|
||||||
channels: yup.array().of(vkChannelSchema),
|
channels: yup.array().of(vkChannelSchema),
|
||||||
templates: templateOptionalSchema,
|
templates: templateOptionalSchema,
|
||||||
|
calendar: calendarGroupConfigValidator.optional(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
|
502
src/utils/date/__tests__/getDateFromText.real.json
Normal file
502
src/utils/date/__tests__/getDateFromText.real.json
Normal file
|
@ -0,0 +1,502 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "Среда, 10 января. 19:15, СПАРТАК.\nВнезапные КОНЬКИ! Сегодня!\n\nЖдём всех, кто поработал пару дней и устал / отдыхал и устал / не работал и устал. И всех-всех остальных.\n\nНа старте ведущий превратится в тыкву, которая проводит испытание новых коньков. В ужасе, но не без любопытства. Кто научит тормозить – тот герой.\n \nЗ.Ы. Прокат коньков стоит 200 руб/час, + залог 1000р за пару коньков. \nСо своими - 150 руб за всё.",
|
||||||
|
"created": "2024-01-10 08:53:24",
|
||||||
|
"date": "2024-01-10 19:15:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "⚡⚡⚡Молния в ночи 🌘🌘🌘\nСуббота, 6 января. 11:00, на парковке экс-Декатлона.\n\nВсем привет. Завтра с парковки бывшего Декатлона едем катать по правому берегу. Маршрута не придумали, но ехать будем неспеша и с перерывами на еду. Настроение: расслабленное и созерцательное. Дистанция — километров 30.\n\nhttps://map.vault48.org/nochnaya_molniya",
|
||||||
|
"created": "2024-01-05 22:01:22",
|
||||||
|
"date": "2024-01-06 11:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Экспедиционно-исследовательский покат в стиле \"У меня МТВ, еду куда хочу\"\nСуббота, 14 октября, старт в 10:00 \nот Администрации Ленинского района (Станиславского, 6а). \n\nМаршрут: в планах проехаться вдоль реки Тулы до Ярково, далее полями до моря и вернуться вдоль Оби. Примерно так https://map.vault48.org/tula-ob, но это не точно, потому что маршрут не главное, главное процесс.\nПротяженность: 100+ км. \nПокрытие: преимущественно трава и грунт.\nТемп: примерно в третьей или второй зоне ЧСС.",
|
||||||
|
"created": "2023-10-13 00:45:41",
|
||||||
|
"date": "2023-10-14 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Среда, 27 декабря. 19:15, стадион Спартак. КОНЬКИ!\n\nПлан такой:\n1) Встречаемся у раздевалок/на катке.\n2) Катаемся.\n3) Едим.\n\nПрокат коньков стоит 200 руб/час, + залог 1000р за пару коньков.\nСо своими - 150 руб за всё.\n\nХватайте с собой коньки, опознавательные светяшки/велофонарики, друзей, термосы с чаем, велосипеды, наколенники, больную голову, новогоднее настроение, желание поворчать, кружку, ложку, любовь к шаурме, шапку с помпоном, планы на лето, изоленту, конфетки, костюм дракона, аааааааааааа!",
|
||||||
|
"created": "2023-12-26 18:03:02",
|
||||||
|
"date": "2023-12-27 19:15:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Суббота, 23 декабря. 11:00 Первомайский Сквер, центральный фонтан.\nГрязеснеголедовозмущённыепрохожиемамаругаеткат, от 5 до 70км\n\nПо причине небывалого ажиотажа в группе, анонсирую заранее: едем в Академ. \n\nНу как едем, читаем прогноз и пытаемся сложить из нулей и чисел, близких к ним, а так же из НГС-овских \"СРОЧНО: ШТОРМОВОЕ ПРЕДУПРЕЖДЕНИЕ ОТ МЧС: НЕБЕСНАЯ ТВЕРДЬ НАЛЕТИТ НА НЕБЕСНУЮ ОСЬ, ОФИГЕЮТ ВСЕ, НО ОСОБЕННО ДВУХКОЛЁСНЫЕ: ТОП ПЯТЬ НОВЫХ ЖК, КОТОРЫЕ ПОСТРОЯТ НА ВЫЕЗДЕ ИЗ ГОРОДА ПОКА ОНИ ЕДУТ\" хоть какой-то план действий.\n\nТочно известно одно: старт в Первомайском сквере, в 11:00, средняя будет в районе 13-15 км/ч. Дальше, в зависимости от уровня разворачивающегося на улицах города ужаса, действуем по обстоятельствам:\n\n- Едем до Выборной. Проверяем, живы ли все приехавшие;\n- Оттуда двигаем на Первомайку. Проверяем, не сгрыз ли кого по пути кабан;\n- Вдоль железки продвигаемся до Сеятеля. Чекаем, не подпрягли ли кого собирать морошку бабки-работорговцы с \"Юности\";\n- В Академе совершенно точно едим буузы с таким остервенением, будто бы год прожили в Казахстане, где есть только невкусные манты;\n- Дальше — либо на электрон, либо через дамбу, кушать пиццу в Краснообске.\n\nПримерно так. Если желаете присоединиться не на старте, пишите в комментарии или личку — придумаем что-нибудь. Если присоединяться не желаете, тут уж ничего не придумаешь, понимаем, сочувствуем, завидуем.\n\nКороче, лецго, мэйтс, вот элз ви хэв ту ду ин Сайберия?",
|
||||||
|
"created": "2023-12-19 10:02:52",
|
||||||
|
"date": "2023-12-23 11:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Молния. Земля покрылась снегом и льдом, а значит пришло время традиционных зимних посиделок в Вальхалле. Сегодня в 18:30.\n\nВелосипедисты едут на велосипедах (их можно оставить в сенях с панорамными стеклами), но не возбраняется и пешком прийти.\n\nНа всякий случай захвачу с собой шахматы :)",
|
||||||
|
"created": "2023-11-05 14:44:19",
|
||||||
|
"date": "2023-11-05 18:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Понедельник, 6 ноября.\nПокат на выживание до дацана в Нижней Ельцовке.\nСтарт в 12:00 от ГПНТБ.\nРасстояние: от 30 км и до последней бузы.\n\nЦель — попытаться доехать до позной, которая в буддийском дацане, что на Зелёной Горке. \n\nКак говорится, глаза боятся, зубы стучат, а ноги крутят!\nВозможно, мы замёрзнем раньше времени, увязнем в снегу, и нам придётся довольствоваться Хан Бузом на Пединституте.\nИли же доберёмся до Первомайки, но не устоим перед искушением сесть на электричку в сторону дома.\nА быть может, мы встанем на путь, ведущий к прекращению страдания и освобождению от сансары, а потом поедем в Академ, а там и до дома своим ходом махнём.\n\nБудущего никто не знает, и кто я такой, чтобы что-то вам обещать.\nВсё, на что я могу надеяться — это что вернувшись после поката домой я скажу себе: «хотя бы я попытался, чёрт побери, на это, по крайней мере, меня хватило, не так ли?!»",
|
||||||
|
"created": "2023-11-01 13:00:22",
|
||||||
|
"date": "2023-11-06 12:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Этеншн, позор, увага!\n\nЭт самае... Опрос давно назревал. В связи с приближением холодов активность в нашем с вами велосообществе заметно снизится. Те, кто катается зимой, кататься не перестанут, а вот количество публичных мероприятий заметно снизится. Хочется узнать мнение активной части сообщества относительно форматов анонсов в зимний период. Сообщество хоть и велосипедное, но в предыдущие зимы мы не отказывали себе в удовольствии анонсировать кроме велопокатушек такие мероприятия как совместные каталки на лыжах или коньках, походы в бассейны и прочие зимние спортивно-увеселительные мероприятия. Иногда даже умудрялись совмещать их с велосипедами, оправдывая тем самым \"законность\" публикаций. Пока видится несколько вариантов.",
|
||||||
|
"created": "2023-10-30 22:15:31",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "29 октября, воскресенье\nМесто - сквер перед Зоопарком\nВремя - 11-11\n\nСтавшая уже традиционной для меня осенняя покатушка по северным недоостровам вдоль правого берега. Места, доступные в весенне-летний период только на плавсредстве. Есть довольно интересные тропинки и виды на реку. Пока не навалило сугробов, надо ехать, я считаю. Лес, песок, камушки, вода. Темп очень неспешный, с перерывами на чай, отогревание рук и носов и любование видами. По расстоянию - не больше 50, суммарно, я думаю. Чисто размять коленки.",
|
||||||
|
"created": "2023-10-28 20:16:49",
|
||||||
|
"date": "2023-10-29 11:11:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "20 октября, пятница, старт 18:16 с Маркса (за спиной у Покрышкина)\nПримерно 60-70 км, не всё асфальт, в бодром темпе, то есть не вваливая, но и без лишних остановок.\n\nЧто ж, на выходные обещают совсем осеннюю погоду, так что надо успевать покататься до выходных. Пока тепло, сухо и относительно светло, хочу полями доехать до Ленинского, там полюбоваться на морюшко и поехать обратно вдоль реки. На сколько \"вдоль\" зависит от многих параметров.\n\nТемнеет уже около 19:00, так что запасайтесь фонарями, утепляйте колени, скачивайте карты полей, если можете заблудиться.\n\nОчень примерный маршрут: https://map.vault48.org/leninskoe_polyami",
|
||||||
|
"created": "2023-10-20 00:23:00",
|
||||||
|
"date": "2023-10-20 18:16:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Люблю покушать, и ничего не могу с этим поделать. Завтра планирую пообедать в Камне-на-Оби. \n \nСтарт в 7:00 от ГПНТБ. Приглашаются все желающие составить компанию. Погоду обещают отменную. \n \nТрек: https://map.vault48.org/kamen",
|
||||||
|
"created": "2023-10-18 11:00:31",
|
||||||
|
"date": "2023-10-19 07:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Бесподобный Мирза Чаус\n14 октября, суббота\nМесто старта - парковка у Гиганта на Димитровском\nВремя старта - 10-00\nКарта - https://map.vault48.org/chaus100_\n\nТак уж получается, что сообщество вдруг захватила речная тематика, шишечная тематика и многокилометровая тематика. Что ж, оно и понятно... Нет, мы не сговорились, честно. Скажем так, это последний проблеск надежды в попытке успеть ухватить осень за её хитрый рыжий лисий хвост. Лично мне очень хочется повторить маршрут до реки Чаус, полюбоваться скалой в Скале и необычными ландшафтами, характерными только для того направления. Надеюсь, в лайве с нами также поделятся впечатлениями участники всех анонсированных экспедиций. Ухх, что будет... Не забудьте пристегнуть ремни. Мы отправляемся!\n\nРасстояние 100+. Поедем неспешно, но без частых остановок, по возможности минуя оживлённые трассы. Обед в дальней точке маршрута, на берегу или где получится. Так что лучше позаботиться о провианте заранее, магазинов будет мало.",
|
||||||
|
"created": "2023-10-13 18:30:33",
|
||||||
|
"date": "2023-10-14 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Четверг, 05 октября. \nЛевобережный дошикат (он же доширакокат). \n \nСбор/старт: 19:10/19:15 с Маркса (памятник Покрышкину, ну или где-то там). Маршрут средней сложности, 4.5 км в один конец. Если в конечной точке маршрута будет занято, найдем другую свободную поляну. \n \nПри себе иметь доширак (1 штуку) и воду (1 литр), желательно взять горелки и прочие походные котелки для кипячения воды. \n\nПро теплую одежду думаю смысла писать нет, так как все всё понимают, а Кристина всё равно приедет в шортах, так как \"не холодно же\". Фотографию любезно предоставил известный в НВС дошираковед Андрей.",
|
||||||
|
"created": "2023-10-05 09:56:36",
|
||||||
|
"date": "2023-10-05 19:10:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Суббота, 7 октября. 9:35, ст. Болотная. \nПроводы лета. Большой восточный вдольждкат. (+конкурс 🚂)\n \nВ недолгую бытность помощником машиниста я объездил все направления, кроме восточного. Новосибирские бригады не любили этот участок из-за сложного рельефа, оставляя его коллегам из Тайги. Но с велосипеда строгий и требовательный Восток кажется простым, неспешным и созерцательно-манящим, как еноту пылесос. Поехали, выходной желтый, не более двадцати.\n \nТрек: https://map.vault48.org/vostochnyj_vdolzhdkat/ \nЭлектричка: https://t.rasp.yandex.ru/thread/R_6404_9610189_g23_4?departure_from=2023-10-07+07%3A00%3A00&station_from=9610189\n❗В комментариях розыгрыш билета на поезд дальнего следования.\n\nВдольждкат условный: много проселочных дорог и хороших лесных грунтовок. Лишь перегон Ояш-Мошково способен вынуть из п̶а̶р̶к̶е̶т̶н̶и̶к̶а гравийника душу, поэтому там на озере будет привал, берите газ. Уставшие/накатавшиеся электричкуют домой, несгибаемые могут порвать цепь или стереть кассету, но, финишировав, ярко завершат сезон, запомнив лето огнями проходных светофоров.",
|
||||||
|
"created": "2023-10-02 06:00:00",
|
||||||
|
"date": "2023-10-07 09:35:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "1 октября, воскресенье\nМесто - пл.Калинина, под снятыми часами\nСбор/старт - 10-50/11-00\nКарта - https://map.vault48.org/pelengator100\n\nЗдравствуйте, дети. Сегодня я расскажу вам сказку...\n\n\"Выдавим дольку чеснока в 5−6 ложек оливкового масла и оставим минут на 15. Батон нарежем кубиками и обжарим получившиеся сухарики до золотистого цвета в этом масле.\nСыр натрём на мелкой тёрке.\nРазотрём в миске желтки с горчицей, туда же выдавим зубчик чеснока. \nПеремешаем, добавим уксус, лимонный сок и оливковое масло. \nЕще раз хорошенько размешаем. Посолим и поперчим. \nЕдем за салатом Ромэн в Емельяновский (40км).\nСалат Ромэн рвём руками на кусочки и выкладываем на плоскую тарелку. На него кладем чесночные сухарики, поливаем соусом и присыпаем тертым сыром. \nВуаля! Салат Цезарь готов!\"",
|
||||||
|
"created": "2023-09-30 20:54:13",
|
||||||
|
"date": "2023-10-01 10:50:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Супер-пупер молния! (очередная и непредсказуемая)\n\nСегодня (17 сентября), почти сейчас (в 14:00) у часовни со сторону Римского-Корсакова на Монументе Славы. Примерно соточка.\n\nПоявляется у меня традиция в день, когда группа марафонцев едет на Черные камни, ехать до Красного Яра. И сегодня такой день. Маршрут рисовать долго и скучно, нитка: Колыванское шоссе - СО - Красный Яр... Дальше по обстоятельствам. \n\nЗаезд практически асфальтовый, скорость вполне унылая, на красивом бережке на камешках посидеть запланировано.",
|
||||||
|
"created": "2023-09-17 12:00:41",
|
||||||
|
"date": "2023-09-17 14:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "16 сентября, суббота\nВремя старта - 11-00\nМесто старта - Технопарк Академгородка\nТрек - https://map.vault48.org/eHH6ZRBug6hZqm7Rzx4XKM9l\n\nПришло время доехать недоеханнное. Прошлой весной сугробы помешали нам проехать этот замечательный маршрут с полем чудес, бобровыми запрудами и высотой (как это было, можно посмотреть здесь - https://vk.com/album-124752609_283941707). \n\nДа, высоту взять не получилось, тонкие колёсики велосипедов застревали в снегу, коленки мёрзли, запасы еды иссякали, а вечер неумолимо приближался. Уверен, в этот раз будет по другому. Как минимум, снега не ожидается. Надо ехать.\nРисовать некогда, поэтому прицепляю прошлогодний трек с финишем на Планетарии, но финиш будет на ГПНТБ в блинах, конечно же.",
|
||||||
|
"created": "2023-09-15 16:02:06",
|
||||||
|
"date": "2023-09-16 11:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня, 15 сентября\nВремя сбора/старта - 23-40/00-00\nМесто - Нарымский сквер\n\nЧто-то давненько мы не публиковали анонсы о ПИНах. Хотя, честно признаться, катали украдкой. Но тут такое дело. Говорят, сегодня крайний ПИН сезона. Говорят, приедет сам Николай Иванович. Почему бы не созвать старую добрую радио-бригаду, присоединиться к торжественному шествию, пошелестеть цепочками и поутюжить ночной город.",
|
||||||
|
"created": "2023-09-15 12:00:30",
|
||||||
|
"date": "2023-09-15 23:40:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "10 сентября Воскресные Коёны V2\n— Н. Коён — В. Коён —\n\nВ 08:40 собираемся у входа в Пригородный вокзал (Новосибирск-главный) вас встретит [id21726985|Игорь Бабушкин]\n Электричка 6807 отправление 08:59, прибытие 09:45\n\nКоёны всегда разные и непредсказуемые... \nНемного асфальта, в основном лесные — полевые дороги и грейдер. \nПротяжённость всего маршрута ~ 85км, набор высоты больше 800м. \nМаршрут интересный природа красивая, будет немного е*еней, много рельефа, иногда лужи, при желании посетим лысую гору, возможно, будет брод. \nТемп планируется прогулочный, для удовольствия и созерцания, но и не медленный.\n\nЯ вас буду ждать на ст. Крахаль, или подсяду на Ине! \nСтарт на ст. Крахаль, в 09:55, финиш по желанию там же, или раньше по ЖД ветке, или в город своим ходом. \nОбратно очень много электричек, уехать не проблема, можно со станции Издревая. \n \nМагазин будет на старте и В. Коёне (половина маршрута), магазин в Н. Коёне может быть закрыт.\nhttps://map.vault48.org/koeny",
|
||||||
|
"created": "2023-09-08 14:24:10",
|
||||||
|
"date": "2023-09-10 08:40:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Попытка номер два.\n\nСегодня. Старт в 19:32 от ГПНТБ. 80км. Асфальт. Темп бодрый. Две остановки на поесть и передохнуть. Маршрут тот же. Автоматическая отмена поката при граде размером более 0.5см.\n\nhttps://map.vault48.org/04092023_razmyatsya_po_asfaltu",
|
||||||
|
"created": "2023-09-06 11:24:53",
|
||||||
|
"date": "2023-09-06 19:32:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня, 04 сентября. Гоу размяться по асфальту. Погоду, вроде, хорошую обещают. Старт в 19:35 от ГПНТБ. Километров эдак 80-90. Темп бодрый. Две остановки. Надеюсь, без проколов.\nhttps://map.vault48.org/04092023_razmyatsya_po_asfaltu",
|
||||||
|
"created": "2023-09-04 16:17:28",
|
||||||
|
"date": "2023-09-04 19:35:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "В Базой на обед\n2 сентября, суббота\nВремя старта - 8-00\nМесто старта - парковка перед Сибфудом на Краузе\n\nЧто может быть лучше после тяжёлой трудовой недели, когда ты уставший и невыспавшийся? Конечно же махнуть 250км чтобы кошерно постоловаться и отметить праздник кедровой шишки, пообщаться, освободить голову от ненужных мыслей и покрутить педальки.",
|
||||||
|
"created": "2023-09-01 11:53:06",
|
||||||
|
"date": "2023-09-02 08:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Хитрые блины\nПятница, 1 сентября\nВремя - 19-30\nМесто - пл.Калинина, под часами\nКарта - https://map.vault48.org/ozerny40\n\nМикеланджело, Вивальди, Страдивари, Карибальди, Галилео, Петрарка, КЭЧ... Давненько хотел туда заехать, разведать. Немного необычный формат, блины надобно вкушать в середине маршрута, потому что блинная до 21 часа работает. Вивальди до 22 работает, а Петрарка без выходных.",
|
||||||
|
"created": "2023-08-31 19:24:20",
|
||||||
|
"date": "2023-09-01 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "3 сентября, воскресенье\n\nМаршрут на 100км. с копейками\nТемп созерцательный\n\nСобираемся в 9:00 у входа в Пригородный вокзал (Новосибирск-главный)\nЭлектричка отходит в 9:20\nВ 11:12 выходим на станции Изынский и двигаемся до поселка Горный, где зайдём в магазин. Сделаем остановку на Лысой сопке. Следующий магазин будет в Усть-каменке. Пролетаем трассу и перед селом Плотникова уходим на гравийные дороги, ведущие в город. \n\nhttps://map.vault48.org/gornyj_ust-kamenka_razdolnoe\nМаршрут после Плотникова имеет ориентировочное направление)",
|
||||||
|
"created": "2023-08-31 18:08:21",
|
||||||
|
"date": "2023-09-03 09:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "В это воскресенье сбор в 09:45, старт в 09:50, конкретной даты нет, но это ближайшее воскресенье (написано в пятницу)",
|
||||||
|
"created": "2023-08-26 15:47:47",
|
||||||
|
"date": "2023-08-27 09:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Ужин БОМЖа \n23 августа, среда \nВремя старта - 20-00 \nМесто старта - площадка перед Зоопарком \nМаршрут - на момент написания поста редактор маршрутов недоступен, наверное кошка опять перегрызла провода у системника, но ехать недалеко \n \nЛюбите ли вы дошираки так же, как люблю их я? Дело нехитрое. Немного лапши, немного воды. Соль - по вкусу. \nИтак. Вода ушла, Кошкин дом снова доступен, поедем туда. Нужно это дело как следует облапшить, ударить яичной по бездорожью. Параллельно послушаем рассказы бывалых алтайцев о \nдвухнедельных преодолениях и всяческих наслаждениях, если кто-нибудь из них приедет, конечно. \n\nБерите ваши космические ложки, термосы и горелки, чайные сервизы и свежеиспечённые по бабушкиному рецепту шаньги. И воды побольше. Едем, едим, доширакаем!",
|
||||||
|
"created": "2023-08-22 19:35:32",
|
||||||
|
"date": "2023-08-23 20:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Николай Иванович Антипин наносит ответный удар\nСегодня, 18 августа, пятница\nВремя - 20-00\nМесто - ГПНТБ\nМаршрут - https://map.vault48.org/leto75\n\nУхх, ребята! Погодка нынче не балует. Морозная осень подкралась неожиданно и сдавила дождливыми лапами и без того слабое покатушечное горлышко. Птицы больше не поют о любви, пёс по кличке Тоска лежит на половице у дверей в почти неначавшееся лето, усталый от повседневной серости путник ищет потухшим взглядом цветные пятна в непроглядной безмятежности бытия. Доколе?!!\n\nЕсли представить, что так будет всегда. Что мы живём на побережье туманного альбиона, то просто не остаётся вариантов, кроме как облачиться в непромокаемые и непродуваемые доспехи и с тёплым кроликом за пазухой отправиться в двухколёсное вечернее странствие по узкому перешейку между тёмно-серым асфальтом и светло-серым небом.\n\nА ещё, если прочитать наоборот слово \"отель\", то получится почти что-то очень короткое и холодное.",
|
||||||
|
"created": "2023-08-18 10:09:24",
|
||||||
|
"date": "2023-08-18 20:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "⚡ 8 августа (сегодня), ГПНТБ, старт 19.30\n\nБодро прокатиться после работы по асфальту до аэропорта и обратно, блины на финише - по желанию",
|
||||||
|
"created": "2023-08-08 13:42:23",
|
||||||
|
"date": "2023-08-08 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "5 августа, суббота\nБердск. Часть первая.\nМесто старта - ГПНТБ\nВремя старта - 12-00\n\nГоворят, ешё немного и лето всё... Надо ехать промониторить обстановку на текущий момент, что поменялось, что убавилось, что прибавилось в славном городе Бердске. Взглянем на кораблики, крепость, бердскую косу, найдём бердскую тропу. Полный чилаут и расслабон, с тупишками и тупиками, морожкой и купанием, по желанию. Лосизм - 0, каеф - 80.",
|
||||||
|
"created": "2023-08-03 21:09:16",
|
||||||
|
"date": "2023-08-05 12:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "⚡МОЛНИЯ⚡\nСегодня, 3 августа, четверг,\nсбор в 19:30 у дома-книжки на пл. Калинина,\nедем купаться на озеро с тарзанкой в мочищенских дачах!\n\nЛето уже почти закончилось, а мы еще не купались... надо что-то с этим делать.\n35 км асфальта туда-обратно, не забудьте плавательные принадлежности, репелленты и фонарики.\n\nПолундра!",
|
||||||
|
"created": "2023-08-03 16:01:27",
|
||||||
|
"date": "2023-08-03 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Спонтанно и фонтанно\nСегодня, 2 августа, среда\nСбор/старт - 19-50/20-00\nМесто - левый фонтан у ГПНТБ\nМаршрут - от фонтана к фонтану по брахистохроне\n\nЗдравствуйте. В этот знаменательный день мы отправимся в увлекательное путешествие по правобережным фонтанам. Кстати, пока я пишу этот пост, я пытаюсь вспомнить хоть один фонтан на левом берегу, но не могу. \nЧёткого маршрута не будет. Цель - найти самый непопулярный фонтан в самом неожиданном месте. Первооткрывателю - конфетка (шоколадная).",
|
||||||
|
"created": "2023-08-02 11:58:14",
|
||||||
|
"date": "2023-08-02 19:50:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Мошково - Успенка - Седова Заимка\nСуббота, 29 июля\nСтарт в 10-00 с площади Калинина\nМаршрут - https://map.vault48.org/uspenka200\n\nАхой. Над поехать немного прокатиться, размяться, а то суставчики совсем затекли, ю ноу блин. Может трава, а может не трава. Может поля, а может гравия. Может пески, а может трески. Может клещ, а может змещ.",
|
||||||
|
"created": "2023-07-28 22:18:21",
|
||||||
|
"date": "2023-07-29 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "СУПЕР-ПУПЕР-МОЛНИЯ!\n\nСбор/старт в 19:00/19:10 от часовни на Монументе славы (со стороны Римского-Корсакова)\n\nКакая погода, такие и планы: неожиданно, с непонятным маршрутом (что-то в направлении Ярково и Алексеевки), домой хочется вернуться в 22. Темп размеренный.",
|
||||||
|
"created": "2023-07-25 17:43:31",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня, 21 июля, пятница\nВремя - 22-00\nМесто - фонтан у ГПНТБ\nКарты нет, маршрута, в общем-то, тоже\n\nВечер добрый, дорогие велосипедисты! Говорят, пошли грибы-грибочки в лесах. Ну а чего бы не пойти им? Дожди вона как поливают. \n\nНу а мы что? Завтра сыро да мокро, послезавтра мокро да сыро. Так и лето пройдёт, не заметишь, нет. Надо это дело исправлять. Нужно этот вопрос решать. Вот и \"тучные\" карты сулят погоды на поздний вечер. \n\nОтчего бы не катнуть, а? Вспомнить формат Принудительных и Неспешных, поплутать по вечернему городу без маршрута, придумывая цели на ходу. Можно, конечно, и в аэропорт скататься, если будет настроение, а можно вообще никуда не ехать, пообщаться на старте, съесть по финишному блину и разъехаться по домам смотреть цветные фильмы на внутреннем экране век. Решим на месте.",
|
||||||
|
"created": "2023-07-21 19:27:18",
|
||||||
|
"date": "2023-07-21 22:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "20 июля, четверг\nВремя - электричка на 9-15 до ст. Мочище\nМесто - вокзал Новосибирск-Главный, встретимся в первом вагоне или у пригородных касс\n\nПонимаю что в будни и не своевременно, но вдруг кому делать нечего. Короче не проехана мною эта сторона, а ехать надо. Там ещё баржи затопленные посмотреть хочу если получится. Стартую на электричке с главного вокзала до Мочище в 9:15",
|
||||||
|
"created": "2023-07-19 22:10:00",
|
||||||
|
"date": "2023-07-20 09:15:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня! Традиционный микропокат до берега Оби. Старт в 19:00 на Немировича 150. Финиш в 19:15. Потом - афтепати. 18+.",
|
||||||
|
"created": "2023-07-18 14:55:09",
|
||||||
|
"date": "2023-07-18 19:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "В Мочище на бережок\nСегодня, 18 июля, вторник\nВремя - 19-30\nМесто - пл.Калинина, у \"книжки\"\nКарта - https://map.vault48.org/JX9rM9yPY3vTBr5TQgG9cRsc\n\nСовершенно секретно. Протокол #1 испытаний спецсредства X в полевых условиях. Цель - установить потребность в нанесении спецсредства X на трущиеся элементы подвижных узлов мобильных металлоконструкций.\nПрограмма испытаний включает в себя асфальтовый прогон 30+км, прибрежную мороженку и финишный блин.\nРезультаты испытаний будут занесены в настоящий протокол по завершении первого этапа. Конец связи.",
|
||||||
|
"created": "2023-07-18 11:58:02",
|
||||||
|
"date": "2023-07-18 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Ууу-еее! Кароч, после такой ПВД не грех и микроотчёт бахнуть. Есть с чем сравнить, поэтому ответственно заявляю, это была одна из самых интересных и насыщенных поездок. Открыты несколько новых локаций, проговорено \"зажизнь\" несколько часов, выпито несколько литров святой воды. Искатали все дорожные покрытия, попробовали все погоды. На пути в Искитим нещадно палило солнце, в Ложке пошёл дождь с градом, а потом подул лютейший ветер. Жару скрасила купель с ледяной водой, дождь скрасили дивные цветущие поля и бесчисленные запруды, ветер скрасила харизма и доброжелательность бабы Вали, велосипедистки и киоскёрши с Маяка. Кстати, она ждёт всех в гости на копчёное сало. Дороги были сухи и легкопроезжабельны, люди были приветливы и общительны, буузы на Жемчужной вкусны и сытны. Чуйский тракт максимально обескуражен нашим отсутствием, обошлись без него почти. Итого, от дома до дома вышло примерно 170км. Особая благодарность Наталье за компанию и общение. Всем, кто мял диван - зря не поехали.\nАльбом тут: https://vk.com/album-124752609_294837890",
|
||||||
|
"created": "2023-07-16 00:17:02",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Молния ⚡⚡⚡\nСтарт от левого фонтана у ГПНТБ в 20.40\n(при наличии дождя на старте покат отменяется) \n\nМаршрут приблизителен, асфальт не исключетелен, но предпочтителен, лоси и лосины приветствуются, фонари обязательны, скорость непредсказуема\n\nhttps://map.vault48.org/progon_k_vode",
|
||||||
|
"created": "2023-07-15 15:34:00",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "29 июля - 6(13) августа. \nИркутск - Листвянка - Слюдянка. Пешком по Кругобайкальской ж.д. \n \nНастоящий казуал должен сделать в жизни три вещи: прыгнуть без парашюта, пожарить картошку в медвежьей берлоге и найти в озере нейтрино. У меня 2/3, нужно заканчивать. \n \nОбычно люди планируют поездки, но на Байкал я оба раза попадал случайно. Теперь случайно туда съездить можешь ты. Поездом в Иркутск, теплоходом до котов ([club6327910|Больших]), [club97669140|байкальский музей] и нерпинарий в [club23741236|Листвянке], затем паромом в порт Байкал. 80км Кругобайкалки придется идти по шпалам, там редко где можно проехать. Но тридцать восемь вековых тоннелей не дадут тебе заскучать в пути, а камнепады и медведи скрасят томность вечеров. \nНа все неделя с небольшим. Если останется время/желание, из Слюдянки можно прокатиться вдоль другого берега, в сторону Улан-Удэ, на фестивали [club92277664|Байкал - Шаман] или [club40590705|Байкальский ветер]. \n \nО КБЖД: https://ru.wikipedia.org/wiki/Кругобайкальская_железная_дорога \nПо КБЖД на велодрезине: https://www.youtube.com/watch?v=2GJALMjHFaE&t \nПодробнее о тоннелях: http://kbzd.transsib.ru/Graph/article09.htm \nНерпинарий: https://vk.com/video-220418443_456239507 \nБайкальский нейтринный телескоп: https://www.youtube.com/watch?v=3WFJrGyQckQ \nМедведь: https://youtu.be/4uM8eNcjrnc?t=311 \nКамнепады: https://youtu.be/2GJALMjHFaE?t=2391 \n \nВ Иркутске, Листвянке и Слюдянке живем в квартирах/гостевых домах, на пешеходном участке в палатках. Для участия пиши в [id11412570|личку].",
|
||||||
|
"created": "2023-07-15 06:00:00",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "15 июля, суббота \nВремя - 10-30 \nМесто - Утиное озеро в Академе (Воеводского, 1) \nКарта - https://map.vault48.org/gulag120 \n \n \nДоставайте из своих чуланов запылившиеся в нафталине мэтэбэ (ударение на второй слог) и гравелли. Неспешно поедем полировать морозовские чернозёмы, суглинки, глинозёмы и говнолины. По задокументированным свидетельствам очевидцев после дождей там вполне мерзко, то что нужно. Далее - в Искитим, посетить (или хотя бы посмотреть на) \"Церковно-исторический музей памяти жертв политических репрессий\" и набрать живой воды в источнике. С дождём я договорился. Всё нормально, дождь будет. Но при нынешних температурах он, скорее, освежающая благость, ниспосланная с небес дабы окропить наши бренные тела. \n\nПосле успешного завершения трипа, всем, кого пустят в таком виде в уважаемое заведение, протокол велит заправить кишки свежепропаренными буузами на Жемчужной. Шаг влево или вправо - побег, лосизм - расстрел на месте. Да пребудут с вами мембраны и ассейверы. \n \nНиже выдержка из описания музея. \n \n\"Самое известное место паломничества в Новосибирской области. Храм был построен на месте, где в советские годы, с 1929 по 1956 год, был расположен Особый лагерный пункт №4 Сиблага. Он считался одним из самых жестоких в России. По легенде, на месте массового расстрела священнослужителей забил целебный родник. \n \nПо воспоминаниям репрессированных, заключенные боялись перевода под Новосибирск. Вместе с уголовниками, осужденными за тяжкие преступления, там отбывали наказание политические заключенные. \n \nВ экспозиции представлены документы и личные вещи узников Сиблага, среди которых было немало священников, доносы, на основании которых невинные люди попадали за колючую проволоку. Чудом сохранившиеся фотографии дают представление о том, в каких условиях жили и работали заключенные. Здесь можно увидеть вагонетку, на которой руками поднимали наверх добытый в карьере известняк. В одной из комнат выполнена реконструкция камеры изолятора для узников лагерного пункта.\"",
|
||||||
|
"created": "2023-07-13 18:40:13",
|
||||||
|
"date": "2023-07-15 10:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "🦆🦆🦆КРЯ-КРЯ, ПОКАТ ОТМЕНЕН, КРЯ-КРЯ🦆🦆🦆\n\nВторник, 11 июля, 19:30, Нарымский у фонтана\nТолмачёво, 40км, бодренько, по асфальту, но МТБ-шно.\n\nНет времени объяснять, в аэропорту пылятся самолёты, надо сгонять, пропылесосить их взглядами, и — обратно. Едим тут, там и на финише, как обычно.\n\nЛосизм ограничен наличием 26-ых мтбшных колёс у ведущего, но если кто приедет со свежебритыми ногами на своей тонюсенькой оглобле и решит прогреть до горизонта, никто за эти бритые ноги хватать не будет и даже пальчиком вот так вот (показывает) не сделает, все взрослые люди, все всё понимают.\n\nЗамыкающего мы потеряли ещё на прошлом покате, так что если что, каждый сам за себя.\n\nАнонс написан разумным сыром, все персонажи выдуманы, но совпадения не случайны.\n\nКарта: https://map.vault48.org/narymskij-tolmachevo",
|
||||||
|
"created": "2023-07-11 12:58:53",
|
||||||
|
"date": "2023-07-11 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "СЕНЧАНКА\n9 июля, воскресенье \nСбор/старт - 11-50/12-00 \nМесто - Сквер \"Водник\" возле НГАВТ \nКарта - https://map.vault48.org/senchanka111\n \nСкромная подставная организация-однодневка под названием ПОО \"Клуб Анонимных Лосей\" набирает группу добровольцев для совместного снижения скорости. Основные требования к кандидатам - возможность принимать по 100км перед завтраком, 200км перед обедом и 300км перед ужином без ущерба для здоровья, иметь накат к началу сезона не менее 1500 морских миль и среднюю скорость 18 узлов. \n \n- Давайте немного познакомимся. Пожалуйста, пару слов о себе и как вы узнали о клубе? \n \n- Да, здравствуйте. Мне достался номер 5 и я лось. Точно не помню, как это случилось. Я начал замечать за собой некоторые странности. Например, после покатушки я стал возвращаться домой засветло. В моём гардеробе начали преобладать обтягивающие вещи. Однажды жена обнаружила в кармане моей джерси гель. Она... она сказала: \"Выбирай, я или гель\". И вот я здесь. \n \n- Спасибо, Номер 5. Давайте похлопаем и продолжим дальше. Вы молодец. \n \n- Эм... Привет. Я Номер 2. Я пришёл сюда с другом. Он сказал, меня это тоже касается. Он сказал, будет интересно. Он сказал, что мне будет хорошо и вообще всем будет хорошо. А мне нравится когда хорошо. И вот я здесь. \n \n- Добро пожаловать в клуб, Номер 2. Кто следующий? Номер 3.\n \n- Вперёд, вперёд! Гнать, гнать! Лось лосиный в лося лосить. Лосила, лосю, буду лосить. Лосины, лосятина, лось лось лось... \n \n- Достаточно. Поприветствуем Номера 3. Кто хочет продолжить? Вам слово. \n \n- Здарова, бл.. Номер 1 я. Ваш клуб нашёл сам, пришёл сам, всё сам. У меня по жизни всё ровно, проблем нет. Хочу чего-то нового, ну, попробовать проехать как эти, как их там.. Петушки-Слабачки. Давай учи, мать, как катать. \n \n- Всё будет, всё будет. Наберитесь терпения. Так, а это кто тут у нас? С рогаами...\n \n- Гуманоиды, вы чё тут устроили, а?!",
|
||||||
|
"created": "2023-07-08 16:52:43",
|
||||||
|
"date": "2023-07-09 11:50:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня, 7 июля \nМесто - ГПНТБ \nВремя - 22-30 \n \nВсех благ! Николай Иванович Антипин не без труда вышел из голодовки и готов продолжать простые движения. На этот раз ему маячат маячки. Нет времени объяснять. Всё будет #дождянебудет.",
|
||||||
|
"created": "2023-07-07 21:41:35",
|
||||||
|
"date": "2023-07-07 22:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Вторник, 4 июля, 19:00, Нарымский сквер, у центрального фонтана.\n30 км, треть из которых не факт что проезжаемая.\nhttps://map.vault48.org/na_vecher\n\nАс-салям алейкум, православные! На пару дней проездом в вашем городе, хочу посмотреть, как вы тут живёте. Нагуглил, что тут у вас есть интересного, нарисовал карту.\n\nПосле дождей не везде будет просто, но мы справимся (наверное). Будут овраги, кладбище и, что самое ужасное, улица Краузе. Финиш будет там, где надоест ехать.",
|
||||||
|
"created": "2023-07-03 14:02:39",
|
||||||
|
"date": "2023-07-04 19:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня, 3 июля, понедельник\nСтарт в 20-00 на пл. Калинина у \"книжки\"\n\nНебольшой прогон по городу, на пару часов. Заехать кое-куда, завезти кое-что. Примерный маршрут: пл.Калинина - Вертковская - Бугринка - Планетарий - ГПНТБ",
|
||||||
|
"created": "2023-07-03 12:00:29",
|
||||||
|
"date": "2023-07-03 20:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Неспешная асфальтовая \nСегодня, 1 июля, суббота \nСбор/старт - 11-20/11-30 \nМесто - Парковка у Гиганта на Димитровском \nРасстояние - 120км \nКарта - https://map.vault48.org/shosse120 \n \nПоеду просто прокатиться и размять косточки. Темп размеренный, без гонок. Может быть дождик, но несильный, судя по прогнозам. Думаю, не помешает.\n\nМаршрут: Свинодановка - СО - Толмачёво - Верх-Тула - ОбьГЭС - Советское - Бугринка - ГПНТБ",
|
||||||
|
"created": "2023-07-01 09:32:54",
|
||||||
|
"date": "2023-07-01 11:20:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Лучше поздно чем, ну вы сами всё знаете😁\nВидос запилил с последнего пвд по 5 водопадам Новосибирской области. \nПоход получился кайфным. \nПогода была разная. \nКоманда подобралась классная!\n\nКлещей видели, комаров нет. Вот совсем нет этих сосущих паразитов🤷♂️ а вот клещей остерегайтесь - парочку сняли. Один укусил.\n\nВодопады прекрасны! Это и ежу понятно. \nТе кто зассал или по каким то причинам не поехал - завидуйте, было чертовски офигенно🤟👍",
|
||||||
|
"created": "2023-07-01 09:05:30",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня, 26.06. Старт в 19.30 ГПНТБ, у левого фонтана.\n\nПо асфальту через первомайку, кольцо в Кольцово, на обратном пути тренировка в горочку (если очень грязно, возможно через Планетарий), на финише блины по желанию.\nДо Кольцово темп побыстрее, дальше как пойдет, очень хотелось бы вернуться до темноты.\n\nМаршрут: https://map.vault48.org/kolco_v_kolcovo",
|
||||||
|
"created": "2023-06-26 12:42:56",
|
||||||
|
"date": "2023-06-26 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Молния! \nСегодня, 25 июня, воскресенье \nМесто - Аллея связистов им. Никулина\nСбор/старт - 15-50/16-00 \nМаршрут - https://map.vault48.org/GRtFzGHGoRbpJtGdJjiMIFaH \nРасстояние - 70+ \n \n \nУмиротворяюще-расслабляющий, молниеносно-позевающий вечерний лайт. Без потерь, без захода в четвёртую зону, без выхода в открытый космос, без купюр.",
|
||||||
|
"created": "2023-06-25 13:52:35",
|
||||||
|
"date": "2023-06-25 15:50:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "МОЛНИЯ!!!\n\nСегодня, 25 июня, 13:00/13:15, за спиной Покрышкина на Маркса\n\nРасстояние около 150 км, темп спокойный, но без тупняков.\n\nПоеду искать мишины очки, оставленные где-то в траве Колыванского шоссе. А раз такое дело, то можно и в Скалу заехать поесть. Вернуться планирую через Толмачево.\n\nМаршрут очень примерный: https://map.vault48.org/marksa-skala-so",
|
||||||
|
"created": "2023-06-25 08:06:10",
|
||||||
|
"date": "2023-06-25 13:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "ПВД ДРУГИЕ 5 ВОДОПАДОВ \n24-25 июня\n\nСбор у касс пригородного вокзала Нск-Главный в 8:30\nСтарт на ст. Льниха в 10:30\n\n120 км на два дня, это не означает что покатушка пройдёт на лайте. Будут участки ебеней с элементами жесткого порно. Так что если вы не готовы лезть через траву, по еле видной дороге, в то время как вас поедают милиарды комаров и других тварей, то этот покат не для вас - езжайте асфальтом. Но на асфальте скучно, а этот покат будет офигенский!!!\n\nНочуем в палатках, пожалуй на самом классом водопаде, где нас ждет \"джакузи\" и девочки😁. \n\nФиниш в Тогучине.\n\nПредупреждаю что в покате возможно вы будете покусаны клещами - поэтому либо оставайтесь дома, либо примите пожалуйста все меры предосторожности. И это нихера не шутка. Этих тварей в этом году слишком много. \n\nВсе вопросы в комменты, или мне в личку.",
|
||||||
|
"created": "2023-06-22 09:40:31",
|
||||||
|
"date": "2023-06-24 08:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "\" 4 М О С Т А \" \n23 июня, пятница \nСбор/старт - 20-15/20-30 \nМесто - площадь Маркса, где-то под забором \nМаршрут - https://map.vault48.org/4mosta \n \n \n...Укутайтесь пледом. Выключите свет. Закройте глаза. Сосредоточьтесь на дыхании. Дышите медленно и глубоко. Прислушайтесь. Слышите?! Эти звуки... Цоканье шипов, шуршание шоссейных трубочек, хруст поясниц. Чувствуете?! Встречный поток прохладного ночного воздуха начинает ласкать лицо, уши, шею. Лёгкий бриз постепенно превращается в шторм, шум заглушает посторонние звуки, заполняет сознание. Теперь можно открыть глаза. Видите?! Бескрайние просторы, манящие своей неизведанностью и первозданной чистотой. Липкая темнота, медленно сползающая по стенке космического пузыря. Сердце начинает биться чаще, стучать, вибрировать. Звёзды плавно осыпаются на спины холмов, их хвойную шерсть, бросая кроткие отблески в хрустальную глубину озёрных впадин. Впереди ещё три моста. Позади только обгоняющие время тени... \n \nКрутим праздновать день рождения самого дружелюбного, уютного и лампового велосообщества. 7 лет социальному организму, объединяющему влюблённых в велосипед, вдохновляющему влюблённых в жизнь и противостоящему влюблённым в себя. Как всегда, правобережные едут на старт через Октябрьский мост, левобережные им же финишируют. Покатушка состоится при любой погоде. Иншалла.",
|
||||||
|
"created": "2023-06-21 22:15:14",
|
||||||
|
"date": "2023-06-23 20:15:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Как описать мой поход на Суенгу?\n- Кароче, ребята, было очень много опасных ситуаций...\n- И комары по мне стреляли...\n- И я упал в лужу нафиг, блин...\n- И я валялся там нафиг...\n\nА если серьёзно, то один мой соучастник ПВД сказал мне, что ему хватило походной романтики. Так что с этого момента, в любом моём походе будет всегда присутствовать \"идеальная, идеалистическая утопическая идея\": Наслаждаться походом так сильно, чтобы захотелось превратить любой момент похода в вечность, чтобы каждый человек мог сказать: - в данный момент я счастлив и хотел бы, чтобы этот поход не заканчивался или хотя бы не исчезал из памяти, как многие другие походы и оставался вечным воспоминанием о приятном, греющим душу. Вообщем как-то так.\n\nА вот и фотки https://vk.com/album-124752609_294147093",
|
||||||
|
"created": "2023-06-21 20:23:06",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Пятница, 16 июня,\nв 20:00 от дома-книжки на Калинина.\nПредпин до Раздольного через Краузе.\n\nРасстояние: 50 км, немного грунтов\nКарта: https://map.vault48.org/helvetica\n\nАдминистрация села Раздольное решила запретить путешественникам делать фото населенного пункта, так как считает местные виды слишком красивыми. Об этом сообщает местный офис по туризму. \n \nЗа введение такого запрета совет села Раздольное, расположенного к востоку от Новосибирска, проголосовал в понедельник, 29 мая. «Научно доказано, что красивые фото из отпуска, размещенные в соцсетях, делают тех, кто их просматривает, несчастными, так как эти люди сами не находятся в том месте», — отмечается в сообщении туристического офиса.\n\nМэр населенного пункта Швачунов Валерий Семенович призвал туристов приезжать в Раздольное, чтобы самим убедиться в красоте видов. Офис по туризму уже удалил снимки деревни из своих аккаунтов в социальных сетях. Тем, кто будет фотографировать Раздольное, отныне грозит штраф в триста рублей.\n\nИсточник: https://lenta.ru/news/2017/05/31/swiss_village/",
|
||||||
|
"created": "2023-06-15 23:45:52",
|
||||||
|
"date": "2023-06-16 20:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Вечерка через Криводановку\n\nНебольшая кардиотренировка после рабочего дня среди недели) \n\nСтарт 15.06 в 18.45 с пл.Калинина от здания-Книжки\nТемп средний 23 км/ч, местами 28-30 км/ч\nОстановки только при необходимости, планирую приехать на финиш до 22.30, пл.Ленина.\nДистанция ~71 км\n\nМаршрут\nhttps://map.vault48.org/krivodanovka",
|
||||||
|
"created": "2023-06-14 23:22:11",
|
||||||
|
"date": "2023-06-15 18:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Челнок до Суенгинского водопада с другими водопадами и пещерами\n17-18 Июня (ПВД на 2 дня)\nСТАРТ: Новосибирск главный, у пригородных касс в 6:20\nЭЛЕКТРИЧКА: \"6605/6007 Новосибирск - Барнаул\" (6:53)\nВТОРОЙ СТАРТ: Черепаново, в 9:37\n\nНи разу не был на Суенгинском водопаде и решил, что выходные этой недели - лучшая возможность исправить это. Темп указывать больше не буду. Просто скажу, что всех жду и никого не бросаю. В Черепаново, в магазины заходить не будем. Сразу стартуем до кафешки возле Огневой заимки. В первый день по плану будет Прямской и Пеньковский водопад, а так же Барсуковская пещера. и конечно же ночёвка у суенгинского водопада. Намотаем около 105 км.\nВо второй день будет всего 80 км по прямой до Черепаново. На обратном пути по желанию после Лихановского можно свернуть вправо, сторону станции Линёво, дабы зацепить Медведский и Родиховский водопад.\n\nКстати, вы знали, что река Суенга золотоносная? Кто будет искать золото? Я буду)\n\nВопросы как всегда в ЛС\n\nhttps://map.vault48.org/chelnok_na_suenginskij_vodopad/edit",
|
||||||
|
"created": "2023-06-12 23:26:32",
|
||||||
|
"date": "2023-06-17 06:20:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Асфальт, грунты, песочек дубль два\nВоскресенье 11.06.2023\nВыезд в 9:00\nСтарт напротив статуи Покрышкина, который на площади Маркса, а не который метро.\n\nhttps://map.vault48.org/asfalt_grunty_i_pesok\n\nВозможно незначительные правки по мере продвижения. Возможно увеличение километража до 100+)))",
|
||||||
|
"created": "2023-06-10 11:42:23",
|
||||||
|
"date": "2023-06-11 09:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "11 - 12 июня. Каракан: туда и обратно. \nНовосибирск-Главный. Поезд уезжает в 06:53. (или Бердск, жд вокзал, 07:55 утра)\n\nСосны как пальмы. Кто-то ходит и стучит. Это не крысы, это к соседям. Мне надо отдохнуть, я очень устал. У моря он заснул… Или это был не сон? Просто вернулись те, кто раньше сделал его жизнь такой, в которую он попал по своей воле? Треск дров говорил, что огонь горит в самом деле. Но он больше не хотел спать, и сон улетучился.\n\nЖрюба, спасибо тебе… Следующий день он мало помнил, только какие-то туманные образы, словно он смотрел цветной фильм, идущий в записи.\n\nЗ.Ы. Маршрут, как и текст поста написаны искусственным интеллектом. Автору затеи нужно отдохнуть. \n\nКак поедем- не знаю, но точно знаю, как жирно поедим на берегу Обского.\n\nhttps://map.vault48.org/k_moryu",
|
||||||
|
"created": "2023-06-09 23:18:02",
|
||||||
|
"date": "2023-06-11 06:53:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Суббота 10.06.2023\nВыезд в 9:00\nСтарт напротив статуи Покрышкина, который на площади Маркса, а не который метро.\n\nhttps://map.vault48.org/asfalt_grunty_i_pesok\n\nUPD: одробности в комментариях к предыдущему посту)))",
|
||||||
|
"created": "2023-06-09 13:40:18",
|
||||||
|
"date": "2023-06-10 09:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Всём добрый день. Завтра, Суббота, 10 июня 10:00 выезжаю по маршруту. \nСтарт на Золотой Ниве, от магазина \"Пятёрочка\", ул. Бориса Богаткова 221.\n\nСкорость средняя примерно 25 км. Всегда катался один. Есть желание катануть с компанией.",
|
||||||
|
"created": "2023-06-09 10:35:11",
|
||||||
|
"date": "2023-06-10 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Го сегодня (08.06) искупаться в озере Старица?) \nСтарт у \"Книжки\" на пл.Калинина в 18.30\nЕдем по Северному объезду, купаемся и обратно)\n\nhttps://map.vault48.org/starica",
|
||||||
|
"created": "2023-06-08 10:24:14",
|
||||||
|
"date": "2023-06-08 18:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Вот мы и съездили Обскую недоСветку, спасибо всем, кто помогал составлять маршрут и тем, кто приехал. Таскали велосипеды на своём горбу по пескам в 30 градусов. Больше не поеду в этот ваш Каракан никогда в жизни)\n\nКараканский водопад был найден одним из Андреев. В результате низкого уровня воды в Обском водохранилище, водопадик стал ручейком.\n\nВ целом маршрут живописный, но очень сложный и я снял с себя 2 клеща, но меня не укусили. Ставлю этому маршруту 7-8 баллов сложности из 10.\n\nФото я решил обработать в \"киношном стиле\". Позже закину в альбом ещё фото в другом стиле. Спасибо за идею Александру.\n\nhttps://vk.com/album-124752609_293807818",
|
||||||
|
"created": "2023-06-07 00:26:08",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "9-12 июня, пятница - понедельник. \nТягун. \n \nАбсолютный хит прошлого года по борьбе с серыми буднями. Ночуем в гостевом доме горного поселка, катаем радиалки по окрестностям. Укатанные грунтовки, лес до небес, суп у горной речки и незнакомая горожанину фауна - повод вынуть мозг из телеги хотя бы на праздники. Войны и любви нынче хватит на всех (©), и пыли, и пустого места. А в Аламбае псы с осени не кормлены. \n \nДетали и треки в прошлом анонсе: https://vk.com/pogonia_nsk?w=wall-124752609_29555 \nФото: https://vk.com/album11412570_286803290 \nВыезд вечером в пятницу, для участия пиши в [id11412570|личку]. \n \nЕхать долго. Если тебе тяжело в электричках, скучно наедине с собой и нужен какой-то внешний шум – не мучай себя и окружающих, поищи другое мероприятие.",
|
||||||
|
"created": "2023-06-06 06:00:00",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Воскресенье 04.06.2023\nСтарт Нарымский сквер 13:00\nФиниш Нарымский сквер\n\nНебольшая покатушка на пляж покупаться и позагорать через ботанический сад и заельцовский лес, всего 22,4 км.\nТ.к. дистанция относительно небольшая темп предполагается бодрый, цель ощутить атмосферу летнего хвойного леса, хорошенько прогреться с последующим окунанием своей тушки в освежающую воду Оби (в районе 15 градусов сейчас +- https://seatemperature.ru/current/russia/ob-novosibirsk-sea-temperature), затем греемся на пляже под солнцем (можно взять плед, кто брезгует лежать на песке), жуем интеллигентно фрукты, пьем соки, бухаем пиво и едем обратно в город через заельцовский до Нарымского сквера. \nhttps://map.vault48.org/vachchva/edit",
|
||||||
|
"created": "2023-06-03 21:45:14",
|
||||||
|
"date": "2023-06-04 13:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "😃😃Понедельник, 05.06.2023☺☺☺\nСбор у ТЦ \"Роща\" (напротив ПКиО \"Берёзовая Роща\") - 19:00\n\n😎😎Всем доброго времени суток😎😎\n😉😉Представляю вашему вниманию😉😉\n🚵♂🚵♀🚴♀Приличный покат на 36км🚴♀🚵♀🚵♂\n\nhttps://map.vault48.org/tri_raza_v_les_cherez_tri_ozera\n\nТемп максимально прогулочный по формуле Расстояние/Опыт/Силы\nВ покат входят : асфальт🚲, просёлки🏘, грунт🏕, лес🌳🌲🌿 и озёра🌊🌊🌊\nДвижение по принципу \"Своих не бросаем, но и за горизонт не лезем\"😉😁😎\n\n😃Финишируем на площадке Заельцовского бора😉😉😉",
|
||||||
|
"created": "2023-06-02 23:31:33",
|
||||||
|
"date": "2023-06-05 19:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Воскресение, 4 июня. 9:10 станция Евсино (электричка с главного в 6:53).\nМраморный карьер, 70км.\n\nДаровчик, мы с братом в 2018 так и не прорвались на мраморный карьер, не пустила \"охрана\", поэтому 4го Июня штурмуем его с обратной стороны, затем заедем в Старый Искитим чего-нибудь докупим и поедем на Беловский водопад, а там уже на последнюю электричку из Линево. \n \nСтарт 4го июня со станции Евсино в 9:10, т.к. все подсаживаются с разных станций. Я на Ельцовке. Кто с Главного поедет, кооперируйтесь самостоятельно, электрон с него в 6:53, встретимся на Евсино.\n\n70км, едем мы не быстро, но успеть на обратную липу надо) На Мраморном карьере стояночка на фотки, мб перекус. Вдоль Берди возможно искупаемся (вода холодная еще), жара будет, берите крем да и репеленты с водой. \n \nЕсли не успеем на последнюю электричку из Линево, поедем до Искитима ждать уже реально последнюю)))) \n \nЕсли есть желающие, присоединяйтесь, сообщайте, чтобы я понимал сколько народу. \n\nhttps://nakarte.me/#m=11/54.53837/83.44226&l=O/W&nktl=HaSxxnR-3MgaT6wlrY9E5g",
|
||||||
|
"created": "2023-06-01 11:56:07",
|
||||||
|
"date": "2023-06-04 09:10:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "WOKадем\nЧетверг, 1 июня, старт в 19:30 \nот ГПНТБ, фонтана \"Речные цивилизации Сибири. Бия\" \nМаршрут: https://map.vault48.org/wokadem",
|
||||||
|
"created": "2023-05-31 20:48:46",
|
||||||
|
"date": "2023-06-01 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "ПВД на 2 дня (3-4 июня)\n220 км\nТемп от 20 км/ч\nСтарт на Главном Вокзале в 4:40, на Электричку до Бердска в 5:14\nДля стартующих со станции Бердск старт в 6:11\n\nОбская ПолуСветка - это один из популярных и хайповых туристических маршрутов для велосипедистов и автомобилистов. По крайней мере, каждый из нас слышал про караканский бор и как там красиво. Но знали ли вы, что этот маршрут, может быть гораздо интереснее, чем кажется? Что в Нижнекаменке есть озеро и водопад, что в 20 км от неё же есть Абрашинский карьер, на который можно съездить, разбив лагерь в Нижнекаменке , сбросив снарягу и налегке доехать до этого карьера. Что ехать по пыльным грунтовкам и асфальтированным трассам, игнорируя более живописные полевые дороги, не обязательно. Я бы хотел всё время этого похода не касаться асфальтированных дорог и лично для меня это практически идеальное воплощение по настоящему интересного туристического маршрута.\n\nНа карте указаны места ночёвки и остановок в деревнях на перекусы. В целом деревень много. Все на самообеспечении, первый день доезжаем до Нижнекаменки, разбиваем лагерь, спим, встаём в 7:00 на паром до Ордынки и катим домой.\n\nОстальные вопросы в ЛС\n\nhttps://map.vault48.org/koncept_vokrug_morya_obskogo_",
|
||||||
|
"created": "2023-05-31 06:14:08",
|
||||||
|
"date": "2023-06-03 04:40:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Видео зарисовка с прошедшего в эти выходные ПВД 5 водопадов. \nБыло офигенно!",
|
||||||
|
"created": "2023-05-30 14:26:39",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Пятница, 2 июня. 21:30, ст. Искитим. \nНочная пригородная. \n \nПроблема среднего возраста в отсутствии свежих эмоций. Там был, это видел, скучно. Идите лесом. И Алтай ваш к Кукуям, скуч-но – думает заматеревший веловолк в очередной черепановской электричке. \nИ вдруг выходит на полузаброшенной, незнакомой платформе. Оказывается, там СНТ, вон за тем перелеском птицефабрика, в деревушке Шадрино есть горнолыжка, а в Искитиме парк с велодорожками. Надо покружить еще, ведь и в Ложке́ никогда не был, и высоту башен замка А̶л̶о̶г̶о̶ ̶Щ̶и̶т̶а̶ электродного завода не представлял. И плевать, что поздно. Есть лишь пара занятий лучше, чем петлять ночью вдоль железки. \n \nСобирайтесь, 007, ибо деревня в блудняк таки попала. \nТрек: https://map.vault48.org/nochnaya_prigorodnaya \nЭлектричка: https://rasp.yandex.ru/thread/R_6627_9610189_g23_4?departure_from=2023-06-02&station_from=9610189 \n \nЕдем к первой электричке из Черепаново в пять утра. Асфальт/грейдер/грунт, местами щебеня. Клещедромов не будет. \nИз-за неприкаянного отребья с марафоном по поднятию статей с пола, возможны сюрпризы на участке вдольждката.",
|
||||||
|
"created": "2023-05-30 06:00:00",
|
||||||
|
"date": "2023-06-02 21:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "29.05.2023\nВелобратство, представляю вашему вниманию небольшую покатуху))) \nПокатуха включает в себя много асфальта, немного грунтов по парку, озеро с уточками и рыбками. Протяжённость 23км. \nВстречаемся в ПКиО \"Берёзовая Роща\", Старт 18:00 \nВ конце маршрута будет возможность подкрепиться вредной, но вкусной пищей🍔🍕🌭🌮🌯 \n \nhttps://map.vault48.org/p73s8e0ZrXbbDr5LIzxVLp49 \n \nP.S.: Маршрут откатан, остановки предусмотрены😉😉😉 \nДля подкорми уточек и рыбок советую взять хлебушка😊😊😊 \nБуду рад новым знакомствам",
|
||||||
|
"created": "2023-05-28 22:56:35",
|
||||||
|
"date": "2023-05-29 18:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "⚡24.05 (сегодня), 19:30 ГПНТБ\n\n20 ленивых вечерних километров для всех. Для нераскатавшихся, раскатавшихся но уставших, не уставших, но желающих просто спокойно покрутить педали после рабочего дня, без цели и смысла\n\nhttps://map.vault48.org/20",
|
||||||
|
"created": "2023-05-24 12:51:56",
|
||||||
|
"date": "2023-05-24 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "ПВД 5 ВОДОПАДОВ 27-28 мая. \nМаршрут довольно простой. Всего 160 км на два дня. Ночуем в палатках, травим байки у костра. \n\nСбор у касс пригородного вокзала НСК-Главный в субботу в 6:30.\nЕдем на электроне 6605, если кто поедет с других станций ориентируйтесь сами. От Главного он уходит в 6:53.\nСтарт в Дорогино в 9:20.\nФиниш в Черепаново в воскресенье в 18:00.\n\nТемп неспешный. Едем все вместе, не лосим, но и не тупим. Всех ждём. \n\nПоход будет проходить в формате самообеспечения, поэтому все спальные и едальные принадлежности берите с собой. Денег также берите. Магазины будут в Медведском, Никоново, Пеньково, Бураново.\n\nЖелающие могут взять купальники и открыть купальный сезон. \n\nВопросы в комменты. \nКто поедет - отпишитесь или ещё как нибудь дайте знать о своём намерении.",
|
||||||
|
"created": "2023-05-24 12:45:50",
|
||||||
|
"date": "2023-05-27 06:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Выходные в Тальменском лесу (27 - 28 мая)\n\nДанная практика уже применялась, так что, повторюсь) \n\nМесто старта любое, время выбираете сами, главная задача добраться к берегу. \n\nПланирую в субботу вечером быть на месте (кому будет нужно, кину локацию). На следующий день хочу тропами выбраться на Искитим, далее на электричке в город. \n\nhttps://map.vault48.org/talmenka_light\n\n54°45′16.96″N 83°14′12.73″E",
|
||||||
|
"created": "2023-05-23 21:13:33",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "⚡⚡⚡вторник 23 мая, старт 19-30 ГПМТБ\nЕдем в аэропорт. Делаем красивые фоточки с ТУшками и обратно. Все",
|
||||||
|
"created": "2023-05-23 16:17:56",
|
||||||
|
"date": "2023-05-23 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня. Воскресенье. 21.05.23 в 19:00. Где-то тут по стрелочкам.\n\nВстреча любителей встретиться на велосипедах. Это не покат, а чисто встреча))",
|
||||||
|
"created": "2023-05-21 16:23:28",
|
||||||
|
"date": "2023-05-21 19:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Воскресенье 21 мая. 8-00 речной вокзал.\n\nИдём вниз по Оби на теплоходе \nв 11-00 будем в Седовой заимке.\nОбратно на пидалках 50 км. Вдоль реки. \nПо писочку и лесам с небольшим вкраплениями асфальта. \nМагазинов по пути достаточно.\n\nТем кто как и я не любить, воду могу предложить сушой доехать до заимки. \n\nКому интересно пишите в телегу: @wek87\n\nТрек рисовать лень. Всё равно не кто не поедет. \nНо если кто то хочет поехать и боится заблудиться. Пишите в комменты. Нарисую скину.",
|
||||||
|
"created": "2023-05-20 10:11:22",
|
||||||
|
"date": "2023-05-21 08:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "19-21 мая, пятница - воскресенье. \nНовососедово - Пещерка - Озерки. Вдоль Салаирского кряжа. \n \nРаскатки позади, посмотрим, насколько тебе удалось похудеть к лету. Начинаем в Искитимских горах и едем гораздо дальше привычного Маслянино, в иные края, точнее, край. Любишь гонять сов по тайге, но воротит от палаточной бытовухи - пиши мне в [https://vk.com/radiotrance_54|личку]. От тебя потребуется увлеченно полуторасотить в грейдерные торчки, не бояться встреч с Винни-Пухом и постараться не дойти до края раньше, чем доедешь. Нерасторопность ведущего тебе в помощь. \n \nЗаброска/выброска электрическими поездами, старт вечером в пятницу. \nТрек и ночевки: https://map.vault48.org/vdol_salairskogo_kryazha_2 \n \nВ работу ушла режиссерская версия маршрута с блудняком в районе ст.Озёрки. Бюджет составил 3.5 тысячи рублей. Тестовые показы прошли успешно, за исключением сюжетного твиста с финалом в Камне-на-Оби вместо Новосибирска. Выглядит натянуто и недостоверно, но на работу на всякий звякни.",
|
||||||
|
"created": "2023-05-16 00:43:45",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Пятница, 12 мая \nСтарт в 20:00 от часовни на Монументе \nРасстояние: 56 км, примерно треть гравия и грунтов, остальное асфальт \nКарта: https://map.vault48.org/alekseevka_2023 \n \nА вместо анонса вот вам лучше четверостишие из Stairway to Heaven: \n \nThere's a feeling I get when I look to the West \nAnd my spirit is crying for leaving \nIn my thoughts I have seen rings of smoke through the trees \nAnd the voices of those who stand looking",
|
||||||
|
"created": "2023-05-10 18:37:20",
|
||||||
|
"date": "2023-05-12 20:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "⚡⚡⚡Молния ⚡⚡⚡\n\nСЕГОДНЯ 8 МАЯ!!!\n\nЛайт покат \nСтарт в 19:30 у фонтана в Берёзовой рощи\nТемп размеренный, не лосиный, отстающих ждём\n\nЕдем по проспекту Дзержинского до Авиастроителей, сворачиваем на грунты и вдоль Чкаловского завода едем до Каменки, смотрим как поживает озеро и обратно по Каменскому шоссе на берёзовую рощу. По желанию участников можно выехать полями на Амбулаторную или через частный сектор на ГБШ\n\nhttps://map.vault48.org/na_kamenku_cherez_chkalovskij_za",
|
||||||
|
"created": "2023-05-08 15:51:24",
|
||||||
|
"date": "2023-05-08 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Суббота, 6 мая.\nСтарт в 12:00 от часовни на монументе.\nМаршрут: https://map.vault48.org/zavod-zaton\nОт завода до затона.\n\nЕсли соточка до Тогучина это для вас пока что слишком, то у меня есть альтернативный покат для ленивого утра субботы.\n\n30 километров разнообразнейшего покрытия, немного индустриальной безысходности, немного кищащих клещами зарослей, ну и конечно же парочка помоек по пути — все это в темпе только что пришедшего со смены упахавшегося работяги. На этот раз точно никакого лосизма.",
|
||||||
|
"created": "2023-05-05 13:36:48",
|
||||||
|
"date": "2023-05-06 12:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Пиво-кат\nСуббота 6 мая 10:00\nНарымский сквер у фонтана\n111 асфальта.\n700 с копейками набора.\nПоеду с горки быстро в горку медленно средняя 22+\nПиво с завода. Рыба с завода. \nОбратно на электричке. \n15:07, 16:35, 20:47\n\nhttps://map.vault48.org/za_pivom",
|
||||||
|
"created": "2023-05-05 11:01:44",
|
||||||
|
"date": "2023-05-06 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Четверг, 4 мая.\nСтарт в 20:00 из Первомайского сквера.\nМаршрут: https://map.vault48.org/nvs_vanilla\n\nПредыдущий свой покат я был вынужден пропустить по уважительным причинам (у меня справка есть), но потребность в раскатке никуда не делась.\n\nПриглашаю вас принудительно неспешно проехать один из самых ванильных НВС-маршрутов: до крематория и обратно.\nДистанция булочная, темп булочный, настроение булочное.\nПриезжайте, потрещим коленками вместе!",
|
||||||
|
"created": "2023-05-03 22:28:00",
|
||||||
|
"date": "2023-05-04 20:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Привет! Собираюсь в поход, пока один) 5-го мая после обеда в 15-16 выезжаем из Новосиба до Бийска (я на машине, могу одного человека с велосипедом взять), ночуем и утром 6-го выезжаем на велах до Горно-Алтайска (105 км). 7 мая: Горный – Черга 82 км. 8 мая Черга – Алтайское(грунты 60 км). 9 мая Алтайское – Бийск 80 км. На тачку и домой. Ночуем в палатках-кемпингах.",
|
||||||
|
"created": "2023-05-01 21:51:43",
|
||||||
|
"date": "2023-05-05 15:16:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Буузокат: Академ эдишн\nПонедельник, 1 мая, старт в 12:00 \nот фонтана в Первомайском сквере. \nМаршрут: https://map.vault48.org/buuzokat_akadem_edishn\nНеспешно едем есть.",
|
||||||
|
"created": "2023-04-30 23:34:50",
|
||||||
|
"date": "2023-05-01 12:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "ОТМЕНЁН:\n\nПереносится на более пивную погоду, а точнее на следующую субботу 6.10 в 10:00\n\nПиво-кат\n1 мая 10:00\nНарымский сквер у фонтана\n111 асфальта.\n700 с копейками набора.\nПоеду с горки быстро в горку медленно средняя 22+\nПиво с завода. Рыба с завода. \nОбратно на электричке. \n15:07, 16:35, 20:47\n\nhttps://map.vault48.org/za_pivom",
|
||||||
|
"created": "2023-04-29 13:28:05",
|
||||||
|
"date": "2023-05-01 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Пятница, 28 апреля. \nСтарт в 20:00 от фонтана на ГПМТБ. \nМаршрут: https://map.vault48.org/raskatka \n \nРаскатка! Вот так вот, рас, рас и готово, рас, рас и готово! \n \nЧто тут ещё можно сказать... колени сами себя не раскатают. \nЗаползём в горочку на планетарии, а там, глядишь, дальше уже полегче должно пойти.\n \nВсего 34 километра, в которых будет всего понемногу: асфальт и гравий, вверх и вниз, вправо и влево, ветер в лицо и в спину. \nНа финише блины, всё как полагается. \nПро темп не лучше не спрашивайте — посоветую лишь надеяться на лучшее, но готовиться к худшему. Как это понимать, тоже не спрашивайте. \n \nНу вот, вроде, и всё: анонс написал, картинку и песенки к посту приложил, осталось только не забыть приехать на старт.",
|
||||||
|
"created": "2023-04-26 20:25:13",
|
||||||
|
"date": "2023-04-28 20:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Здравствуйте, люди добрые. Извините, что к вам обращаемся, но завтра (в четверг) состоится традиционное хмельное велотурне. Отбытие в 18:30 с пл. Маркса. Общий путь - 2,6км. На этот раз на свежем воздухе.\n\nПри наличии осадков или резком похолодании турне отменяется, билеты возврату не подлежат.",
|
||||||
|
"created": "2023-04-26 13:25:34",
|
||||||
|
"date": "2023-04-27 18:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "7-9 мая, воскресенье - вторник. \nКузбасс. 200 километров вдоль Томи + музей Томская писаница. \n \nДофаминовая экспресс-раскатка для тех, кому надоел Северный объезд. Едем по дорогам между Транссибом и Кемерово, туда по одному берегу Томи, обратно по другому. Небольшой трафик, большой рельеф, первая зелень на деревьях и ночевка в монгольской юрте на территории заповедника. Бонусом понтонный мост через Томь + знаменитые юргинские шашлыки. Кто слышит про Писаницу впервые - фото с прошлой поездки: https://vk.com/album11412570_292746606 \n \nОкситоцин ожидается в виде жареной картошки и теплой постели. По возвращении будет достаточно времени, чтобы занять любимое место под салют. \nТрек: https://map.vault48.org/tomskaya_pisanica/ \nДля участия пишите в [id11412570|личку]. \n \np.s. Несмотря на небольшие пробеги (100+60+30км), трезво оценивайте свои силы в начале сезона. Электричка не девочка, ждать не будет.",
|
||||||
|
"created": "2023-04-26 06:00:00",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "А вот уже и видео готово с сегодняшней покатушки. \n\nНарод - вы все огонь🔥 \nСегодня был офигенский покат🤟 Погодка как подстроилась под нас и команда выдалась бодрой.\n\nНадеюсь никого сильно не смутили говна в районе Коена🐷 Во всем остальном вроде все было классно👍 Так или иначе жду отзывов и комментариев от участников.",
|
||||||
|
"created": "2023-04-23 22:23:31",
|
||||||
|
"date": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Воскресенье 23 апреля. \nИскитим - Академ 50 км, с возможностью увеличить пробег.\n\nСбор в 8 утра у касс пригородного вокзала НСК-Главный \nСтарт в Искитиме в 10:00\nФиниш в Академе примерно в 16:00, дальше по желанию - либо своим ходом до города, либо на электроне.\n\nВ планах развести костёр на берегу Коена и пожарить сосисек. Ну и конечно прокатиться немного😀\n\nТемп неспешный, но и без тупняков, хочется в город вернуться к 5 часам.\n\nhttps://map.vault48.org/iskitim_-_akadem",
|
||||||
|
"created": "2023-04-21 21:31:16",
|
||||||
|
"date": "2023-04-23 10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Воскресенье 23.04 Нарымский Сквер возле фонтана в 15-00\nМаршрут по асфальту \nСильно ломить не буду. Скорость 20-25 км/ч\n\nhttps://map.vault48.org/230423",
|
||||||
|
"created": "2023-04-21 18:57:57",
|
||||||
|
"date": "2023-04-23 15:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Воскресенье. 04.06.23 (сегодня) 19:30. Место старта: Valhalla, Владимира Заровного, 40. Место финиша: Valhalla, Владимира Заровного, 40. Маршрут: https://map.vault48.org/GEIiPYjH0VEQTUu4K6H5pdMS . Протяженность маршрута: 0. Темп: как получится, по времени тоже. На шоссее маршрут хорошо проезжаем, да и на фиолетовом пухляше не будет проблем. На маршруте возможно увидим как ставится мат в 2 хода, а может быть даже узнаем как правильно играть в пешечном эндшпиле.",
|
||||||
|
"created": "2023-06-04 13:12:25",
|
||||||
|
"date": "2023-06-04 19:30:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Сегодня (понедельник 19.06.23). Старт в 19:00. Поедем на бережок, там посидим. Место сбора: улица Немировича-Данченко, 150. Маршрут не длинный, должны вывезти все.",
|
||||||
|
"created": "2023-06-19 10:36:52",
|
||||||
|
"date": "2023-06-19 19:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "14.10.2023. Осенний Базой.\n300 километров асфальтового счастья с прекрасным кафе посредине. Кедровые шишки для фанатов кедровых шишек.\n\nСтарт в 8:00 от ГПНТБ, финиш там же.\nТрек: https://map.vault48.org/ZAia2ryrc7sOLt5iSxcb07fA",
|
||||||
|
"created": "2023-10-13 15:36:11",
|
||||||
|
"date": "2023-10-14 08:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Закрытие велосезона 2023. \nВоскресенье, 31.12.2023. 13:00 Маркса. Памятник Покрышкину (точнее там где он был раньше). \n \nНикогда не было и вот опять. Уже классическая предновогодняя покатушка с Маркса через Монумент, парк Кирова, на правый берег по Димитровскому мосту, оперный театр, и до ГПМТБ. Несколько фоточек с закрытий сезонов прошлых лет. Погода по прогнозу огонь, так что сокращение маршрута маловероятно. Наличие термосов с горячим чаем и мандаринок обязательно!",
|
||||||
|
"created": "2023-12-27 11:06:13",
|
||||||
|
"date": "2023-12-31 13:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Аэропортокат: Winter edition\nСуббота, 30 декабря 2023, старт в 11:00 \nот Администрации Ленинского района (Станиславского, 6а).\nМаршрут: https://map.vault48.org/aeroportokat_winter_edition_",
|
||||||
|
"created": "2023-12-28 22:49:20",
|
||||||
|
"date": "2023-12-30 11:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Открытие велосезона 2024\nМесто сбора - Зоопарк\nВремя сбора/старта - 31 декабря, 19-45/20-00\nМаршрут - https://map.vault48.org/gja07ruVbEMfB0Pgh4KV8thZ\n\nХО ХО ХО! Если кто-нибудь хочет встретить НГ в лесу у костра на уютной полянке, нарядить живую ёлку, пожарить зефирки, опрокинуть кружку безалкогольного глинтвейна - присоединяйтесь.\n\nПоеду на велосипеде в любом случае. Если совсем завалит снегом, буду просто использовать вел как вьючного ишака. Если не завалит, сделаю кружок через Дачное шоссе. Съестные припасы и развлечения беру с собой, разумеется. Жду нули, затем - вольная программа.",
|
||||||
|
"created": "2023-12-29 21:29:04",
|
||||||
|
"date": "2023-12-31 19:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "Каракан 11 – 12 июня 2023: Итоги \n \n+ Покат был максимально лайтовым. Никакого лосизма, никаких продираний сквозь заросли, никаких тасканий велов, никаких преодолений. \n+ Путь от дома до пункта назначения занял всего 9 часов. Примерно в 15:10 были на берегу, а это значит, что времени на отдых, многократное употребление еды и исследование окрестностей осталось предостаточно. \n+ Пыльная дорога, отсыпанная щебнем между Быстровкой и Завьялово настолько короткая (около 7 км и преодолевается примерно за 20 минут) что неудобствами, которые она доставляет, можно пренебречь. \n+ Лесная дорога от Факела Революции до берега безупречна. Только ради неё стоит ехать в Каракан. Каждый велосипедист хотя бы раз в жизни должен по ней проехать. \n+ С клещами не сталкивались, комары встречались редко, были добрые и совсем не кусались. \n+ Обратно выехали не очень рано, часов в 10. В город приехали не очень поздно, часов в 19 и это, пожалуй, идеально. Достаточно времени на то чтобы выспаться и разобрать снарягу. \n \nНевыносимую печаль в этой поездке вызывали вереницы лесовозов и горы из стволов деревьев вдоль дороги. Не упускайте возможности скатать в Караканский бор, пока его не выпилили. \n \nФотографии с поката: https://vk.com/album-124752609_293941505\nПрямые трансляции НВС покатов: https://t.me/pogonia_live \nПодразделение НВС для тех, кто любит писать, что не приедет на покат: https://vk.com/nvs_sportloto",
|
||||||
|
"created": "2023-06-13 23:13:27",
|
||||||
|
"date": "2023-06-11 15:10:00"
|
||||||
|
}
|
||||||
|
]
|
32
src/utils/date/__tests__/getDateFromText.synthetic.json
Normal file
32
src/utils/date/__tests__/getDateFromText.synthetic.json
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"text": "Во вторник сбор в 09:45, старт в 09:50, конкретной даты нет, ищем следующий вторник (написано в пятницу)",
|
||||||
|
"created": "2023-08-26 15:47:47",
|
||||||
|
"date": "2023-08-29 09:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "В пятницу сбор в 09:45, старт в 09:50, конкретной даты нет, видимо, в следующую пятницу (написано в пятницу)",
|
||||||
|
"created": "2023-08-26 15:47:47",
|
||||||
|
"date": "2023-09-01 09:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "3 января, в 19:45 (написан в декабре на январь след. года)",
|
||||||
|
"created": "2023-12-29 21:29:04",
|
||||||
|
"date": "2024-01-03 19:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "3 февраля, в 19:45 (написан в декабре на февраль след. года)",
|
||||||
|
"created": "2023-12-29 21:29:04",
|
||||||
|
"date": "2024-02-03 19:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "3 мая, в 19:45 (написан в декабре на май след. года)",
|
||||||
|
"created": "2023-12-29 21:29:04",
|
||||||
|
"date": "2024-05-03 19:45:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"text": "3 сентября, в 19:45 (написан в декабре на сентябрь, который далековато, поэтому будет в том же году)",
|
||||||
|
"created": "2023-12-29 21:29:04",
|
||||||
|
"date": "2023-09-03 19:45:00"
|
||||||
|
}
|
||||||
|
]
|
28
src/utils/date/__tests__/getDateFromText.test.ts
Normal file
28
src/utils/date/__tests__/getDateFromText.test.ts
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import { format, parse } from "date-fns";
|
||||||
|
import { getDateFromText } from "../getDateFromText";
|
||||||
|
|
||||||
|
|
||||||
|
interface Case {
|
||||||
|
text: string,
|
||||||
|
created: string,
|
||||||
|
date: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getDateFromText", () => {
|
||||||
|
const real: Case[] = require('./getDateFromText.real.json');
|
||||||
|
const synthetic: Case[] = require('./getDateFromText.synthetic.json');
|
||||||
|
|
||||||
|
it.each(real)("(real case) $text", ({ text, created, date }) => {
|
||||||
|
const createdDate = parse(created, "yyyy-MM-dd HH:mm:ss", new Date());
|
||||||
|
const result = getDateFromText(text, createdDate);
|
||||||
|
|
||||||
|
expect(result ? format(result, "yyyy-MM-dd HH:mm:ss") : "").toBe(date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(synthetic)("(synthetic case) $text", ({ text, created, date }) => {
|
||||||
|
const createdDate = parse(created, "yyyy-MM-dd HH:mm:ss", new Date());
|
||||||
|
const result = getDateFromText(text, createdDate);
|
||||||
|
|
||||||
|
expect(result ? format(result, "yyyy-MM-dd HH:mm:ss") : "").toBe(date);
|
||||||
|
});
|
||||||
|
});
|
34
src/utils/date/getDateFromText.ts
Normal file
34
src/utils/date/getDateFromText.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
import { addYears, differenceInMonths, isBefore } from "date-fns";
|
||||||
|
import { getDayMonthFromText, getTimeFromString } from "./getDayMonthFromText";
|
||||||
|
|
||||||
|
export const getDateFromText = (
|
||||||
|
val: string,
|
||||||
|
createdAt: Date
|
||||||
|
): Date | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
|
||||||
|
const dayMonth = getDayMonthFromText(text, createdAt);
|
||||||
|
if (!dayMonth) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = getTimeFromString(text);
|
||||||
|
if (!time) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = new Date(
|
||||||
|
createdAt.getFullYear(),
|
||||||
|
dayMonth[1],
|
||||||
|
dayMonth[0],
|
||||||
|
time[0],
|
||||||
|
time[1]
|
||||||
|
);
|
||||||
|
|
||||||
|
// handle posts written in november-december for next year's january-february
|
||||||
|
if (isBefore(date, createdAt) && differenceInMonths(addYears(date, 1), createdAt) < 5) {
|
||||||
|
return addYears(date, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return date;
|
||||||
|
};
|
158
src/utils/date/getDayMonthFromText.ts
Normal file
158
src/utils/date/getDayMonthFromText.ts
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
import { addDays, differenceInDays, startOfISOWeek } from "date-fns";
|
||||||
|
|
||||||
|
const months = [
|
||||||
|
"янв",
|
||||||
|
"фев",
|
||||||
|
"мар",
|
||||||
|
"апр",
|
||||||
|
"мая",
|
||||||
|
"июн",
|
||||||
|
"июл",
|
||||||
|
"авг",
|
||||||
|
"сен",
|
||||||
|
"окт",
|
||||||
|
"нояб",
|
||||||
|
"дек",
|
||||||
|
];
|
||||||
|
|
||||||
|
const daysOfWeek = [
|
||||||
|
"понед",
|
||||||
|
"втор",
|
||||||
|
"сред",
|
||||||
|
"четв",
|
||||||
|
"пятниц",
|
||||||
|
"субб",
|
||||||
|
"воскр",
|
||||||
|
];
|
||||||
|
|
||||||
|
type DayMonth = [number, number];
|
||||||
|
|
||||||
|
/** Searches for strings like 1 января */
|
||||||
|
const byText = (val: string): DayMonth | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
|
||||||
|
// matches "12-22 мая" or "12 - 22 мая"
|
||||||
|
const matchWithDash = text.match(new RegExp(`([1-2][0-9]|0?[1-9]|3[0-1])(?:(?: - |-| – |–)\\d{1,2}) (${months.join("|")})`));
|
||||||
|
// matches 12 мая, 12-го мая
|
||||||
|
const rer = new RegExp(`([1-2][0-9]|0?[1-9]|3[0-1])[\\-\\–A-Za-zА-Яа-яЁё ]{1,4}(${months.join("|")})`);
|
||||||
|
const match = text.match(rer);
|
||||||
|
|
||||||
|
if (!match?.length && !matchWithDash?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = parseInt(matchWithDash?.[1] ?? match?.[1] ?? '');
|
||||||
|
const month = months.indexOf(matchWithDash?.[2] ?? match?.[2] ?? '');
|
||||||
|
|
||||||
|
if (
|
||||||
|
!Number.isFinite(day) ||
|
||||||
|
!Number.isFinite(month) ||
|
||||||
|
day < 1 ||
|
||||||
|
day > 31 ||
|
||||||
|
month < 0 ||
|
||||||
|
month > months.length - 1
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [day, month];
|
||||||
|
};
|
||||||
|
|
||||||
|
const byNumber = (val: string): DayMonth | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
|
||||||
|
const match = text.match(/(?<![\.|\d|\w])(0?[1-9]|[1-2][0-9]|3[0-1])\.(1[0-2]|0[1-9])/);
|
||||||
|
if (!match?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const day = parseInt(match[1]);
|
||||||
|
const month = parseInt(match[2]) - 1;
|
||||||
|
if (
|
||||||
|
!Number.isFinite(day) ||
|
||||||
|
!Number.isFinite(month) ||
|
||||||
|
day < 1 ||
|
||||||
|
day > 31 ||
|
||||||
|
month < 0 ||
|
||||||
|
month > 11
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [day, month];
|
||||||
|
};
|
||||||
|
|
||||||
|
const byToday = (val: string, refDate: Date): DayMonth | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
if (text.match(/сегодня/)) {
|
||||||
|
return [refDate.getDate(), refDate.getMonth()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.match(/завтра/)) {
|
||||||
|
const tomorrow = addDays(refDate, 1);
|
||||||
|
return [tomorrow.getDate(), tomorrow.getMonth()];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (text.match(/послезавтра/)) {
|
||||||
|
const tomorrow = addDays(refDate, 2);
|
||||||
|
return [tomorrow.getDate(), tomorrow.getMonth()];
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
const byDayOfWeek = (val: string, refDate: Date): DayMonth | undefined => {
|
||||||
|
const text = val.toLowerCase();
|
||||||
|
const regexp = new RegExp(`(${daysOfWeek.join("|")})`);
|
||||||
|
const match = text.match(regexp);
|
||||||
|
if (!match?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dayOfWeek = daysOfWeek.indexOf(match[1]);
|
||||||
|
if (dayOfWeek < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const refDayOfWeek = differenceInDays(refDate, startOfISOWeek(refDate));
|
||||||
|
const distanceFromCreatedAt = dayOfWeek > refDayOfWeek
|
||||||
|
? (dayOfWeek - refDayOfWeek)
|
||||||
|
: (7 - (refDayOfWeek - dayOfWeek)); // turn to next week
|
||||||
|
|
||||||
|
const date = addDays(refDate, distanceFromCreatedAt);
|
||||||
|
return [
|
||||||
|
date.getDate(),
|
||||||
|
date.getMonth()
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Note: months start with 0, days start with 1 */
|
||||||
|
export const getDayMonthFromText = (val: string, refDate: Date) =>
|
||||||
|
byText(val) ?? byNumber(val) ?? byToday(val, refDate) ?? byDayOfWeek(val, refDate);
|
||||||
|
|
||||||
|
export const getTimeFromString = (
|
||||||
|
val: string
|
||||||
|
): [number, number] | undefined => {
|
||||||
|
// 16:45
|
||||||
|
const matches = val.match(/(?<![\.|\d|\w])([01]?[0-9]|2[0-3])\:([0-5][0-9])(?![\d|\w])/);
|
||||||
|
|
||||||
|
// 16-45
|
||||||
|
const dashRegexp = new RegExp(`(?<![\\.\\d\\wа-яА-ЯЁё])([01]?[0-9]|2[0-3])[\\-\\–]([0-5][0-9])(?![\\d\\wа-яА-ЯЁё]|(?: (?:${months.join("|")})))`);
|
||||||
|
const dashMatches = val.match(dashRegexp);
|
||||||
|
|
||||||
|
// some people specify time as 19.45, it's weird, but let's try to parse it
|
||||||
|
const dotMatches = val.match(/(?<![\.|\d|\w])([1-9]|[1|2][0-9])\.(1[3-9]|[2-5][0-9])(?![\d|\.|\w])/);
|
||||||
|
|
||||||
|
if (!matches?.length && !dotMatches?.length && !dashMatches?.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hours = parseInt(matches?.[1] ?? dashMatches?.[1] ?? dotMatches?.[1] ?? '');
|
||||||
|
const minutes = parseInt(matches?.[2] ?? dashMatches?.[2] ?? dotMatches?.[2] ?? '');
|
||||||
|
|
||||||
|
if (hours < 0 || hours > 24 || minutes < 0 || minutes > 60) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [hours, minutes];
|
||||||
|
};
|
6
src/utils/text/getSummaryFromText.ts
Normal file
6
src/utils/text/getSummaryFromText.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/** Makes summary from first 3 strings of text */
|
||||||
|
export const getSummaryFromText = (text: string) => {
|
||||||
|
const match = text.match(/(.*\n?){0,3}/)
|
||||||
|
|
||||||
|
return match?.[0] ?? '';
|
||||||
|
}
|
1
src/utils/text/maybeTrim.ts
Normal file
1
src/utils/text/maybeTrim.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const maybeTrim = (text: string, maxChars: number) => text.length > maxChars ? `${text.slice(0, maxChars)}...` : text;
|
|
@ -13,10 +13,8 @@
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {},
|
"paths": {},
|
||||||
"lib": ["esnext"],
|
"lib": ["esnext"],
|
||||||
"strict": true
|
"strict": true,
|
||||||
|
"skipLibCheck": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./src/index.ts", "./custom.d.ts"]
|
||||||
"./**/*",
|
|
||||||
"./custom.d.ts"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue